roxygen2 rebuilds NAMESPACE from @export* tags, so renaming a tagged function
changes the package's public API on the next document() even when the NAMESPACE on
disk and the persisted isExported say it is private. checkSafety now refuses a
top-level function whose directly preceding #' block has an @export* tag. Documents
that a successful rename leaves .Rmd/.qmd/.Rd/pkgdown/roxygen cross-reference
consumers unchanged; the rename result has no provider-owned note field.
parse-worker.ts and languages/r/query.ts imported @eagleoutice/tree-sitter-r
statically. A binding that cannot load (no prebuild, ABI mismatch) therefore
crashed every worker and every module importing the R provider, failing analyze
for all languages. The worker now loads it behind the same guard as the vendored
grammars, through the shared grammar loader, and the query module resolves it on
first use as the Kotlin one does. With the binding removed, analyze indexes the
other languages and skips R files with one logged error.
The NAMESPACE scanner skipped conditional (if/else) bodies, dropped raw-string and
multi-line arguments, and discarded a malformed directive together with everything up
to the next column-0 directive, all silently. Rename treated 'not in the export lists'
as proof of privacy, so it could rename a function whose export() entry the reader had
never seen and leave NAMESPACE naming a missing function.
The scanner now reports when a skipped or discarded region names an export-family
directive, and parseRNamespaceExports also flags an argument of one that it cannot
read. loadRPackageConfig stores that as RNamespaceInfo.exportsUnreadable and rename
refuses with incomplete_semantics. An ordinary NAMESPACE, including conditional
imports, does not set it. The indexed isExported is unchanged. r-provider.md updated.
populateRNamespaceImports and populateRQualifiedCalls replace entries of the
ParsedFile array (import synthesis; any dropped external pkg::fn site), but the
rename guard looks the recorded config up by the original ParsedFile object. A
package file that called stats::sd(), or whose package importFrom()ed a name from
another repository package, therefore read as 'NAMESPACE was not read' and every
rename in it was refused with incomplete_semantics.
Record the config against the objects as received and again against the final
array. Adds rename fixtures for a private and an exported function in a file
with an external qualified call, a private function in a package with a local
importFrom(), and extends the incremental NAMESPACE test with an external call
and an incremental run that reuses the parse cache.
After an edit of NAMESPACE alone, the unchanged R/ files are not
re-processed, so the persisted isExported of a newly exported function
stays false and rename read that as proof of privacy. The rename guard now
also uses the NAMESPACE the planner has just loaded from disk (recorded by
the R workspace hook against each parsed file): it refuses when NAMESPACE
exports the name by export(), exportPattern() or S3method(), and fails
closed (incomplete_semantics) when NAMESPACE is gone or unreadable, when
an exportPattern() was dropped or ran out of budget, or when no config was
recorded. The persisted flag remains a second, additive check. The
incremental test now asserts the refusal on the stale index and after
--force; unit cases cover each way NAMESPACE can change. Docs updated.
R leaves an already attached package where it is, so library(a);
library(b); library(a) still lets b mask a. The merger counted the last
call as a fresh attachment, and because the pipeline folds the bindings of
a name in one at a time, a package masked at one step was forgotten at the
next. Remember each package's first attachment, masked ones included, on
the surviving bindings, and flip the expectations that pinned the other
direction (A/B/A -> B, B/A/B -> A) in the unit and pipeline tests.
`publicRepoId` is an HMAC under a key drawn once per process
(`randomBytes(32)`), so the 22-character base64url id is effectively
random on every run. The test asserted `not.toContain('ws')` to show the
id does not leak the path, but any id contains "ws" by chance with
probability 21/4096 (~0.5%). CI hit it on #3624 with
`gt5f4oNtpcwsi6vep8lzQN`.
Across 20,000 fresh keys the old assertion failed 118 times and the new
one 0 times. The test now checks the base64url shape and that the full
path is absent; '/' is outside the alphabet, so a chance match cannot
happen.
Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
validateName let ... and ..N through to a misleading name_collision. Reject
them as reserved, say in the message that new names are ASCII-only by design,
and report an old name made only of non-ASCII characters as an unsupported
identifier rather than an infix function or operator.
A formal defaulting to NULL is the usual placeholder of an optional
callback, so classing it as constant let a rename edit the call of the
parameter and drew a false edge to a same-named top-level function. Treat it
as 'maybe': the rename refuses the call and the scope publishes the formal
as a lexical name. Update the provider guide and pin both paths.
rExportPatternMatches answered false for every name once the budget was
spent, so a duplicate definition of a name that had been exported earlier
flipped to private. Remember the names matched while the budget lasted and
answer them from that set; undecided names still read as unexported, which
the documentation now says.
normaliseArg trimmed the text inside quotes and backticks, so export(" a ")
named the symbol a instead of the symbol ' a ', which is what R reads. Trim
only around the argument, and pin it with a parser test and a pipeline case
where a package defines both a and ` a `.
Build the pattern from empty groups so only the length rule is in play, and
check that it compiles, matches 'aa' and not 'a', and that one more
character is refused.
getRTopLevelMethodOwnerName is the setMethod helper and returns null for any
argument node, so the negative assertion could not fail. Use the owner lookup
the R6 path uses, in the positive and the negative case.
The helper listed only 'name <- function', while the indexer also lists
<<- and = definitions, so a generic defined with = never reached the S3
guard. Add cases that go through the helper with each operator.
rMergeBindings kept the first binding of a node, so a batched call with a
package named by several library() calls was order-sensitive. Keep the
binding whose call starts later, add a unit case and pin the ABA and BAB
shapes through the pipeline.
On LadybugDB 0.18.3 (1.6.12, 1.6.13-rc.57) every CHECKPOINT left its
shadow file handle's frame groups charged against max_db_size until the
database closed (LadybugDB #924, fixed by #925 before 0.21.1): about
8-9 MiB per checkpoint, empty ones included. Analyze checkpoints every
5 s and per embedding window, so a long run can report "Maximum database
size ... has been reached" while the file is small, and a retry in a
fresh process succeeds.
The new native test opens the database through initLbug with a 256 MiB
ceiling and issues 128 driver-path checkpoints (tryFlushWAL). It fails
on 0.18.3 with the reporter's message and passes on 0.21.1, so a future
engine pin that reintroduces the leak is caught. Registered in the
lbug-db project and the cross-platform native list.
Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
LadybugDB 0.18.3 (shipped in 1.6.12 and 1.6.13-rc.57) misreads string
columns after a selective write. The CodeEmbedding table is affected by
the embedding writeback's own statements, without any COPY: after
per-node DELETE + CREATE and a checkpoint, 3,936 of 8,192 tuples scan
wrong and the {nodeId} lookup misses a retained row. On a released
build this makes unchanged nodes look new (re-embedding) and puts blank
or truncated ids into the CodeEmbedding key ("duplicated primary key").
The 0.21.1 engine on main fixes it.
- reproduce-search.cjs gains an `embedding` scenario using the
production CREATE / DELETE-by-nodeId statements; it fails on 0.18.3
and passes on 0.21.1. The vitest wrapper runs it with the other
scenarios; per-scenario expectations are now data, not branches.
- incremental-method-integrity runs both analyses with a deterministic
loopback embedding endpoint and asserts that only the edited file is
re-embedded and every other CodeEmbedding row (4,096+) is unchanged
from a cold reader.
Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* feat(eval): measure PR cost before merge
Reuse authenticated historical MCP measurements for one pinned PR preview. Keep the protected main evaluator and publish separate evidence that cannot authorize a release.
* fix(eval): gate PR cost runs on a label
Require a repository writer label event and retain the opt-in label
and exact PR head before dispatch and paid sessions. Keep the
request handler on trusted base code and execute evaluation on main.
* ci: document trusted cost label trigger
* fix(eval): restrict previews to maintainer labels
Remove manual PR preview inputs and authorize Maintain/Admin roles before calling the protected reusable evaluator. Compare workflow measurement settings with historical evidence instead of exempting the entire workflow.
---------
Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com>
rename-provider-r.test.ts was charged 100 s in WINDOWS_WEIGHTS_SEC but
takes 600-750 s on the Windows runner (49 persisted fixtures, each a
native analyze). The shard packer therefore placed it on shard 5/10 with
the go and dml-gate suites, which exhausted the 20-minute watchdog on
two consecutive attempts.
Charge it 620 s, add a regression test that keeps it off the go and
dml-gate shards, and raise the recorded-workload ceiling from 17 to 18
minutes: the table total grew by about 520 s, so a perfect ten-way
split now averages about 1047 s per shard.
* fix(rename): bind apply to reviewed plans (U7)
Retain exact semantic edits behind single-use preview tokens and reject
source, target, inventory and graph drift before application. Preserve
explicit partial-write accounting and keep large unedited sources covered
with hashes instead of retained text.
Refs #2973
* fix(rename): cover unindexed sources (U7)
Address PR #3617 review by binding every supported source to streamed byte
hashes and canonical targets, including tracked and untracked files absent
from the graph. Keep the exact reviewed edits and bound evidence memory.
Use conventional even-sample medians and extend the existing CI benchmark
to capture and validate unindexed sources without relaxing timing gates.
Note: pre-existing packed Tree-sitter GLIBCXX_3.4.31 host failure in
parse-cache-identity-package.test.ts is not addressed by this PR.
* fix(rename): preserve lazy startup (U7)
Keep language providers off the MCP startup path; load their existing
classification only when rename inventory is requested. The existing
12 startup closure checks now pass.
Address PR #3617 review by including token validation in reviewed apply
timing and growing indexed evidence before preview, preserving the
20MiB controls under a 256MiB native pool. Migrate dispatch and retention
tests to the preview token contract, keeping write-failure oracles.
Note: pre-existing auto-sync /tmp fixture security and installed ARM64
grammar-header failures remain outside this PR; the packed Tree-sitter
GLIBCXX_3.4.31 host failure also reproduces on unchanged main.
* test(rename): always remove apply fixture (U7)
Own the temporary directory from creation in an outer finally so setup
and preview assertions cannot bypass cleanup. Keep spy restoration in
its inner finally and preserve all write-failure assertions.
Address PR #3617 cleanup feedback.
* fix(rename): retain targets and config evidence
* test(rename): verify approved physical output
* test(rename): cover native filesystem matrix
* test(rename): align preview contract fixtures
---------
Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com>
r-provider.md documents this as a limitation of the shared incremental write path. Pin the
current behaviour (stale after an incremental analyse, correct after a forced one) so a fix
there shows up and the document is updated with it.
Control flow inside an expression (y <- if, switch, &&, ||, ifelse, return() or break inside
tryCatch/local), stop(), on.exit() and tryCatch handlers stay one block, and a break or next
outside a loop falls back to EXIT. Nothing exercised these forms.
The documented claim that a rename succeeds across files had only same-file successes behind
it, and the undetected paste0() lookup was unpinned. Add a qualified pkg::name() cross-file
rename and a run-time-built lookup name, and count the R cases so that an emptied fixture
directory cannot leave the suite green with no test.
Add the r-call-shapes fixture, a package that contains each shape r-provider.md says creates
no edge or node: lapply/Map/do.call/match.fun/get/registry/pkg::f as values, infix operators,
obj@slot, UseMethod/NextMethod, super$ and private$, inherit = pkg::Base, an inherited
self$ call, setClassUnion/setValidity/setReplaceMethod/Class$methods, roxygen @export,
exportClassPattern, renv parsing and the unread import forms. Positive controls sit beside
them, and the whole CALLS set is pinned.
The layout fixture already holds callers in tests/, scripts/, vignettes/ and inst/, but only
their export status was asserted. Pin every CALLS edge, with reason and confidence, for both
projects, so a misdirected edge that keeps the count fails.
The deferred-owner pass and the NAMESPACE export refinement only ever ran cold in the R
tests. Analyse r-packages and r-package-layout twice, the second time from the saved parse
cache with no worker pool, and require the same definitions (isExported, ownerId) and the
same CALLS, EXTENDS, IMPORTS and member edges.
rArityCompatibility had no test at all. populateRQualifiedCalls and
rFileBelongsToDiscoveredPackage are reached with no package config, and a run must not see
the previous run's answers; neither was asserted. The npm rebuild hint for the R grammar
names the scoped package; nothing checked it.
R matches argument names case-sensitively, so setClass("A", CONTAINS = "B") names no
base; the scope query linked it anyway. Drop the CONTAINS spelling from both predicates.
Pin the VIRTUAL placeholder: a class naming both a real base and VIRTUAL gets both edges
(the dedup set must be seeded only from edges that target the placeholder), the edge keeps
its confidence and reason, and running the heritage hook twice mints one node and one edge.
The caps in r-provider.md were asserted through the constants themselves, so changing a
cap, or the number the document gives, failed nothing. Pin each cap, check that the
document states the same number, and test the state cap, the pattern length cap, the
hidden-package scan budget and a spent matcher at their exact boundaries. Also pin the
backslash-u fallback to the letter u when fewer than four hex digits follow.
rExportChecker was a shared export that only the R provider used, and it made
export-detection.ts differ from upstream for a function that returns true.
ObjectiveC already writes the same checker inline.
The provider now carries it as () => true with the reason beside it, and
export-detection.ts is byte-identical to upstream again.
extractParameter runs for each formal and re-collected and re-parsed the whole
comment block above the function every time, so work grew as formals x block
lines (2,000 formals under 2,000 @param lines took 11.9 s).
The parsed block is now kept per assignment node in the tree, as the file's
class names already are.
The R provider header said library() and source() bring everything into scope
with no named imports and that imports are standard import nodes: source()
binds no name, NAMESPACE importFrom() produces named imports, and the import
captures come from the R scope query. The scope resolver said a bare member
call never reaches an R5 method and that R has no super, but RefClass methods
call siblings by bare name (unresolved today) and R6 has super$method().
rQualifierLocality scanned every parsed file path once per distinct pkg::
qualifier, so a run's work was qualifiers x files (4,000 x 5,000 took 0.84 s).
populateRQualifiedCalls now builds the set of <name>/R/ directory names that no
discovered package owns once, and each qualifier is a set lookup. The answers
are unchanged, and a call without the index still scans.
A formal or a for variable bound no name in the scope model, so
function(data, transform) transform(data) was credited with a call of a
top-level transform() at 0.85, and the unique-name fallback made the same guess
across files.
Each function scope now publishes the names its formals and for loops bind as
lexical names, the shadowing barrier the scope model already has for a
parameter. A formal with a constant default that the body never assigns is left
out, because R skips a value that is not a function when it looks up a callee
(the rename provider reads it the same way).
Two library() calls that attach packages defining the same name bound the call
to whichever package file sorted first by id, so a script that attached lp2
after lp reached lp's function while R reaches lp2's.
rMergeBindings now keeps, within the wildcard tier, only the bindings of the
library() call that comes last in the file. A package of several files that
defines the name twice stays ambiguous, and a binding with no call position
leaves the tier as it was.
A function bound inside another function lives in that call's frame, so no
other function in its file or another can call it by its bare name. The
unique-name fallback ignored that: when the name was unique in the repository,
a bare call elsewhere bound to the closure-local function at 0.5.
The R fallback veto now refuses a function or class that the file's scope
model holds below the top level. A candidate the scope model does not hold, and
every top-level definition, are judged as before.
Resolving an importFrom() name that the provider's NAMESPACE does not export
walked every export entry before giving up, so a provider with 20,000 exports
and a caller importing 20,000 names took about two seconds, and NAMESPACE text
is the only bound.
The walk only looked for entries that start or end with a backtick (a quoted
"`%+%`" entry keeps them). Those are now collected in one pass per export set,
and a lookup is a set membership test; the answers are unchanged.
Discovery read the disk on its own and skipped only renv, packrat, revdep and
.Rcheck, so an installed library under lib/, library/, .Rlibs/, vendor/ or
build/ registered its packages as repository packages (their qualified calls
then fell to the name-based guess instead of reading as external), and a tree
of ignored directories spent the 5,000-directory hidden-package budget and
made discovery read as incomplete.
Discovery now skips dot-directories and the directories the indexer itself
ignores (isHardcodedIgnoredDirectory), and a DESCRIPTION with a Built: field,
which only an installed package has, registers nothing and hides nothing. The
depth-3 and 200-directory bounds are pinned with their boundary values.