The ratio guard divided by a 15 ms floor on the small run, so shapes whose
small run took 1 to 2 ms (comments before the naming argument) could hide
a quadratic regression that cost less than 150 ms at 8,000 arguments. The
small size now doubles from 2,000 until its fastest run lasts 20 ms, and
the large run stays four times that size, so the ratio is taken between
runs long enough to measure. The largest shape runs for about 2.4 s.
The guide documents ambiguous_reference for an unqualified call into
another file and a refusal for R6 methods, but no fixture covered
either. Add both. The R6 method is refused by the provider as a class
member (unsupported_occurrence); the guide wrongly listed it among the
shared engine's stale_graph refusals, which now name only dotted
functions and S4 classes.
Member ids carry their class since fc32e539, so the two greet methods of
Parent and Child in s4_and_r6.R are two nodes instead of one. The
owner-absence test sits outside the R test set script and kept the old
count of 22.
The header said the suite hard-fails on CI like the other worker-backed
suites. Only object-literal-owner-resolution shares that tripwire; the
others skip silently when dist/ is missing. The header now says so, and
that the integration tier builds dist/ first.
The warning for a language whose grammar could not be loaded told the
user to run npm rebuild tree-sitter-<language>. R's grammar is published
as @eagleoutice/tree-sitter-r, so the hint named a package that does not
exist. The grammar loader now keeps a small map of grammars published
under another name, and every other language gets the same hint as
before.
R skips a binding that is not a function when it looks up the callee of
f(x), but the rename provider claimed every parameter, so a call under
function(x, helper = 2) was left unrenamed and failed after the rename.
A parameter whose default is a constant and which the body never assigns
is no longer claimed, so the call is renamed with the function it
reaches. A parameter that may hold a function (a function or computed
default, or an assignment to it in the body) is claimed as before, and
a call of the name under it is refused as ambiguous_reference, since it
may reach either. A parameter without a default that is called stays a
function-valued parameter and its call is left alone.
A <<- inside a function is a global re-definition that the scope graph
keeps as a second declaration of the same name. A rename dropped the
selected function and its calls but left the superassignment, so after
the rename the function wrote a new global of the old name instead of
replacing the renamed one. The top-level form was already refused by
the engine; checkSafety now refuses any <<- or ->> to the name, in or
out of a function, with the same ambiguous_reference code.
The guard looked only at the text before the first dot, so
to.json.foo (a method of the dotted generic to.json) passed, and base
generics such as print, format, summary and plot were never checked
unless the sources defined them. Every dot-separated prefix of the new
name is now tested against the functions the indexed sources define and
a small list of base generics.
A function written in backticks keeps them in its binding key and on its
node, while NAMESPACE entries and importFrom() carry the bare symbol, so
importFrom(pkg, `%||%`) found no defining file and the call degraded to
the name-based guess, which cannot tell two packages' `%||%` apart.
rFileTopLevel now keys the top-level names by symbol (rBareName) and
records the spellings it saw. resolveRNamedImport looks files up by
symbol, populateRNamespaceImports synthesises the import with the
definition's own spelling (one per spelling the provider uses) and lets
a backtick-defined function of the caller package mask its import. The
global-name-fallback veto and the pkg::name() lookups compare symbols.
The dotted-tail guard still compares the name as written, which is how
finalize indexes it. Names without backticks answer exactly as before,
pinned by a characterisation snapshot recorded before the change.
The method patterns required `A <- setRefClass(...)`, so a bare
`setRefClass("A", methods = list(...))` produced the Class and its fields
but no Method. The method extractor and the class owner lookup never needed
the assignment; match the call itself in both queries.
rMergeBindings sorted same-tier bindings with localeCompare, whose order
follows the machine's locale. Compare node ids by code unit, as the package
discovery does.
expandRWildcardNames scanned every parsed file with find() for each
library() edge (450,000 edges over 3,000 files took 5.9 s). Build the
module-scope lookup once per pass, as the C, C++ and Python providers do.
The grammar parses NA, NA_integer_, NA_real_, NA_character_ and NA_complex_
as `na` nodes, not identifiers, so the identifier branch never ran and no
type was inferred. Type them as integer, numeric, character and complex, and
plain NA as logical.
A UTF-8 byte-order mark before `Package:` hid the package, so its qualified
calls were dropped as external. And `\s*` crossed a newline, so an empty
`Package:` field captured the next line's `Version:` as the package name.
Strip the mark and keep the value on the field's own line.
[[:constructor:]], [[:toString:]] and [[:__proto__:]] resolved on the
prototype chain and spliced function source text into the pattern, so the
pattern compiled to garbage instead of being rejected as an unknown class
and reported as dropped. Look classes up as own properties.
An unterminated `{` after an `if` scanned to the end of the file, and the
resynchronisation then landed inside the region just scanned, so a file of
`if (a) {` lines with no `}` was quadratic (300 KB took 8.8 s, and the file is
parsed several times per analysis). Index where every block ends in one pass
and answer each block from the index. Parsing results are unchanged.
The pattern /^`+|`+$/g is quadratic on an entry such as x, a long run of
backticks, then y; the entry is repository-controlled text read on every
importFrom lookup of a name the package does not export. Trim with two scans.
A backticked name is an identifier whose text keeps its backticks, so the
rename provider never matched `helper`(x) with helper. The call was left
behind, and a local `helper` <- function(...) was not seen as a shadow, so
a call to the local function was silently renamed. Match occurrences,
claims and bindings on the symbol, and compare the symbol in the safety
checks.
export(`%+%`) names the symbol %+%, while the definition node keeps its
spelling `%+%`, so the export check never matched and the function was
flagged as not exported (the same for exportPattern()). Compare the symbol
through one shared helper, rBareName.
The slot pattern matched only list(...) and representation(...), although
the field extractor accepts any call, so setClass("A", slots = c(a =
"numeric")) and setRefClass("R", fields = c(...)) created no Property nodes.
Accept c(...) for both. An unnamed string in slots = c("a", "b") or
fields = c("a", "b") names an untyped member, as in R; an unnamed string in
representation(...) or list(...) is still not a member. The new pattern
captures its callee as @_vecOwner, so the class-naming capture filter does
not mistake these strings for the class name.
attachDeferredROwners resolved the owner hint through the class registry,
which every language shares. A Python class Circle next to an R Circle made
the name ambiguous, so the R methods and slots lost their HAS_METHOD and
HAS_PROPERTY edges; and with no R Circle, an R setMethod attached to the
Python class.
Keep only candidates whose graph node is an R class before the uniqueness
test. The change is inside R's postParse hook, so shared code is untouched.
extractRParameters tested for identifier and argument nodes, but the grammar
emits one parameter node per formal, so it always returned an empty list.
Every R6 and RefClass method therefore had parameterCount 0 and a #0 id, and
a variadic method was not marked variadic.
Read the parameter nodes: the name field gives the formal, a default makes it
optional, and a dots name makes it variadic (such a method has no count and
no arity tag in its id).
Member ids carried no owner, so a second class in one file that shared a
member name with an earlier one (push, items, a setMethod generic, a slot)
minted the same id as the first and lost the member, along with its
HAS_METHOD or HAS_PROPERTY edge.
Register resolveDetachedOwnerName for R. It names the class declared by an
ancestor R6Class assignment, setClass or setRefClass call, or the signature
class of a setMethod call, so ids become Method:<file>:Stack.push#n and
Property:<file>:Stack.items. The owner edge is still attached by postParse
through ownerNameHint. The change is R-only: the hook is optional and
undefined for every other language.
The r-packages characterisation counts now include Child.greet, the member
the id collision used to drop.
The @param name pattern [\w.]*\w[\w.]* backtracks quadratically on a long
name followed by anything but a blank (32,000 characters took about half a
second per line), and parseRQualifier's lazy [\s\S]*? does the same on a
long run of blanks. Match [\w.]+ and require a word character separately,
and take the text before the first '::' directly. Accepted inputs and
results are unchanged (checked against the old patterns on two million
generated strings).
Tests cover large adversarial inputs and the dotted formal names.
inSet() scanned a class's ranges from the front on every state visit, but the
search loop charged one unit per visit regardless of class size, so a wide
class multiplied real work the budget never counted. Ranges are sorted,
merged and disjoint for every CharSet (normalise() for written classes,
constants and complement() for escapes), so a binary search is equivalent.
Charged work is unchanged.
Tests pin the reads per visit, the edges of a large class and the charged
work counters.
* fix(routes): reject middleware and route metadata false positives
* fix(routes): invalidate cached phantom routes on upgrade
* test(routes): cover cache upgrades across merged schema versions
Verify that schemas 137 through 140 invalidate both parse stores after merging main into PR #3561 and advancing the schema to 141.
---------
Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
A reviewer's focused R run hit two worker-startup timeouts and had to rerun
with a 30-second readiness setting. The worker pool waits only 5 seconds by
default (GITNEXUS_WORKER_READY_TIMEOUT_MS), and every R test that parses
through the real worker pool started cold against that budget, unlike
csharp-razor-view-components and structural-pair-coverage, which already
raise it.
Apply the same idiom to the R tests that spawn the pool: stub the variable to
60000 in beforeAll and restore it with vi.unstubAllEnvs() in afterAll. Covers
r-worker-owner-absence, cfg/r-cfg-join, r-post-parse-gating, and the
resolvers/r*.test.ts files. rename-provider-r goes through
rename-integration-cases.ts, which already passes a 15-second budget to its
child process, so it is unchanged.
Tests only; no production code, schema or parse-cache change.
parseRoxygenParams read the parameter name with `\w+`, which stops at the first
dot, so `#' @param na.rm Data` documented nothing and the formal got no type
(the old unit test pinned that as "does not bind a dotted parameter name").
Reproduction: four new cases in type-env-r.test.ts (na.rm, .data,
max_n.rows, braced form with a continuation line) returned undefined instead of
'Data' on the unfixed pattern. The name pattern is now `[\w.]*\w[\w.]*`, so a
dotted identifier matches while `...` and `x,y` stay unbound; the former
"does not bind" case is replaced by positive cases plus a not-a-formal control.
expandRWildcardNames walked the flat localDefs of the target file and filtered
only dotted names, so a function nested inside another function
(`outer <- function() { helper <- function() 1 }`) was offered as a
package-level name to every file that calls library()/require() on the package.
Reproduction: two new cases in r-dotted-s3-hooks.test.ts failed on the
unfixed code (['outer', 'helper', 'top'] instead of ['outer', 'top'];
['a', 'b', 'c', 'd'] instead of ['a']). The expansion now keeps only names bound
at module scope (rFileTopLevel().names, which already feeds the dotted-tail
guard); top-level functions and bare-call S4/R6 classes are still listed
(controls added; test/integration/resolvers/r.test.ts unchanged and green).
The R rename guard compared the raw text of each `string_content` node with the
old and new name. An escaped spelling (`"f\u006fo"`, `'f\x6fo'`, octal) keeps
its escape inside string_content, and a raw string (`r"(foo)"`) has no
string_content node at all, so renaming `foo` proceeded while a get() or
do.call() on the escaped or raw spelling still resolved the old name.
Reproduction: 10 cases in the new test/unit/r-rename-string-guard.test.ts
returned 'ok' instead of unsupported_occurrence on the unfixed provider, and the
two new fixtures string-lookup-escaped and string-lookup-raw (rename-provider-r)
renamed successfully on the unfixed build. The guard now decodes each `string`
node (escapes, octal, raw strings) and compares the value.
docs/languages/r-provider.md said "spelled as a string literal"; it now states
the whole-value rule and what is still not detected (substring, paste0(),
non-indexed files).
skipConditionalBody returned just after the `if` of `else if (cond) body`, so
the main scan then read `(cond)` as stray text and emitted the body's
importFrom, S3method, export, exportClasses, exportMethods and exportPattern
directives as unconditional (the same for `if (a) if (b) body`).
Reproduction: 12 new cases in r-namespace-parser.test.ts failed on the
unfixed scanner (e.g. `if (a) {} else if (b) importFrom(x, y)` returned x::y;
`else if (b) export(hidden)` exported `hidden`). The nested `if (cond)` is now
stepped over iteratively (no recursion, tested with 50,000 nested ifs) and the
body that follows it is skipped.
LinearRegex.seed() filtered the closure's character states against the start
character before charging only 1 + (accepting states), so the range scans of
the failed state tests were free. A pattern of a few large character classes
could be probed with distinct start characters (each a filter miss, memoised
only up to 4096 keys) at almost no recorded cost.
Reproduction: for 20 alternated classes of 200 ranges, one miss on a
non-matching name recorded work 1 (expected >= 4000 range checks); a loop of
600 distinct start characters never exhausted a 1,000,000 budget. The filter
now charges 1 + ranges.length per examined state on a cache miss; hits stay
cheap. New tests in r-export-pattern-shared-budget.test.ts.
Review finding: `methods::setClass(...)` is ignored by the S4 field detector.
Confirmed by test, and wider than fields: every S4/RefClass pattern
(setClass, setRefClass, setGeneric, setMethod in R_QUERIES, plus the field and
method extractors) requires a bare identifier callee, so a namespace_operator
callee creates no class, slot, generic or method. R6::R6Class is supported.
Widening touches nine files (queries, naming-argument, captures, owner hooks,
post-parse, scope resolver, both extractors) and the qualifier-veto logic, so
it is not a trivial change. Documented as a known limit in
docs/languages/r-provider.md and pinned by characterisation tests in
r-definition-anchors.test.ts (each paired with its unqualified control).
Review finding: an empty then/else arm loses its cond-true/cond-false edge.
Not reproducible as lost information: an empty `{}` arm (like a missing
`else`) is the header's seq fall-through edge, the same convention as every
other CFG visitor, and control dependence labels it as the complement of the
sibling explicit arm. Probed on the current code: `if (c) {} else work()`
gives c-cond-false->work() with CDG c->work():F, and
`if (c) work() else {}` gives CDG c->work():T.
Adds exact edge-structure and CDG-label tests (all passed on the unmodified
visitor) and documents the fall-through convention in the visitor header.
No behaviour change.
The R6 public/private field queries listed only some expression kinds, so
`public = list(x = NA)` (also Inf, NaN, complex, if/for/while/repeat, a$b,
pkg::v, v[1], v[[1]], {..}, (..)) produced no Property although
RFieldExtractor accepts any value that is not a function_definition.
Reproduction: new test/unit/r-r6-field-values.test.ts failed 46 of 76 cases
on the unfixed query (expected ['x'], received []). The query now enumerates
every named grammar expression kind except function_definition, shared by the
namespaced and bare R6Class patterns.
Restore parse-cache.ts and incremental-parse-cache.test.ts to upstream/main,
so this branch no longer touches either file.
- R files were never parsed before this branch, so no parse-cache entry for
them can be stale: .r files had no language and were in no chunk.
- Chunk keys are a hash of the chunk's file paths and content hashes with no
language or grammar component, so chunks of existing languages keep exactly
the same keys and entries; adding R only adds new R chunks.
- The cache version is `${SCHEMA_BUMP}+${package version}`, so any release
changes the package part and invalidates every entry anyway.
- The bump only protected reviewers re-testing successive heads of this branch
on a single package version, and it kept conflicting with upstream's own
bumps on every merge.
The diff against upstream/main adds no capture or ParseWorkerResult change for
any other language: shared edits are optional provider hooks that only R
defines (postParse, resolveMemberOwnerNode, cfgVisitor, rename).
Plain R functions rename through the scope graph; S3, string-spelled,
S4, R6 and NAMESPACE-exported names are refused. Adds fixtures, the
rename-provider-r test and its cross-platform shard entries.
R now provides a cfgVisitor (if/else, for, while, repeat, break, next,
return(...), nested functions), so the CFG provider gate holds for R and
--pdg emits BasicBlock, CFG and CDG data. There is no def/use harvest, so
reaching definitions report no-facts for R. The v142 parse-cache comment
notes that cached R results lack a CFG side channel.
* fix(ingestion): re-point Go struct member edges when struct is declared in a sibling file (#3558)
findEnclosingClassInfo mints a Go method's owner id from the file
containing the method (Struct:<methodFile>:<Type>), while the Struct
node is keyed by the file declaring the struct (parse-worker uses
declFile). Same package, different file => the HAS_METHOD edge's
sourceId points at a node that does not exist and the edge is silently
dropped at COPY into LadybugDB.
Add reconcileGoStructOwnerEdges, run at the end of the parse phase
after ALL worker chunks are merged so every package's structs are
visible. It rewrites only edges that would otherwise dangle, and only
when exactly one Struct node shares the dangling id's package directory
and simple name; 0 or >=2 candidates keep the old drop behavior (a
wrong owner is worse than a dropped edge).
Index build is lazy: dangling Struct sources are rare (struct fields
never dangle — they live in the struct's own file), so the common
non-Go or fully-same-file case pays two per-type edge scans and no
whole-node pass.
Regression test: test/integration/go-cross-file-struct-owner.test.ts
(fails-before: 1 HAS_METHOD edge; passes-after: 3).
* fix(ingestion): scope Go struct-owner reconcile by language and package
Address PR #3559 review findings:
- Scope the pass to Go: dangling HAS_METHOD/HAS_PROPERTY owner ids whose
Struct:<file>:<name> file is not Go source (Rust, Swift, ...) are never
re-pointed, and only Struct nodes stamped language 'go' (the marker the
worker sets on every declared symbol) are candidates. Negative test pins
a Rust-shaped dangling edge staying untouched beside a same-name Go
struct.
- Key candidate structs by (dir, declared package clause, name) instead of
(dir, name): one directory can carry 'package pkg' and external-test
'package pkg_test' files declaring the same type name, which the old key
saw as ambiguous and dropped fail-closed. Clauses are read via the shared
inferGoPackageName derivation with a call-scoped cache. Integration test
covers the 3-file layout (pkg decl, pkg_test decl, pkg_test methods file)
and asserts the edge lands on the pkg_test.Widget node.
- Keep the direct call in parse-impl with a TODO: the pass needs a
graph-level post-merge hook, which no LanguageProvider/ScopeResolver hook
provides today.
* refactor(ingestion): remove Go graph repair from shared parse phase
* fix(ingestion): resolve Go ownership during ingestion (language-provider.ts)
* Add resolveDetachedOwnerName function to Go config
* Refactor method owner population logic in Go
* Refactor receiver binding to handle unnamed receivers
* Enhance route handler method resolution logic
Refactor route handler to improve ownership resolution and method indexing.
* Add emitPostResolutionEdges to scope resolver
* fix(ingestion): resolve Go ownership during ingestion (ownership.ts)
* fix(ingestion): resolve Go ownership during ingestion (ast-helpers.ts)
* Add resolveDetachedOwnerName to provider methods
* Update SCHEMA_BUMP to 141
Increment schema version to 141 for new features and fixes.
* fix(ingestion): resolve Go ownership during ingestion (decl.go)
* fix(ingestion): resolve Go ownership during ingestion (external_decl_test.go)
* fix(ingestion): resolve Go ownership during ingestion (external_methods_test.go)
* fix(ingestion): resolve Go ownership during ingestion (foreign.rs)
* fix(ingestion): resolve Go ownership during ingestion (go.mod)
* fix(ingestion): resolve Go ownership during ingestion (local.go)
* fix(ingestion): resolve Go ownership during ingestion (methods.go)
* fix(ingestion): resolve Go ownership during ingestion (go-cross-file-struct-owner.test.ts)
* Refactor comments and method registration in tests
Updated comments for clarity and modified method registration to include qualified names and ownership.
* fix(ingestion): resolve Go ownership during ingestion (has-method.test.ts)
* Update SCHEMA_BUMP to 141 in tests
* refactor(ingestion): remove obsolete Go struct owner reconciliation
* fix(go): model build constraints for method ownership
* test(go): cover build constraint grammar and platform variants
* fix(go): resolve method owners across compatible build variants
* test(go): verify variant ownership through ingestion pipeline
This test verifies that build-variant owners are consistent across different workers, the semantic registry, and the graph. It sets up various scenarios to ensure that ownership is correctly resolved and that there are no dangling edges in the ownership graph.
* fix(go): reject impossible known constraints when peer is unknown
Address PR #3559 review feedback. Regression coverage follows in this update.
* feat(ingestion): allow providers to group incremental resolution files
Add the optional language-neutral hook used by the Go ownership writeback fix for PR #3559.
* fix(go): group sibling files for incremental ownership resolution
Use the existing Go package-directory key for the provider hook.
* fix(incremental): collect sibling resolution groups for changed paths
Keep group keys local to each provider and include current siblings of deleted files.
* fix(analyze): persist ownership changes in unchanged Go siblings
Expand changed and deleted resolution groups before importer traversal. Use the same effective file set for deletion and writeback so package-wide ownership changes reach the database.
* test(incremental): cover provider-local resolution groups
Cover changed, added, deleted and unrelated paths for the new sibling expansion helper.
* test(go): compare incremental ownership writes with cold rebuilds
Reproduce ownership changes after deleting and restoring a Windows declaration while the shared method file stays unchanged. Verify incremental writes and persisted ownership parity with forced rebuilds.
* test(go): exclude impossible owners with unknown method constraints
Use real parsing to cover impossible owner edges and valid owners hidden by impossible competitors.
* test(go): pin filename rules and unknown constraint overlap
Document Go first-dot filename behavior and verify impossible known constraints in both operand orders.
* test(go): register incremental ownership coverage across platforms
Include the new database-backed Go ownership suite in the cross-platform test list.
* test(go): schedule incremental ownership suite with native database tests
Complete PR #3559 review fixes. Local validation: 47 focused tests across five suites passed; build and source/test type checks passed. The full suite was interrupted without a result.
* refactor(ingestion): centralize worker owner hook forwarding
Pass the language provider into the private cached owner lookup instead of repeating four owner hooks at five call sites. Keep qualified-name selection explicit and preserve the exported helper signature.
Validation: CLI/shared build and test typecheck pass; repository lint has no errors; 591 tests pass with one skipped across 13 ownership and worker suites. Web typecheck is blocked by uninstalled web dependencies. Graph checks are unavailable because this checkout has no registered index.
* test(rename): add Go cross-file fixture module
Part of the cross-file method ownership regression for PR #3559. The completed fixture uses real indexing, public rename preview/apply, warm cache replay and post-rename reindexing. All 14 Go rename integration cases pass locally.
* test(rename): add cross-file fixture before/types.go
* test(rename): add cross-file fixture before/methods.go
* test(rename): add cross-file fixture before/callers.go
* test(rename): add cross-file fixture after/go.mod
* test(rename): add cross-file fixture after/types.go
* test(rename): add cross-file fixture after/methods.go
* test(rename): add cross-file fixture after/callers.go
* Add test case for renaming pointer receiver method
Complete the real persisted-index regression fixture for PR #3559. The type, pointer receiver method and callers are in separate Go files. Public rename preview/apply must make exactly four edits across the structural interface family while preserving an unrelated same-name method, comments and strings.
The fixture checks six exact ownership, interface and call edges before rename, after warm parse-cache replay and after reindexing the edited sources. All 14 Go rename integration cases pass. Formatting and the CLI typecheck pass. Graph impact/detect-changes could not run because this checkout has no registered index.
---------
Co-authored-by: System administrator <root@nixos-infra.home>
Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
* fix(lbug): round GITNEXUS_LBUG_MAX_DB_SIZE up to a power of two
The native buffer manager rejects any max DB size that is not a power of
two, but the env parser accepted any positive integer, so a value such as
300000000000 failed at open and was then reported with a misleading
"set a larger limit" hint.
Round a positive safe integer up to the next power of two and warn once
naming the raw and effective values. Values that are not safe integers
now fall back to the 16 GiB default.
Refs #3529
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* feat(lbug): add remedies for the address-space ceiling and past-EOF read
Add two pure classifiers beside bufferPoolExhaustionRemedy. They parse only
what the engine message states, label the byte count as the limit the engine
reported (not the configured value), and name GITNEXUS_ATOMIC_INCREMENTAL=0
only for runs that knob changes. Neither asserts why the failure occurred.
Refs #3529, #3530
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* feat(lbug): add size-evidence formatting for failure diagnostics
formatFileEvidence is a pure formatter over stat-shaped input (apparent
size, allocated blocks, raw statfs f_type), so sparse and regular cases are
testable without a real sparse file. describeFileEvidence wraps it, never
throws, and reports a missing file as unavailable.
Refs #3530
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* fix(analyze): honor GITNEXUS_ATOMIC_INCREMENTAL=0 and classify staged failures
GITNEXUS_ATOMIC_INCREMENTAL was only read as === '1', so the documented =0
mitigation did nothing for watch and auto-sync, which pass atomicIncremental
as an option. An explicit =0 now beats the caller's request. Full rebuilds
and the escalation to staging still stage and are unaffected.
In the analyze catch block, snapshot the live and staging files before the
checkpoint driver stops and the database closes (closing can checkpoint the
staging file), classify the error once, then log the remedy and the held
evidence for a size-ceiling or past-EOF failure. The off-switch is named
only when the failed run was an atomic-incremental staged write. A max-size
message the classifiers do not recognise keeps the generic hint, and the
original error is rethrown unchanged. The atomic-incremental staging log
line now records the staged file size.
Refs #3529, #3530
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* docs: document max DB size rounding and GITNEXUS_ATOMIC_INCREMENTAL
Note that GITNEXUS_LBUG_MAX_DB_SIZE must be a power of two and is rounded
up, add GITNEXUS_ATOMIC_INCREMENTAL (1 and 0, and which builds 0 does not
affect) to both README env tables, and add a Troubleshooting entry for the
two intermittent embedding write-back failures and the evidence lines GitNexus
now logs for them.
Refs #3529, #3530
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* fix(review): apply review findings
Review fixes:
- Label the GITNEXUS_LBUG_BUFFER_POOL_SIZE=0 sentinel in the failure
evidence line instead of printing "buffer pool 0 B".
- Replace FailureRemedyContext {staged, offSwitchApplies} with a single
StagedBuildMode ('in-place' | 'staged-rebuild' | 'staged-incremental'),
removing the { staged: false, offSwitchApplies: true } illegal state.
- Re-attach the LBUG_MAX_DB_SIZE doc comment to the exported constant.
- Cover the in-place classified failure through runFullAnalysis, and the
buffer-pool sentinel label, with new tests.
Also carries the simplification pass: failure evidence is collected only for
a classified failure, the staged-build flag is reused, the env var is read
once, and the evidence tests share one temp-dir helper.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* fix(lbug): bound failure-evidence stats and record checkpoint activity
Review findings #1 and #3 on #3570.
describeFileEvidence raced nothing: a stat on a stalled 9p/NFS/virtiofs
mount hung the analyze failure handler before the driver stopped, the
database closed and the original error was rethrown. Each stat/statfs now
has a 2000 ms deadline and fails open; a missing file reads "absent"
instead of "unavailable", a stalled one reads "unavailable (timed out ...)".
collectFailureEvidence (new) is the single evidence collector: it reads
the checkpoint activity snapshot and dispatches every stat in one
Promise.all before its first await, covering the live index, the staging
copy and its .wal/.shadow/.wal.checkpoint sidecars. That is what lets a
#3530 report tell a short main file from pages pending in the sidecars or
from a CHECKPOINT GitNexus itself ran just before the failing DELETE.
runCheckpointWithRetry now records completed count, last completion time
and in-flight count. Both the periodic driver tick and checkpointOnce (the
staged window-start CHECKPOINT) go through it.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* fix(lbug): state the off-switch cost, pin the rounded max size, isolate ambient env
Review findings #5 (note), #6 (wiring) and #11 on #3570.
offSwitchNote now says that GITNEXUS_ATOMIC_INCREMENTAL=0 can leave the
live index partly written after a failed in-place write, and that the next
run then rebuilds it in full.
A new test stubs GITNEXUS_LBUG_MAX_DB_SIZE=300000000000, reloads
lbug-config and asserts the native Database constructor receives 2 ** 39
and that exactly one rounding warning is logged. The existing tests all
called the parsers directly and matched the constructor argument with
expect.any(Number), so reverting the wiring left the suite green.
vitest.config.ts sets GITNEXUS_ATOMIC_INCREMENTAL to the empty string
(neither '0' nor '1', i.e. unset) like GITNEXUS_MEMORY, so an exported =0
no longer switches off every staging test.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* fix(analyze): carry the failure diagnosis in the error message and label escalated runs
Review findings #2, #4, #5, #7, #8 and the run-analyze half of #6 on #3570.
#4: the remedy and `Failure evidence` lines were only sent to onLog, which
`analyze --watch` wires only with GITNEXUS_VERBOSE=1 and the auto-sync
launcher drops - the only two callers that request atomic-incremental
staging. They are now appended to the rethrown error's message in place
(identity and prototype preserved, native text first so classifiers
anchored on it still match), which every caller prints. A thrown value
that is not an Error falls back to a log. Auto-sync truncated that message
to 240 characters before logging it; the log now allows 2000 while the
persisted summary stays short.
#8: remedy delivery is best effort, so a throwing onLog sink or an
unwritable message can no longer replace the native error.
#2: escalatedFullWrite is declared above the try, and an escalated
atomic-incremental run is classified as a rebuild. Its remedy no longer
points at GITNEXUS_ATOMIC_INCREMENTAL=0, which does not stop the
escalation from staging.
#5: when GITNEXUS_ATOMIC_INCREMENTAL=0 overrides a caller that requested
staging, the run logs one line saying so and what a failed write costs.
#7: the stage-time size line uses the same "staging copy" label as the
failure evidence.
#6: tests pin the evidence-before-driver-stop-before-close order, the exact
configured max DB size in the evidence, and the appended text against the
substring classifiers in the CLI and auto-sync.
The READMEs describe the new evidence lines, ask reporters to include the
stage-time line, and name GITNEXUS_WAL_MANUAL_CHECKPOINT=0 as the bisect
knob for the checkpoint hypothesis.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* Address PR review feedback (#3570)
- wal-checkpoint-driver: count a checkpoint as completed only when
tryFlushWAL actually ran one. It returns false when no connection is
open, and the failure evidence renders the count as "N completed".
- auto-sync runner: an oversized native error now logs its head and tail
(1000 characters each) so the appended failure diagnosis, which is always
the tail, survives. The persisted 240-character summary is unchanged.
- lbug-config: the max-size remedy said GITNEXUS_LBUG_MAX_DB_SIZE "must be
a power of 2", but non-powers are rounded up. It now says so, as do the
matching lines in gitnexus/README.md.
- failure-evidence and file-evidence tests use the default registered
temp-dir prefix, so the stale-sidecar sweep recognises their fixtures.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* Address PR review feedback (#3570)
- run-analyze: stringify the thrown value through a helper that cannot
throw, so an exotic throw (null-prototype object, throwing toString)
cannot abort the failure handler before it stops the driver, closes the
database and reclaims the staging copy.
- run-analyze: a classified non-Error throw is wrapped in an Error (original
kept as `cause`) so the diagnosis travels in the message like every other
case, instead of in a log that the auto-sync launcher drops. This removes
the separate log fallback.
- auto-sync runner: the oversized-error log keeps head and tail within the
2000-character cap; the separator no longer pushes it to 2003.
- lbug-config: the max-size rounding warning prints the operator's value as
supplied (300000000000.9), not the integer it floored to.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* Address PR review feedback (#3570)
- lbug-config: the max-size remedy's rounding example was itself a power of
two. It now shows a value being rounded: 300000000000 becomes 549755813888.
- auto-sync-runner test: the comment said the whole log line is capped at
2000 characters; the assertion covers the error payload after the prefix.
- extension-loader test: pin that the installer child is handed the rounded
max DB size for a non-power-of-two override. The installer prefers that
argument over the env var and has its own non-rounding parser, so this is
what keeps a valid override working there.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* Address PR review feedback (#3570)
- Keep the whole failure diagnosis in auto-sync's capped log line: the
tail now reserves the remedy and evidence lines (with a 240-char floor
for the native head) instead of an even head/tail split.
- Count only this run's checkpoints in the failure evidence: snapshot the
process-global activity when the run begins and report the difference,
so an earlier `analyze --watch` refresh no longer leaks in.
- Diagnose classified failures thrown while opening the staging copy, via
helpers shared with the write-path catch.
- Halt checkpoint ticks before measuring failure evidence: start
`walCheckpointDriver.stop()` (which clears its timer synchronously)
before the stats, then await the drain before close.
Note: pre-existing timeouts in run-analyze-fts-repair ("fails repair mode
when no base meta exists", "abandons a pending set that fails
EMBEDDING_RESUME_MAX_ATTEMPTS times in a row") reproduce on the unchanged
PR head on this host and are not addressed by this commit.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com>
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
* chore(deps): consolidate Dependabot GitHub Actions updates
* chore(deps): consolidate web updates and secure transitive dependencies
* fix(web): preserve diagram appearance with Mermaid 12
* test(ci): align workflow security tests with updated action pins
* fix(ci): split coverage across eight shards to avoid job timeout
CI run 38693066214 cancelled Ubuntu coverage shard 1 after 25 minutes while tests were still passing. The missing coverage blob then caused the execution audit and CI gate to fail.
Increase only the coverage shard count from four to eight. Keep the timeout, full suite, coverage thresholds, and execution audit intact.
Validation: all 1,247 test files form a complete, disjoint eight-shard partition using the configured Vitest sequencer; 117 targeted tests passed (2 existing skips); workflow generator, Prettier, concurrency validation, and diff checks passed.
* fix(ci): leave headroom for Windows test shards
Run 38697469283 exhausted the 20-minute Windows watchdog. Use ten platform shards while retaining eight coverage shards and all existing timeouts, coverage gates, and execution audits. The accompanying timing refresh and recorded-profile regression keep all 145 platform-sensitive test files covered exactly once.
* fix(ci): refresh underestimated Windows suite timings
Refresh eight materially underestimated timing weights using Windows runs 38697469283 and 38693066214. The old partition placed native suites ahead of cli-e2e with insufficient time left under the watchdog. Keep every suite and the partitioning algorithm unchanged.
* test(ci): replay the Windows timeout workload
Add a deterministic regression from recorded Windows CI timings, independent of the scheduling table. It failed before the fix with a 1,426-second shard and now protects three minutes of watchdog headroom.
Validation: 118 targeted tests passed (2 existing skips); typecheck, formatting, workflow generation, concurrency and diff checks passed. Independent review found no actionable issues. All 145 platform test files are selected exactly once; replaying the complete observed profile gives a 973-second maximum across ten shards. Hosted CI remains the final elapsed-time check.