Commit graph

143 commits

Author SHA1 Message Date
mengkaka
4154b63131
feat(indexing): add Objective-C semantic indexing support (#3179)
* docs: add Objective-C fork provider notes

* feat(objective-c): add deterministic provider and grammar

* feat(objective-c): finalize provider MVP

* fix(objective-c): harden provider integration

* fix(objective-c): normalize bare macro markers

* docs(objective-c): integrate provider documentation

* fix(objective-c): harden resolution and header classification

* fix(objective-c): complete provider follow-ups

* fix: address Objective-C review follow-ups

* chore: format Objective-C grammar sources

* fix(objective-c): harden review follow-ups

* Address PR review feedback (#3179)

Keep Objective-C chunking and macro recovery aligned with the grammar, and stop Community MEMBER_OF edges from leaking into symbol context.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Address follow-up review on ObjC chunking and language fallback.

Keep preprocessor directive text from changing file-scope brace depth, group real ivar nodes, skip header modifiers, and restore Rakefile/Gemfile detection through getLanguageFromFilename.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Parse Objective-C headers with the objc grammar in embeddings.

ensureAndParse and structural extraction now use the same content classifier as ingest, including method snippets from .h files, so Protocol/Category/Class chunks are not re-parsed as C++.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Address PR review feedback (#3179)

Keep file-scope macro elision off C line splices and @interface/@protocol/@implementation bodies, and attach ivar attributes to the following instance variable when chunking.

Co-authored-by: Cursor <cursoragent@cursor.com>

* chore(bench): rebaseline Objective-C CSV emit

* feat(objective-c): add workspace resolution and linear emit benches

Plain .h files are classified as C++, so the ObjC pass could not
resolve #import of those headers. Load a C/C#-style workspace once
per pass, and keep protocol-candidate USES linear.

Refs #3179

Co-authored-by: Cursor <cursoragent@cursor.com>

* Address PR review feedback (#3179)

- Compare LadybugDB labels() as a scalar when excluding Community MEMBER_OF edges.
- Walk superclass members, skip file-static C sibling defs, and ignore comments in ObjC header/macro scans.

Note: pre-existing failure in objective-c-provider integration (worker-pool ready timeout) not addressed by this PR.
Co-authored-by: Cursor <cursoragent@cursor.com>

* Address PR review feedback (#3179)

Emit Objective-C declaration captures so compilation-unit siblings can share
header/implementation bindings, and keep class vs protocol visibility groups
distinct.

Note: pre-existing failure in worker-pool startup (GITNEXUS_WORKER_READY_TIMEOUT_MS) not addressed by this PR.
Co-authored-by: Cursor <cursoragent@cursor.com>

* Address PR review feedback (#3179)

Emit every comma-separated property/ivar declarator, and count @interface
after a multiline block comment closes so in-declaration macros stay intact.

Note: pre-existing failure in worker-pool startup (GITNEXUS_WORKER_READY_TIMEOUT_MS) not addressed by this PR.
Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: ximengkai <ximengkai@soyoung.com>
Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-09 09:40:21 +00:00
cosark
a4e70ec3b4
docs: add RepoCloud one-click deploy button (#3212)
Co-authored-by: cosark <cosark@users.noreply.github.com>
Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
2026-09-08 09:23:52 +01:00
Subham Kundu
95858e7549
Update README.md (#3217)
Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
2026-09-08 07:25:22 +01:00
Parafee41
47c2799ba7
fix(analyze): add cold parser rebuild option (#3153) 2026-09-04 05:14:00 +01:00
Navid EMAD
932d937085
feat: add Zig language support (#1432)
* feat: add Zig language support

Adds a Zig LanguageProvider grounded in @tree-sitter-grammars/tree-sitter-zig 1.1.2.
The grammar's published peerOptional `tree-sitter@^0.22.1` is suppressed via an
npm `overrides` entry that aliases the peer to the bundled `tree-sitter@0.21.x`;
load-time smoke testing confirmed ABI compatibility.

v1 capabilities:
  - .zig file detection + Prism syntax mapping
  - Top-level + nested function_declaration as Function/Method
  - struct/enum/union (anonymous in the grammar — owner name resolved from the
    enclosing variable_declaration in class/field/method extractors)
  - container_field as struct/union fields and enum variants
  - top-level const/var as Variable nodes
  - free + member call_expression as @call edges
  - @import("./foo.zig") local-file resolution; std and external packages
    return empty (no ghost edges)
  - pub keyword detection for export checking
  - no heritage hooks (Zig has no inheritance; queries never emit @heritage.*)

Generic extractor changes (backward-compatible):
  - field-extractors/generic.ts and method-extractors/generic.ts: empty
    `bodyNodeTypes` falls back to the type declaration node itself as its own
    body container — needed because Zig's struct_declaration directly contains
    its container_field children. (The `extractOwnerName` hook this commit
    originally introduced now exists upstream; Zig just configures it.)

Out of scope (deferred):
  - usingnamespace, build.zig.zon package graph, comptime/anytype
  - scope-resolution hooks (emitScopeCaptures, interpretImport, …): Zig is
    classified `experimental` and uses the generic fallback resolution path
  - cross-package imports (std, deps)

Tests:
  - new fixture test/fixtures/sample-code/simple.zig
  - Zig describe block in tree-sitter-languages integration test
  - simple.zig added to parsing.test.ts fixture-existence list
  - Zig added to ingestion-utils detection unit test
  - Zig smoke case in parser-loader-abi.test.ts
  - Zig grammar registered in the grammar-literal validation gate

* feat(zig): integrate build.zig.zon resolution + Union label from PR 1096

Ports the additive pieces of grgisme's standalone Zig provider PR
(https://github.com/abhigyanpatwari/GitNexus/pull/1096) onto the rebased
provider:

  - build.zig.zon `.path` dependency resolution for bare-name
    @import("pkg") (parseZigBuildZon / loadZigBuildZon in
    language-config.ts, resolveZigImportInternal in import-resolvers/zig.ts,
    wired through ImportConfigs.zigBuildZon). `.url` deps and
    repo-escaping paths return null cleanly. 13 unit tests.
  - `union(enum)` containers now produce `Union` nodes (not Struct):
    'Union' added to ClassLikeNodeLabel + CLASS_LIKE_LABELS, the Zig query
    tags @definition.union, CONTAINER_TYPE_TO_LABEL maps union_declaration
    to 'Union'. The label was already plumbed graph-wide on main.
  - /^build$/ entry-point pattern (build.zig).
  - zig-basic lang-resolution fixture + resolvers integration test.

Adapted to current main while porting:

  - labelOverride relabels container-nested fns Function → Method
    (mirrors isKotlinClassMethod); the structure phase no longer derives
    Method from the legacy method-extraction path for plain
    @definition.function captures.
  - IMPORTS/CALLS edges require scope-resolution hooks
    (emitScopeCaptures / interpretImport) since the legacy DAG removal;
    Zig does not implement them yet, so the integration test documents
    that with a skipped import-edge case. The resolver itself is wired
    into the resolver factory and becomes live when the hooks land.

Not ported: named-bindings extractor (the legacy namedBindingExtractor
API no longer exists) and the bespoke field extractor (the generic
factory's extractOwnerName / empty-bodyNodeTypes hooks cover Zig).

Co-authored-by: Garrett Griffin-Morales <grgisme@gmail.com>

* feat(zig): scope-resolution hooks — IMPORTS and CALLS edges (Ring 3)

Implements the registry-primary scope-resolution path for Zig, the
prerequisite for cross-file edges since the legacy DAG removal. Adds the
standard per-language stack under languages/zig/:

  - query.ts: scope query (containers as Class scopes, blocks, functions),
    declarations (container anchors placed on the container node itself so
    the def lands in its own Class scope and the name binding auto-hoists
    to the parent — populateClassOwnedMembers needs the class-like def
    among the class scope's ownedDefs), @import statements (#eq?-gated
    builtin), parameter/constructor type bindings, and call/constructor
    reference sites. The grammar is required lazily (optionalDependency).
  - captures.ts: emitZigScopeCaptures — groups query matches, drops the
    plain-variable group for container/import bindings (their dedicated
    rules bind the name), and relabels container-nested fns
    @declaration.function → @declaration.method (labelOverride parity).
  - interpret.ts: namespace-kind imports (const x = @import("…")) and
    type bindings — self-parameter convention marks the receiver, Zig
    sigils (*, ?, [], error unions, const) stripped from type names while
    dotted qualifiers (mod.T) are preserved for Case-3 namespace-prefix
    receiver dispatch.
  - simple-hooks.ts: parameter bindings stay function-local (Go
    rationale), local-over-import merge precedence, bounds-check arity
    (always 'unknown' today — no synthesized arity metadata).
  - scope-resolver.ts: emit-side wiring; build.zig.zon threads through
    loadResolutionConfig into the same resolveZigImportInternal the legacy
    resolver config wraps. fieldFallbackOnMethodLookup off (statically
    typed). Registered in SCOPE_RESOLVERS.

The resolvers integration test un-skips the import-edge case and gains
CALLS assertions: free call (main → helper) and receiver-bound method
dispatch through a namespace-qualified constructor
(var p = pioneer.Pioneer{…}; p.tick() → main → tick).

* fix(zig): Union is class-like + missing-grammar warning (review pass)

Self-review findings on the Zig branch:

  - scope/walkers.ts `isClassLike` and finalize-algorithm's
    CALLABLE_OR_TYPE_LIKE did not include 'Union': a `union(enum)`
    container's methods got no ownerId from populateClassOwnedMembers, so
    method dispatch on union receivers silently dropped. Widened both
    sets; the zig-basic fixture gains a Tag method + a CALLS assertion
    (main → isEnergy) that fails without the widening (verified by
    reverting).
  - optional-grammars.ts now lists tree-sitter-zig with an npm `probe`
    (it is an optionalDependency, not vendored): users with .zig files
    and no prebuild get the standard one-line stderr warning instead of
    a silently degraded index.
  - Deduplicated the container-method predicate: `isZigContainerMethod`
    + ZIG_CONTAINER_TYPES now live once in languages/zig/captures.ts and
    feed both the provider labelOverride and the scope-capture relabel.
  - README language matrices: Zig row now claims Type Annotations,
    Constructor Inference, and Config (build.zig.zon) — all true since
    the scope-resolution hooks landed.

* fix(zig): anchor @declaration.variable to the binding identifier

`(variable_declaration (identifier) @declaration.name)` matched EVERY
identifier child of the node, so `const first = target;` also declared a
phantom local named `target` in the enclosing block. That phantom shadowed
the real function for later references (and starved callable-value-flow
seeds of a target). The `.` anchor pins the pattern to the first named
child — the bound name.

Regression test in resolvers/zig.test.ts pins the capture set.

* feat(zig): callable-value-flow captures + main's per-language conformance gates

Post-rebase catch-up: since this branch forked, main added three "every
registered language must appear here" tests. Each needs a Zig entry:

- callable-value-flow (#2522): Zig now emits `@callable-flow.*` facts via
  `synthesizeCallableFlowCaptures` (ZIG_CALLABLE_CAPTURE_OPTIONS in
  zig/captures.ts). tree-sitter-zig's `call_expression` carries arguments
  as direct children with no wrapper node, which the shared helper could
  not decompose, so this adds a language-neutral `extractCallArguments`
  hook (mirror of `extractFunctionParameters`; `undefined` = shared path).
  Zig joins the provider matrix as 'matrix' with a real assign→copy→
  argument→invoke case.
- external-import-conformance (#2953): `@import("std")` beside a decoy
  `src/std.zig` resolves to nothing; the decoy stays reachable via the
  relative spelling. Zig holds the property (no suffix fallback), so it is
  a case, not a KNOWN_GAPS entry.
- import-target-index-reuse contract (#2909): membership-probe-only
  fixture (minimumScans: 0, same shape as Rust).

* fix(zig): address gitnexus-check review findings

One commit per the bot's list so each item is easy to check off:

- parser-loader: Zig row gains `userSkippable: true`, so
  `GITNEXUS_SKIP_OPTIONAL_GRAMMARS` (=1 or a list naming `zig`) disables it
  at analyze time like swift/dart/kotlin — as `optional-grammars.ts` already
  documented. Covered in parser-loader-skip-optional.test.ts.
- export-detection: `zigExportChecker` stops at the first declaration it
  reaches. A non-`pub` fn inside `pub const T = struct {…}` was reported
  exported because the walk continued up to the wrapper.
- import-resolvers/zig: a `.path = "."` dep normalizes to '' and no longer
  grows a leading slash (`/src/main.zig` could never match).
- language-config: build.zig.zon parsing strips `//` comments string-aware
  (a `//` inside `.url = "https://…"` survives) and matches braces while
  skipping string literals, so a commented-out `.path` cannot declare a dep
  and a `}` in a comment/string cannot truncate the block.
- method-extractors/configs/zig: the leading `self` receiver is excluded
  from `parameters` (Rust parity). Fixing that exposed a worse bug: the
  `parameters` node is a plain child of `function_declaration`, not a
  `parameters:` field, so `childForFieldName('parameters')` was always null
  and every Zig method had no parameters, no receiver and `isStatic: true`.
  One `zigParameterList` helper now feeds all three readers.
- variable-extractors/configs/zig: container (`struct`/`enum`/`union`) and
  `@import` bindings are skipped via the same predicate the scope captures
  use (`isZigContainerOrImportBinding`), instead of the comment merely
  claiming they were.
- tree-sitter-languages.test.ts: the "missing grammar" case now forces the
  absent-binding path through the loader's runtime opt-out on a fresh module
  instead of passing vacuously when the package is installed.

New: test/unit/zig-extractors.test.ts (exports, receiver/parameters,
variable guard); zig-import-resolver.test.ts gains the `.` dep, comment and
brace cases.

The `createFieldExtractor` heads-up needs no change: the added branch is
unreachable for every existing config (none has empty `bodyNodeTypes`).

* fix(zig): address second gitnexus-check review pass

- import-resolvers/zig: `..` above the repository root now returns null
  instead of aliasing a same-named root file (`../bar.zig` from `main.zig`
  is not `bar.zig`); the stale "extension is stripped and re-added" comment
  is corrected to what the code does.
- variable-extractors/configs/zig `extractType`: read the `type:` field only.
  The positional fallback returned the INITIALIZER of `const f = target;`
  as its type and gave up on compound annotations (`*Foo`, `?[]const u8`).
  The comment claiming 1.1.2 has no `type` field on variable_declaration
  was wrong (verified by AST dump) — and it is what led the review to
  suspect the callable-flow `extractAssignment` callback, which was
  already correct for `extern var f: T;`.
- receiver detection: only a FIRST parameter named `self` is the receiver.
  `emitZigScopeCaptures` tags first-position parameters
  (`@type-binding.first-parameter`), `interpretZigTypeBinding` requires the
  tag as well as the name, so `zigReceiverBinding` no longer turns
  `fn f(a: u32, self: T)` into an instance method.
- resolvers/zig.test.ts: both suites `describe.skipIf(!zigAvailable)`
  (Swift/Dart pattern) — the grammar is an optionalDependency.
- tree-sitter-languages.test.ts: the Zig parsing case gates on
  `isLanguageAvailable` instead of a catch-all `return`, so an installed
  grammar that fails to load fails the test; comment no longer calls Dart
  and Swift npm optionalDependencies (they are vendored).
- test/helpers/literal-collectors: `DIR_LANG` gains `zig`, so literals under
  `languages/zig/**` are validated against the Zig grammar alone rather than
  against every grammar.
- walkers.ts `isShapeLike` doc: Union IS included (via isClassLike, wired by
  Zig's union member container); Typedef remains the only deferred one.
- language-config `ZigBuildZonConfig.pathDeps` doc: values are the raw
  `.path` strings; the resolver normalizes.

Tests: zig-import-resolver (+1), zig-extractors (+3).

* fix(zig): address third gitnexus-check review pass

- language-config `parseZigBuildZon`: the `.dependencies = .{` header and
  the per-entry `.<name> = .{` headers are matched only OUTSIDE string
  literals (per-offset string mask + `matchZonHeader`). A `.name` or
  `.description` value spelling `.dependencies = .{ .fake = .{ .path = … } }`
  used to be taken as the block and returned the fake dep instead of the
  real top-level one.
- import-resolvers/zig: an absolute import (`@import("/foo.zig")`) returns
  null. The path walker skipped every empty component, so the leading `/`
  vanished and `/foo.zig` resolved as importer-relative `src/foo.zig` — an
  in-repo edge for an import Zig rejects as outside the module path.
- tree-sitter-languages.test.ts: the Zig parsing case gates on the PACKAGE
  being installed (`createRequire().resolve`, minus a deliberate
  `GITNEXUS_SKIP_OPTIONAL_GRAMMARS` opt-out), not on `isLanguageAvailable`,
  which is false for absent AND for installed-but-broken bindings — so a
  load failure (ABI mismatch, bad export) now fails the test instead of
  skipping it, as the comment already claimed.
- walkers.ts `isShapeLike` doc: `Union` sits in `isClassLike` because that
  is the label set the ownership walkers consult, not because unions
  inherit — Zig has no inheritance and no heritage hooks. The previous
  wording ("inheritance-capable owner") said otherwise.
- Not re-fixed (already addressed in the second pass, findings carried
  over): "receiver = any parameter named self" — `interpretZigTypeBinding`
  only sources a first-position parameter as `self`; `zigReceiverBinding`'s
  doc now states that invariant. "`DIR_LANG` has no zig entry" — it does.
  Extended one level out: `BASENAME_LANGS` (`zig.ts`) and `PREFIX_LANGS`
  (`ZIG_`) map to the Zig grammar too, so extractor configs and the
  export-detection set are validated against Zig alone. That immediately
  caught a dead `childForFieldName('parameters')` in
  method-extractors/configs/zig `zigParameterList` (there is no such field;
  the named-child lookup was already the one doing the work) — removed.

Tests: zig-import-resolver (+2: absolute path, header inside a string);
both fail on the previous code.

* fix(zig): address fourth gitnexus-check review pass

- language-config: `parseZigBuildZon` only accepts a `.path` that is a
  DIRECT field of a dependency entry. Nested blocks inside the entry body
  are blanked (string-aware, offsets preserved) before the `.path` regex
  runs, and a match starting inside a string literal is rejected, so
  `.dep = .{ .url = "…", .meta = .{ .path = "x" } }` no longer becomes a
  path dep. Regression test in zig-import-resolver.test.ts (fails on the
  previous code).
- tree-sitter-languages test: the "grammar is absent" case now drives the
  loader's real `source.load()` catch branch — `node:module` is stood in
  with a `createRequire` whose require throws MODULE_NOT_FOUND for
  `@tree-sitter-grammars/tree-sitter-zig` and delegates everything else —
  instead of the `GITNEXUS_SKIP_OPTIONAL_GRAMMARS` opt-out, which has its
  own test. It also asserts the opt-out flag is NOT set and that other
  grammars still load (non-fatal optional failure).

Not re-fixed:
- "Return type is read from the wrong tree-sitter field": tree-sitter-zig
  1.1.2 has NO `return_type` field on function_declaration — the type after
  `)` is the `type` field (AST dump: `builtin_type "i32" field=type`; the
  proposed `childForFieldName('return_type')` is null for every function).
  A pin test in zig-extractors.test.ts asserts both the grammar fact and
  that `returnType` is extracted (`void`, `!*Counter`).
- "`DIR_LANG` has no zig entry": it does (added in the first pass and
  answered again in the third); the finding is carried over unchanged.

* fix(zig): address fifth gitnexus-check review pass

- tree-sitter-languages test: the Zig "functions, structs, enums, and
  imports" case now asserts the `import.source` capture for
  `const std = @import("std");` (the fixture's only import), so a query
  change that drops Zig import matching fails it instead of passing
  unchanged.

Not re-fixed:
- "Return type is read from the wrong tree-sitter field": carried over from
  the fourth pass unchanged. tree-sitter-zig 1.1.2 has no `return_type`
  field on function_declaration; the return type IS the `type` field, and
  the pin test added in the fourth-pass commit
  (zig-extractors.test.ts, `childForFieldName('return_type')` is null,
  `returnType` = `void` / `!*Counter`) proves it.
- "`DIR_LANG` has no zig entry": carried over unchanged for the third time;
  the entry exists since the second-pass commit.

* feat(zig): export fn visibility, opaque containers, named test blocks, member ownership

Ports the parts of upstream PR #305 (closed, unmerged) that our Zig
provider lacked, plus two gaps found while porting.

- `export fn` / `export var` (C-ABI linkage, never `pub`) are exported;
  the pub/export predicate is now shared by the export checker and the
  method/variable extractors' visibility (`hasZigVisibilityKeyword`).
- `const H = opaque { … }` is a Struct-labelled container (it may own
  methods, never fields) in both the structure queries and the scope
  query; ZIG_CONTAINER_TYPES is the single source for the extractor
  configs.
- `test "name" { … }` blocks are Function nodes named by the string
  node WITH quotes, so `test "add"` beside `fn add` cannot merge onto
  Function:<file>:add; `test_declaration` joins FUNCTION_NODE_TYPES and
  the Zig method config names it in the enclosing-function walk, so
  calls inside a test attribute to the test. Anonymous `test {}` and
  decl-tests `test add {}` are scopes without a node (an empty-name hook
  result stops the walk instead of falling through to the identifier of
  the function under test).
- Empty container bodies (`struct {}`, `opaque {}`) no longer mint a
  nameless Property: tree-sitter-zig 1.1.2 recovers them as a
  container_field with a MISSING identifier; #not-eq? guards in both
  queries and the field extractor drop it.
- Owner walk (`findEnclosingClassInfo`): an anonymous container bound
  by the enclosing `variable_declaration` takes the binding identifier,
  same shape as the Go `type_spec` branch. Before this NO Zig member
  had an owner — zero HAS_METHOD / HAS_PROPERTY edges for Zig.

Not ported from #305, deliberately: `builtInNames` (a bare-name call-site
drop filter; `alloc`/`free`/`append`/`print` are the most common user
method names in Zig and `std.*` receivers are already external via the
import binding), `usingnamespace` (removed in Zig 0.15), `@cImport`,
`build.zig` ignore, and the web-app changes.

* test(zig): absent-grammar case owns GITNEXUS_SKIP_OPTIONAL_GRAMMARS

The loader parses the opt-out variable lazily once per module copy, so
under `GITNEXUS_SKIP_OPTIONAL_GRAMMARS=zig` (or `all`) — a supported way
to run — the fresh loader took the opt-out branch and the "not the
opt-out path" assertion failed before the absent-binding path ran.

Clear the variable for the fresh module and restore it in `finally`,
instead of returning early: the branch stays exercised in every
environment. Verified: fails on the previous code under `=zig`, passes
with and without the variable now.

* fix(zig): declare the Union relation pairs — analyze aborted on any union

`gitnexus analyze` exited 1 on every Zig repository that declares a
`union` (including the zig-basic fixture itself): the member-ownership
commit made `union_declaration` a MEMBER_OWNER, so HAS_PROPERTY /
HAS_METHOD edges are emitted FROM a Union node, but `Union` was not in
LINKABLE_LABELS, so the schema's scope-bridge cross product never
generated a `FROM Union` pair and LadybugDB rejected the edge
(`labelPair: "Union|Property"`). Resolver tests stayed green because
they never write to the DB.

- `Union` joins LINKABLE_LABELS (also bridges `Tag{…}` constructor
  references); the three hand-written `→ Union` target pairs move to the
  generated half, per the STRUCTURAL_PAIR_DDL rule.
- structural-pair-coverage gains an optional-grammar corpus with
  zig-basic (`Union|Property`, `Union|Method` sentinels), skipped when
  the grammar is absent.
- Rust `union_item` note updated: the three gates it cited are widened.

Note for reviewers: the DDL fingerprint changes (#2808), so existing
indexes are rebuilt on next analyze.

* feat(zig): resolve path deps through the dep's build.zig and src/root.zig

The bare-name resolver only knew `src/<name>.zig` and `src/main.zig`.
`zig init` has written `src/root.zig` for libraries since 0.12, so the
default library layout never resolved. Now: the root the dep's own
build.zig declares (`b.addModule("<name>", .{ .root_source_file =
b.path("…") })`, name-matched module first), then src/root.zig,
src/<name>.zig, src/main.zig. `normalizeZigDepPath` is shared by the
loader and the resolver.

* feat(zig): Const/Variable defs, member imports, receiver typing, generic type constructors

Coverage gaps found by indexing idiomatic Zig against the branch:

- Const / Variable nodes: ZIG_QUERIES had no @definition.const /
  @definition.variable, so `pub const VERSION`, error sets and type
  aliases were absent and zigVariableConfig never ran. Rules are gated on
  the literal `const` / `var` keyword — tree-sitter-zig 1.1.2 parses
  statement assignments (`x = 5;`, `x += 1;`, `_ = expr;`) as keyword-
  less `variable_declaration`s, and the scope query minted a phantom
  local per assignment and one `_` per discard. Container and @import
  bindings are skipped via `shouldSkipDefinitionCapture`.
- Imports: `const X = @import("x.zig").X` (named / alias), `const X =
  ns.X` where `ns` is an @import binding of the file (promoted to a
  named import), and `pub usingnamespace @import(...)` (wildcard, with
  `expandsWildcardTo`). All three lost the file-level IMPORTS edge.
- Receiver typing: `var x: T = undefined` / decl literals `const x: T =
  .init()` (annotation), `var c = T.init()` / `mod.T.init()` (call
  return), `List(u8){}` (instantiation literal); `normalizeZigTypeName`
  drops the comptime argument list.
- Generic type constructors `fn List(comptime T: type) type { return
  struct {…}; }`: the returned container is a Struct/Union/Enum named
  after the fn, owns its members (HAS_METHOD / HAS_PROPERTY), binds in
  the module scope beside the Function def, and is emitted ahead of it
  so a named import binds the type.
- `export` vs `pub`: `visibility` is now `pub`-only (Zig-module fact);
  `isExported` keeps `pub|export` (visible outside the unit, as C's
  external linkage). `export fn` without `pub` is not reachable from
  other Zig files.
- Extractor configs share `zigContainerName`; ast-helpers' owner walk
  learns the type-constructor shape.

Tests: zig-idioms fixture (10 resolver cases), extractor/interpret unit
cases for each rule.

* fix(zig): address sixth gitnexus-check review pass

- Windows absolute `.path` deps (`C:\x`, `C:/x`) return null from
  `normalizeZigDepPath` like POSIX ones; a `/`-only check let them
  through as repo-relative.
- `parseZigBuildZon` accepts the `.dependencies = .{` header only at
  brace depth 1 (a direct field of the file's `.{`), so a same-named
  field nested in an earlier struct cannot hijack the block.
- `importsExecuteWhereWritten: false` on the provider: `@import` is
  compile-time name lookup (as C `#include`, Rust `use`); a body-level
  `@import` is no longer marked `runsOnlyWhenCalled`.
- Namespace imports record the MODULE as `importedName`
  (`zigModuleNameOf`: last path segment without `.zig`), per the shared
  contract; the local handle stays `localName`.
- Keyword-less `<ident> = @import(…)` (`_ = @import("x.zig")` in a test
  block) is a `side-effect` import: file edge, no binding. Only
  `const`/`var` declarations bind a name or feed alias promotion.
- `extractZigFunctionName` doc: an empty name is falsy, so the enclosing-
  function walk skips the test node and continues to the File; it does
  not "end" there.

Not re-fixed: "DIR_LANG has no zig entry" — carried over for the fourth
pass in a row; `test/helpers/literal-collectors.ts` has had `zig` in
`DIR_LANG` (line 91) and `BASENAME_LANGS` since the second-pass commit.

Regression tests: absolute-path spellings, nested `.dependencies`
decoy, namespace/side-effect interpretation, function-scoped `@import`
not deferred (all four fail on the previous source).

* fix(zig): address seventh gitnexus-check review pass

- The scope query's `@import` binding rules are keyword-gated (`"const"` /
  `"var"`, first-child anchored) like every other binding rule, and the
  keyword-less `<ident> = @import(…)` statement has its own
  `@import.side-effect` rule. Tree-sitter queries cannot express "no
  keyword child", so that rule also matches the keyword shapes and
  `emitZigScopeCaptures` drops those (they are the binding rules'
  matches). Behaviour is unchanged from the sixth-pass fix — the existing
  side-effect test covers it — the query text now carries the guard the
  finding asked for.

Not re-fixed: "DIR_LANG has no zig entry" — fifth pass in a row;
`test/helpers/literal-collectors.ts` has had `zig` in `DIR_LANG` since
the second-pass commit. Left for a human reviewer to close.

* fix(zig): resolve @import of the repo's own build.zig modules (F3)

Bare-name imports were resolved through build.zig.zon path deps only, so
the module a repo's ROOT build.zig declares for itself —
`b.addModule("lightpanda", .{ .root_source_file = b.path("src/lightpanda.zig") })`,
imported by name from 378/567 Lightpanda files — never produced an IMPORTS
edge, and nothing reached through `lp.X` resolved. A repo with a build.zig
but no build.zig.zon got no resolution config at all.

- language-config: `parseZigRootModules` (static scan of the root build.zig:
  `addModule("<name>", …root_source_file = b.path("<p>.zig")…)`, and
  `createModule`/`addModule` bindings named via `addImport("<name>", m)` or
  `.imports = &.{ .{ .name, .module = m } }`; generated / `.url` / computed
  modules are skipped) → `ZigBuildZonConfig.rootModules`.
- `loadZigBuildZon` → `loadZigBuildConfig`: reads the zon AND the root
  build.zig; null only when neither contributes.
- resolver: root modules are consulted before path deps; std/builtin/root
  still never resolve.
- fixtures: zig-idioms gains a Lightpanda-shaped root module (+ decoy
  `addOptions().createModule()`); new zig-rootmodule (build.zig, no zon).

Corpus (Lightpanda): IMPORTS 3014→3389 (378 edges to src/lightpanda.zig,
was 0), CALLS 13885→13989, ns.f() 79.3%→83.3%,
param.m() type=ns-qualified 43→46/417.

* fix(zig): import every @import in expression position; resolve @import("x").f()

Both query sets only saw `@import` as the value of a const/var or under
`usingnamespace`, so an @import in any other position produced no file
edge: Lightpanda's `pub const Interfaces = .{ @import("a.zig"), … }`
registration table (288 modules), call arguments
(`CounterEnum("size", @import("ArenaPool.zig").BucketSize)`), comparison
operands (`JsApi == @import("x.zig").JsApi`) and member-call receivers
(`try @import("dump.zig").root(...)`) — 417 of 3,401 in-repo import pairs
had no IMPORTS edge, and the 80 inline-receiver calls resolved 0 times.

Scope query: a catch-all `@import.inline` rule matches every `@import`
builtin; `emitZigScopeCaptures` drops the ones a binding rule (or the
keyword-less side-effect rule) already claimed (by string-node id) so a
bound import is never doubled, emits the rest as side-effect imports once
per distinct source per file, and binds a member-call receiver as a
namespace import whose local name is the builtin's own text — the
`@reference.receiver` text on that call is identical, so the shared
namespace-receiver lookup (Case 1) resolves the member in the imported
module.

ZIG_QUERIES: the three variable_declaration/usingnamespace-anchored
`@import` rules collapse into the same single builtin rule (the structure
phase only skips import matches; one match per builtin keeps
tree-sitter-languages' exact-capture assertion intact).

Lightpanda corpus (zig-corpus-check, before → after): IMPORTS 3014 → 3426,
in-repo pairs missing 417 → 5 (4 under a default-ignored `cache/` dir, 1 a
commented-out import the census regex counts), `@import(..).f()` 0/80 →
72/80 (the 8 left are `@import("root")` and non-import builtins the census
mislabels), CALLS 13885 → 13959; every other line unchanged.

* feat(zig): model file-structs — a file with top-level fields is a Struct named after the file

In Zig every file is a struct; one that declares top-level fields is an
instantiable type whose name is the file stem (`Page.zig` declares `Page`,
`@typeName` agrees), and its top-level `fn`s taking `self` are its methods.
Lightpanda spells 413 of 567 files this way and, before this, `page.getArena()`
on a `page: *Page` parameter resolved 23 of 993 times (2.3 %) — `impact` on
`Page.getArena` reported 0 callers for 159 call sites, and 2,395 top-level
fields were ownerless Property nodes.

Definition phase: `((source_file (container_field …)) @definition.struct)` +
the class extractor names it from the file path (`zigContainerName(source_file,
filePath)`); top-level fns/fields are owned through the new
`LanguageProvider.resolveFileTypeOwner` hook (consulted by
`findEnclosingClassInfo` when the walk reaches the tree root, and by the
method/field extractors' owner lookup) — ids become `Method:<file>:Page.getArena#0`
/ `Property:<file>:Page.session` with HAS_METHOD / HAS_PROPERTY edges.

Scope phase: `emitZigScopeCaptures` emits a Class scope over the whole file
(same range as the Module scope, nested under it — the pair `canParentScope`
already admits) plus a Struct def anchored on it; member NAME bindings are
hoisted back to the Module scope by `zigBindingScopeFor` so `Page.init()`
(namespace member) keeps working, while ownedDefs stay in the Class scope so
`populateClassOwnedMembers` stamps the owner. The file-level `const Page =
@This();` alias no longer mints a Const (it would shadow the Struct); `@This()`
aliases in type position (`self: *SigHandler` in Sighandler.zig, nested
`Self`) are rewritten to the container name so receivers resolve. A namespace
import of a `.zig` file gets a NAMED twin of the file stem so `x: *Page` in the
importer binds the type as well as the module.

Shared, additive: `resolveFileTypeOwner` hook; `filePath` threaded to
`ClassExtractionConfig.extractName` / `extractOwnerName`; nameless
`definition.struct` passes `getLabelFromCaptures` like `definition.class`
already did (extractor synthesizes the name).

Lightpanda corpus (zig-corpus-check): param receivers typed by a file import
23/993 → 993/993; `self.m()` 99.2 → 100 %; annotated locals 11.6 → 23.2 %;
CALLS 13,885 → 15,689; HAS_METHOD 1,477 → 8,003; ownerless Property 2,395 →
276; Function/Method 8,378/1,518 → 2,562/7,330; no row regressed.
Fixture `zig-filestruct` (Page/Session/Sighandler/util) + unit tests pin the
shape, the stem naming, the alias rewrite, the namespace twin and the
unchanged namespace-file behaviour.

* test(zig): expression-position import case sees the file-struct type twin

* fix(zig): stop reading member calls `x.f(arg)` as direct calls named `f`

tree-sitter-zig spells `field_expression` as `object:`/`member:`; the shared
callable-flow reader only knew `property`/`field`/`method`, so every Zig
member call collapsed to a DIRECT call named after the member and the
solver fanned each argument out to every same-named callable
(4,761 cap warnings on Lightpanda, `Global.deinit -> Global.deinit`
self-loops through `pub const release = deinit;`).

Shared (grammar-neutral, receiver-gated):
- `memberParts` also reads `member` (only C/C++ `offsetof_expression` and
  JS `class_body` expose that field, without a receiver field).
- A member call is a field-stored-callable invoke only when a MEMBER store
  (`o->run = handler`, `self.f = target`) or a declared callable-typed field
  is visible — a same-named plain binding no longer gates it.
- `direct-callee-name` requires a direct designator: `.init(x)`,
  `' '.join(x)`, `string.Join(x)` name no callee to seed by simple name.
Zig: formals are numbered without the leading `self`, so `r.run(target)`
joins `cb` and yields `Runner.run -> target`.

Goldens for python/csharp regenerated: the only drift is the dropped
`direct-callee-name` on `' '.join(...)`, `text.strip().ljust(...)`,
`string.Join(...)`.

Lightpanda: cap-warnings 4761 -> 2, cvf self-loops 10 -> 0,
CALLS 13885 -> 13857 (28 removed, all callable-value-flow: 10 self-loops,
17 same-name fan-out, 1 lost `on -> TypeErased.start`; +1 correct
`Arena.alloc -> allocator`).

* fix(zig): bind container field types so `self.field.m()` resolves (F5)

A container's field types were never bound on its Class scope: the scope
query's `container_field` rule captured only the name, and
`emitZigScopeCaptures` synthesized no `@type-binding.field` group. The
compound resolver reads member types from that scope
(`typeOfMemberOnClass` → `classScope.typeBindings.get(field)`), so
`self.session.name()`, `self.counter.incr()` — Lightpanda's dominant
cross-object call shape — resolved 9 of 2803 times (0.3 %).

- query.ts: capture `type: (_)? @declaration.field-type` on
  `container_field` (enum variants have none).
- captures.ts: per typed field, push a `@type-binding.field` group (name =
  field, type = the type text, `@This()` aliases rewritten to the container
  name like parameter types); anonymous inline containers are skipped. The
  binding lands on the container's Class scope — the file's Class scope for
  a file-struct — since `zigBindingScopeFor` hoists only declaration names.
- query.ts/captures.ts: `const page = self.page;` / `var s = self.session;`
  one-level field aliases become `@type-binding.alias` bindings whose "type"
  is the RHS path; the resolver's member-alias branch re-resolves it as a
  receiver chain. Import aliases (`const Counter = counter.Counter;`) are
  dropped — they are named imports.
- interpret.ts: `@type-binding.field` → 'annotation',
  `@type-binding.alias` → 'assignment-inferred' (an annotation on the same
  binding wins).

Corpus (Lightpanda, zig-corpus-check): self.field.m() 11/2803 (0.4 %) →
1433/2803 (51.1 %); ident.m() bound=local-field-access 29/1902 (1.5 %) →
307/1902 (16.1 %); chain.m() 146/5022 (2.9 %) → 642/5022 (12.8 %);
CALLS 15838 → 18066. self.m() / free f() / ns.f() unchanged.

Tests: unit (zig-extractors) — one @type-binding.field per typed field with
sigils stripped and aliases rewritten; the binding hosted on the container's
Class scope (file-struct: the file's Class scope, not Module) with the
written spelling as declaredSpelling; field aliases bound to the RHS path
and never for import aliases. Integration (zig-idioms `holder.zig`,
zig-filestruct `Page.zig`): `viaField → incr` ×2 into counter.zig,
`viaAlias → get/twice`, `sessionName → name` / `sessionLabel → name` into
Session.zig. All fail without the change. The optional-payload capture
`if (self.opt) |c| c.incr()` is not asserted (F6).

* fix(zig): `pub const X = @import(…)` at file scope republishes X (reexportsName)

Lightpanda's `lightpanda.zig` is one long list of `pub const Arena =
@import("Arena.zig");`, and most files name their types through it (`const
lp = @import("lightpanda"); const Arena = lp.Arena;`, `arena: *lp.Arena`).
The scope side treated those bindings as plain imports of the hub file, so
the hub never published the names it re-exports and a third file's `const
Arena = lp.Arena;` (promoted to a named import of `Arena` from the hub) found
nothing.

`emitZigScopeCaptures` now marks named/alias import groups whose declaration
is a file-level `pub const` — the `@import(...).X` form, the alias promotion
`pub const Bar = ns.Bar`, and the file-struct type twin of `pub const Arena =
@import("Arena.zig")` — and `interpretZigImport` sets the shared contract's
`reexportsName: true` on them (the Python `__init__.py` shape, consumed by
`buildReexportClosures`). Private and fn-local bindings stay unflagged.

Not covered here: a receiver ANNOTATED with the dotted hub path (`arena:
*lp.Arena`) — Case 3 of the receiver-bound pass looks the member up with
`findExportedDef`, which only sees locally declared names; following
re-exports there is a shared change left for a follow-up.

* fix(zig): type receivers through `const X = <type expr>;` aliases (F7)

`const LocalAlias = Local;`, `const T2 = Thing;` (alias of an alias/import)
and `const B = util.List(u8);` (an INSTANTIATED generic type constructor)
were plain `@declaration.variable` bindings, so `LocalAlias.mk()`,
`var l = LocalAlias.mk(); l.go()`, `T2.make()`, `B.init()`, `B{}` and
`var x: B` all typed nothing (review repro r3-flow b1..b9; Lightpanda:
`pub const Proto = HtmlElement;` x68, `const Allocator = std.mem.Allocator`
x104, `pub const KeyIterator = GenericIterator(...)`, fn-local
`const R = ...(...)`).

Model: a `@type-binding.alias` binding of the alias NAME to the value's type
text — Rust's `let x = y` / JS's `const B = Foo`, source
'assignment-inferred' — NOT a TypeAlias def. Reasons: (1) the shared
machinery already chains typeBindings (`followChainedRef` in the extractor,
`followChainPostFinalize` after propagation), so `var l = LocalAlias.mk()`
and `var x: B` reach the target through the alias with no new shared code;
(2) nothing shared follows a `TypeAlias` def to its target — `isShapeLike`
only makes the alias itself a member owner (TS object-type aliases) — so a
relabel would have needed language-named shared code; (3) graph node ids
are UNCHANGED: every alias stays `Const:<file>:X`. `normalizeZigTypeName`
already drops the comptime arguments, so `util.List(u8)` binds `util.List`
and resolves through the namespace import (Case 3). The identifier /
member shapes also take `var` (`var cur = orig; cur.go()` — the cursor
idiom, same binding as Rust's `let x = y`).

Heuristic, stated as such: a CALL value is kept only when the callee's last
identifier is TitleCase (Zig's naming convention for types), because the
grammar cannot tell `util.List(u8)` from `util.makeThing()` and the latter
belongs to the call-return rules; the call-return group is dropped for the
same TitleCase shape so the two never race on match order. Import bindings
(`const Stack = @import("x.zig").Stack`), promoted namespace-member aliases,
enum/decl literals (`.foo`) and the `type:` annotation of
`var b: T = undefined;` are excluded.

Not done: the two-hop `pub const bridge = js.Bridge(T); bridge.accessor()`
chain. `js.Bridge` is a Function that RETURNS `bridge.Builder(T)` (a call,
not a container), so the alias binds `js.Bridge`, Case 3 finds a Function
with no members in js.zig, and Case 3b is skipped for a namespace head.
Following that hop needs a namespace-member return-type route in shared
code (or a Zig `resolveQualifiedReceiverMember` hook that re-implements
member lookup without the model); left for a follow-up.

Corpus (Lightpanda, `harness/zig-corpus-check.mjs`): CALLS 15838 -> 16051
(+213, 0 removed), `ident.m() bound=local-alias` 6/63 -> 36/63,
`local-call` 159 -> 176, `module/unknown` 110 -> 116, `local-other`
271 -> 273; `self.m()`, `free f()`, `ns.f()` unchanged or up.

* fix(zig): one alias rule set — F7's alias rules subsume F5's field-access alias rules

* fix(zig): type locals through try/catch/orelse, return types and payload captures

F6 of the gitnexus-check review. Three gaps in the value flow that types a
local receiver, all measured on Lightpanda:

1. `@type-binding.call-return` needed the `call_expression` as the DIRECT
   value child, so `const p = try Page.init(…)` (2,551 sites), `… catch
   return` (410) and `… orelse return` typed nothing. The rule is now one
   keyword-gated declaration match; `emitZigScopeCaptures` unwraps `try`,
   `catch`, `orelse` and parentheses (`zigUnwrapValue`) and decides what the
   value types (`zigCallReturnTypeOf`): a module-level receiver still names
   the type (`Counter.init()` → Counter, Rust `Foo::new()`); a free call
   binds the callee name (`makeThing`); a member call on a fn-LOCAL receiver
   (parameter / local / payload — Zig forbids shadowing, so "declared in the
   fn" is exact) binds the compound `node.asElement()` the shared resolver
   walks to the method's return type — instead of typing `el` as `Node`. A
   TitleCase callee (`List(u8)`) is a type constructor and binds nothing.

2. No `@type-binding.return` existed. `fn make() !*Thing` now binds
   `make ↦ Thing` in the enclosing scope (Module for free fns, the container's
   Class scope for methods, where the compound resolver reads it). Builtins,
   `type`, `@TypeOf(…)` and comptime type parameters (`?*T`) bind nothing;
   `@This()` / `Self` returns name the container. `normalizeZigTypeName` now
   strips the error union BEFORE the payload's sigils, so
   `Allocator.Error!*Page` → `Page` (it used to leave `*Page`).

3. Payload captures had no binding at all. `populateZigRangeBindings`
   (registered as `populateRangeBindings`) types `for (items) |it| / |*it|`,
   `for (items, 0..) |it, i|`, `if (opt) |v|`, `if (call()) |v|`,
   `while (it.next()) |x|` from the SUBJECT's written type minus one layer
   (`[]T` element, `?T` payload) — declining when the layer is not visible
   (`ArrayList(T)`) — and the same projection for `const t = items[i]` /
   `opt.?` / `ptr.*`. `catch |err|` and `switch` prongs are skipped.

Corpus (Lightpanda, gate before → after): CALLS 15838 → 17853;
`ident.m() bound=local-try/catch/orelse` 11/1298 → 584/1298;
`local-call` 159/1282 → 398/1282; `payload` 82/1085 → 233/1085;
`other-recv:call_expression` 5/991 → 457/991; `local-other` 271 → 364;
`self.m()` 100 %, `free f()` 97.5 %, `ns.f()` 83.4 % unchanged; nothing down.

Not covered: `const t = ns.f()` (a namespace fn's return type across files —
Case 3 has no path from a namespace head to a callable's return binding),
and expression receivers (`items[i].run()`, `o.?.run()`).

* fix(zig): resolve leftover fixture merge markers (Page.zig)

* fix(zig): reconcile F6 value inference with F7 aliases and F5 field bindings

- A fn-local TitleCase receiver (`const R = generic.List(u8); var l =
  R.init();`) is a type alias (F7), not a value local: `R.init()` names the
  type `R` like `Counter.init()` does at module level, so `l` chains
  R → util.List → push. F6's local-receiver rule now excludes TitleCase heads.
- The F6 unit helper only collects the value-inferred / return kinds it
  owns; F5 field and F7 alias bindings for the same names are asserted in
  their own suites.

* fix(zig): give function-local and anonymous containers an identity (F8)

`const R = struct {…}` declared inside a fn (Lightpanda's reflection.zig
has ~20, one per builder) all collapsed onto one `Struct:<file>:R` with one
`R.get`; anonymous containers (`std.sort.pdq(…, struct { fn lessThan … }
.lessThan)`, `const byte_size = struct { fn it … }.it;`, `?struct { min,
max }` field types) had no identity at all, so their fns were OWNERLESS
Methods (`Method:<file>:lessThan#3`) that collided across a file.

`zigContainerName` now yields the graph IDENTITY on both phases:
  - function-local named: `<enclosing callable>$<name>` — `Reflect.string$R`
    (Java local-class `$` chain; `populateClassOwnedMembers` leaves it whole);
  - anonymous: `<host>$<ordinal>` — `build$1`, `Outer$1`, `Page$1`
    (javac's `Outer$1` numbering per host, in source order);
  - a `test` host is keyed `test@L<line>` (its string does not survive the
    class extractor's qualified-name normalization).
`zigContainerBindingName` keeps the spelling code writes (`R`) for scope
bindings and `@This()` alias rewrites (`@declaration.binding-name`).

Structure phase: bare `(struct|enum|union|opaque_declaration)` rules mint the
local/anonymous nodes via the class extractor; `shouldSkipDefinitionCapture`
keeps exactly one rule per container (`zigContainerAnchor`); a new
grammar-neutral `resolveContainerTypeOwner` provider hook lets the shared
owner walk name a container from context, so `Method:<file>:Reflect.string$R
.get#0` and its HAS_METHOD source agree by construction. Scope phase: the
wrapper group splits name/binding-name for locals and anonymous containers
get synthesized `@declaration.<kind>` defs (`is-synthetic`).

Lightpanda: ownerless Methods 14 → 0, fns without a node 55 → 0, ownerless
Properties 276 → 5, HAS_METHOD 8003 → 8074, HAS_PROPERTY 7275 → 7403,
CALLS 15838 → 15857, Struct 1905 → 2199; no resolution bucket dropped.

* fix(zig): re-add the implicit receiver on the call side so both method-call spellings reach the callback formal

`extractFunctionParameters` sliced the leading `self` off the formals, which
lined up `r.run(target)` (target@0 ↔ cb@0) but lost the explicit spelling
`Runner.run(&r, target)` (&r@0, target@1 ↔ cb@0): the callback never joined
its formal and `run → target` was missing (PR #1432 review by koriyoshi2041).

Formals are numbered once per function while the receiver differs per call
shape, so the fix lives in `extractCallArguments`: keep `self` as formal 0
and prepend the receiver as actual 0 when the callee is a member call on a
VALUE receiver — chain head is a fn-local name that is not TitleCase, the
same value-vs-type rule F6 uses. Namespace / type / decl-literal receivers
(`Runner.init(cb)`, `helpers.apply(cb)`, `List(u8).init`, `.init(cb)`) get
no prepend. Known residual gap, documented: a module-level value receiver
(`global_runner.run(cb)`) is not fn-local and still misses.

Tests: the F2 integration case now asserts both spellings plus a namespace
call; the unit contract pins `self@0, cb@1` and the per-call actual index.

* fix(zig): address eighth gitnexus-check review pass

- Named dependency modules: `parseZigBuildModuleRoots` scanned
  `addModule("<name>", .{ … })` with a `[^}]*` regex, so a nested field
  before `.root_source_file` (`.imports = &.{ .{ … } }`) ended the match
  at the inner `}` and demoted the module to an unnamed fallback — the
  first exe/lib root in the file then answered `@import("<name>")`. The
  named lookup now walks the balanced `addModule(…)` argument list
  (same scanner as `parseZigRootModules`, comment-stripped, string-aware);
  the unnamed fallbacks are unchanged. Regression test in
  `zig-import-resolver.test.ts`.
- Type-position `@import`: `var x: @import("m.zig").T = undefined;` was
  read as an import binding of `x` on both sides — the query rules match
  the `type:` child like a value, and `isZigContainerOrImportBinding`
  scanned every named child — so `x` was never declared and became a
  named import of `T`. The helper now skips the `type:` field, and
  `emitZigScopeCaptures` drops binding-rule matches whose `@import` sits
  in the annotation (`isZigTypePositionImport`) without claiming the
  source, so `x` binds as a variable and the file edge survives as a
  side-effect import. Regression tests in `zig-extractors.test.ts`
  (variable extractor + scope captures).
- Union in the class-capture skip guard: the parse-worker's inline
  class-like predicate lacked `Union`, so a `Union` definition bypassed
  `shouldSkipClassCapture` unlike every other `ClassLikeNodeLabel`.
  Added the label; no Zig behavior changes (Zig defines no skip hook), so
  no test.
- File-owned method ids in `findEnclosingFunctionId`: the arity lookup
  used `findEnclosingClassNode` while the owner came from the file-owner
  aware `cachedFindEnclosingClassInfo`, so a Zig file-struct's top-level
  fn produced `Method::Page.get` without the `#<arity>` suffix. It now
  uses `findEnclosingClassNodeOrFileOwner`, the definition-phase lookup.
  Consistency fix only: `ParseWorkerResult.calls` / `.assignments` (the
  sole consumers of this id) are merged but not read since #942 — CALLS
  edges come from the scope pipeline, whose ids were already right — so
  no observable graph change and no test.

Not re-fixed:
- `test/helpers/literal-collectors.ts` `DIR_LANG` has no `zig` entry
  (raised for the seventh time): the entry exists (`zig:
  SupportedLanguages.Zig`, added by the second-pass commit), so
  `languages/zig/**` literals are already validated against the Zig
  grammar alone; documented in the PR body since the fifth pass.

* fix(zig): keyword-gate the constructor type-binding rules

The three `@type-binding.constructor` rules (`const p = T{…}`, `mod.T{…}`,
`List(u8){…}`) matched any `variable_declaration` with an identifier and a
`struct_initializer`, keyword or not — and tree-sitter-zig 1.1.2 parses a
re-assignment `p = T{…};` (and `_ = T{…};`) as the same node type. So an
assignment minted a constructor binding for `p` in its own block, and one
for `_`. Zig's static typing makes the extra binding redundant (`p`
already carries its type from its declaration: annotation, constructor or
inferred value), so it cost little, but it declared nothing and stood out
against every other binding rule (`@declaration.variable`, the import
rules, the call-return rules), which are keyword-gated for exactly this
shape. Split each rule into `"const" .` / `"var" .` variants, like the
call-return rules.

Regression test in `zig-extractors.test.ts`: `p = T{…}`, `q = mod.T{…}`,
`l = List(u8){}` and `_ = T{…}` after their declarations yield only the
three declaration bindings (fails on the previous query). The zig,
callable-value-flow, grammar-literal and tree-sitter-languages suites are
unchanged.

Raised twice by gitnexus-check (passes on 2026-08-18 12:42 and 12:56).

* fix(zig): re-baseline the callable-flow capture fingerprints, keep `await f<T>(x)` a direct callee

The `benchmarks (GITNEXUS_BENCH)` CI job gates two capture fingerprints
that this branch's shared callable-flow change (3e62b99a) moved without
re-baselining: `bench/python-scope/baseline-fingerprint.txt` and four
languages in `bench/scope-capture/baselines.json` (csharp, cpp,
typescript, kotlin). Both `--check` runs pass on origin/main and failed
on this branch; every other language matched its baseline on the same
run.

Drift, verified by dumping the canonical matches on both trees:
- csharp / kotlin / python: exactly the intended change — a MEMBER call
  (`string.Join(x)`, `.forEach { }`, `' '.join(x)`, `.ljust(w)`) no
  longer carries `direct-callee-name`; the argument fact is unchanged.
- cpp: `choice.select(1)` (cpp-deleted-overload) drops an INDIRECT
  invoke + its synthetic `@reference.call.free` that were gated only by
  the same-named free `select` binding; the site is a genuine method
  call already captured as `@reference.call.member`. capture_groups_fp
  4605 -> 4601.
- typescript: `await svc.verify<T>(x)` (member) and `initializer()(cb)`
  (call-of-call) drop the name as intended. But `await verifyToken<T>(x)`
  — a DIRECT call — lost it too, because tree-sitter-typescript parses
  `await f<T>(x)` as `call_expression(function: await_expression(f),
  type_arguments, …)` and the new direct-designator gate saw an
  await_expression, not `f`. `wrappedExpression` now unwraps
  `await_expression` (no named field, so the field-based unwrap missed
  it), restoring parity with main for the direct spelling while the
  member spelling stays nameless. Regression test added; it fails
  without the unwrap on both assertions.

Gates run locally: scope-capture --check (15 languages), python-scope
--check, import-target --check, tsc, eslint, prettier, the callable-flow
/ golden / tripwire / resolver test files (33 files), full suite with
coverage (82.9/71.5/89.1/86.4 vs 26/23/28/27 thresholds).

* test(zig): fail CI when the optional Zig grammar is absent

`@tree-sitter-grammars/tree-sitter-zig` is an optionalDependency, so every
Zig suite gates on `isLanguageAvailable(Zig)` and the ABI load-smoke accepts
a clean load failure for an optional grammar. Both are the right contract for
a platform with no prebuild, and together they leave a hole: if the grammar
never installed on any CI runner, this PR would merge with all eight Zig
resolver suites plus the structure-phase suite reported green-by-skip, having
never executed the native Zig parser once.

Close it with the `GITNEXUS_REQUIRE_FTS` idiom already used for the FTS
suites. `GITNEXUS_REQUIRE_ZIG=1` declares "this runner has a prebuild, the
grammar MUST be here", and a missing grammar becomes a failure instead of a
skip. tree-sitter-zig@1.1.2 publishes prebuilds for {darwin,linux,win32}-
{x64,arm64}, so the flag is set on two required jobs that all run on covered
platforms: the sharded ubuntu `tests` job and the three-OS `abi-assert` job.

- test/helpers/optional-grammar.ts: the registry mapping a language to its
  require-variable, plus `describeGrammarPresence`, a presence assertion that
  FAILS when required-but-absent. Deliberately a separate test rather than
  flipping the suites from skip to fail: a skipped suite reports success, so
  only a failing test can turn "Zig never ran" into a red job.
- parser-loader-abi.test.ts: the optional exemption is revoked for a language
  the environment declares required, so an ABI-broken Zig binding fails the
  smoke instead of passing as a clean absence.
- optional-grammar-gate.test.ts: pins the two ways the gate could silently
  never fire — reading a variable name CI does not set, or accepting a value
  CI does not write.

Nothing changes for a run that leaves the variable unset: local runs and any
future prebuild-less platform still skip. Verified both directions --
`GITNEXUS_REQUIRE_ZIG=1` alone: 149 passed, 0 skipped; with
`GITNEXUS_SKIP_OPTIONAL_GRAMMARS=1` forcing the grammar away it fails 2 tests
with an actionable message; with the skip flag but no require flag it is
green-by-skip exactly as before.

Addresses the test-only blocker in the gitnexus-check review of #1432.

* fix(zig): type the optional-grammar gate by grammar key, not language

The gitnexus-check finding on parser-loader-abi.test.ts:155 is right, and
none of the gates caught it: `tsconfig.json` includes only `src/**/*`, so
neither `tsc --noEmit` nor CI typechecks the test tree, and vitest strips
types without checking them. Confirmed with a scoped tsc run over the file:
`error TS2345: Argument of type 'string' is not assignable to parameter of
type 'SupportedLanguages'`.

Not fixed with the proposed `key as SupportedLanguages` cast, which would
assert something false: `listGrammarSources()` yields one row per SOURCES
entry, including variants like `typescript:tsx` that are not enum members.
`isOptionalGrammarRequired` now takes the grammar KEY it is really given,
and the registry keeps a `satisfies Partial<Record<SupportedLanguages,
string>>` so every key we write is still pinned to a real language.

Two new cases cover the failure mode the type error was pointing at — a
registry key that can never match what the ABI smoke passes, leaving the
gate configured-looking and permanently inert: every OPTIONAL_GRAMMAR_ENV
key must be a key `listGrammarSources()` yields and must be marked optional
there, and an unregistered variant (`typescript:tsx`) must not be required
even with the variable set.

Same blind spot, two more latent errors in files this PR adds, both fixed:
`Parser.Language` is not an exported member (use the `setLanguage` parameter
type, as parser-loader-abi.test.ts already does), and the `ParsedImport`
filter did not narrow the union, so `localName` was read through a `!` on an
arm that has no such property — now a type predicate. The one remaining
error under the same probe, in `resolvers/callable-value-flow.test.ts:319`,
predates this branch (authored 2026-07-17, on main) and is left alone.

structural-pair-coverage's optional-grammar case switches from
`it.concurrent.each` to `it.concurrent.for`: only `for` passes the test
context as a second argument (`each`'s callback is `(...args: T[])`), and
that context carries the dynamic `skip()` the per-language gate calls.
Behaviour is unchanged — grammar present: 10 passed; grammar forced away:
9 passed, 1 skipped.

Whole test tree typechecking is a separate, much larger job: the same probe
over `test/**` minus fixtures reports 734 pre-existing errors across the
repo. Out of scope here.

* fix(zig): address tenth gitnexus-check review pass

- `normalizeZigDepPath`: normalize backslashes BEFORE the absolute-path
  check. A UNC dep (`\\server\share\dep`) used to slip past the check and
  normalize to the repo-relative `server/share/dep`; root-relative `\dep`
  had the same hole. Both now return null. Regression case added to the
  absolute-spellings test with the files those misreadings would resolve.
- `bindPayloads`: a pointer capture `for (pages) |*p|` now records `*Page`
  (declaredSpelling) instead of `Page` — the `*` is an anonymous payload
  child before the identifier. Method dispatch is unchanged (`rawName`
  strips the sigil), but a deref projection `const q = p.*;` now sees the
  pointer layer. New fixture fn `viaPtrCaptureDeref` + assertion; fails on
  the previous code (verified by stashing the src fix).
- `optional-grammar-gate.test.ts`: renamed the `typescript:tsx` case — the
  key IS a registry row; what makes it inert is the missing gate entry. Now
  also asserts a key no registry yields.
- `structural-pair-coverage.test.ts`: header updated — ten tables (not
  eleven) are absent from every rule's target side; `Union` left the set
  when Zig made it linkable.
- `language-classification.ts`: doc comment now names zig in the
  experimental set (added after Ring 1).

Not re-fixed (invalid findings):
- "owner-hook contract wired to an undeclared variable": stale-diff read —
  `findEnclosingClassInfo` declares `resolveFileTypeOwner` /
  `resolveContainerTypeOwner` as optional parameters (ast-helpers.ts:905,
  917) and parse-worker threads them at every call site; tsc compiles clean.
- "optional Zig grammar added unconditionally to the parsing fixture
  suite": the cited block only `fs.readFile`s the committed fixture file to
  assert it is non-empty — no parser or grammar load is involved.

* fix(scope-resolution): mark construction-site CALLS edges in reason (opt-in), enable for Zig

PR #1432 human review, item 2: a Zig struct literal `T{ .f = x }` (no
parens) is modelled as a CALLS edge to the type — the Rust `T { .. }` /
Go `T{}` shape — and nothing on the edge told it apart from an invocation
(`get_next_spawn → SpawnRequest` from seven `return SpawnRequest{ … }`).

`ScopeResolver.markConstructionSites` (default off): when set, the edge
emitted for a `callForm === 'constructor'` site gets ` (constructor)`
appended to its reason, in both emit paths — `local-call (constructor)` /
`import-resolved (constructor)` in the free-call fallback and
`scope-resolution: call (constructor)` in the reference bridge. The Zig
resolver opts in. `Reference` gains an optional `callForm`, copied from
the site by `buildReference`, so the bridge can see the form.

Why `reason` and not a property or edge type: relationships carry no
arbitrary properties, a new column changes the relation DDL and moves
SCHEMA_FINGERPRINT, and `reason` is the channel the IMPLEMENTS `-pointer`
receiver form already uses. Why opt-in: the unsuffixed strings are a
pinned contract asserted verbatim by the other language suites
(php/cpp constructor calls expect exactly `import-resolved`); every
non-Zig edge stays byte-identical.

Tests: `references-to-edges-call-form.test.ts` pins both vocabularies
and the default-off behaviour; `zig.test.ts` asserts
`Reflect.string → Accessor` / `Reflect.url → Accessor` carry
`local-call (constructor)` next to a plain invocation, and that every
marked edge targets a Struct.

* feat(zig): track qualified struct literals (`mod.T{…}`) as construction sites

PR #1432 re-test (issue comment on 97571d23): 163 qualified literals
`mod.Type{ … }` in a real project produced no CALLS edge at all, so only
same-file and imported-name literals were tracked as construction sites.

One query rule captures `(struct_initializer (field_expression object
member))` as `@reference.call.constructor` WITH the receiver. Captured as a
free constructor instead, the site resolves by its simple tail and a
workspace-unique `Thing` answers for `c.Thing{}` whichever module the
source named (measured: c.zig defines no `Thing`, the edge went to a.zig's).
With the receiver the site takes the receiver-bound namespace case — the
path `mod.fn()` takes — which resolves inside the module the receiver is
bound to: `a.Thing{}` / `b.Thing{}` bind their own files, `c.Thing{}` binds
nothing, `std.Thread.Mutex{}` binds nothing next to a local `Mutex`.

That case's edge now goes through `constructionSiteReason` too, so the
opt-in marker (`import-resolved (constructor)` / `global (constructor)`)
reaches it; `markConstructionSites` joins `ReceiverBoundProviderSubset`.
Byte-identical for every provider that does not set the flag.

Also answers the twelfth gitnexus-check pass: the `bodyNodeSet.size === 0`
guard on the extractor factories' no-wrapper branch is deliberate (a config
with wrappers whose node lacks one is a bodiless declaration); the two
comments now say so instead of reading as a universal last resort. Go's
method config, the only other empty-`bodyNodeTypes` config, never reaches
the branch (its `extract()` gates on method/function nodes the class-node
caller never passes).

Tests: new `zig-qualified-literal` fixture (same-named `Thing` in two
modules, a module without it, an external `std` qualifier next to a local
and an imported `Mutex`); `zig-basic` pins `pioneer.Pioneer{…}` and the
union `pioneer.Tag{…}` as marked construction sites.

* feat(zig): resolve hub re-exports, enum-variant receivers and type-named receivers (real-project audit)

Audit of three real Zig projects indexed with this branch (tigerbeetle 246
files, mach 132, ghostty 788): method reachability was 63 % / 35 % / 55 %,
and three shapes accounted for most of the misses.

1. Hub modules. Zig projects publish types through a file made only of
   re-exports (`pub const Terminal = @import("Terminal.zig");`, `pub const
   PRNG = @import("prng.zig");`, `pub const Thing = @import("thing.zig")
   .Thing;`). Such a file owns NO local binding, and `findExportedDef` reads
   local bindings only — so `terminal.Terminal.init()`, `t: stdx.Thing`,
   `var p = stdx.PRNG.from_seed()` and `h: stdx.BoundedArrayType(u8, 4)`
   all resolved to nothing. Measured before → after: CALLS into ghostty's
   `src/terminal/` from outside it 46 → 253 (150 `terminal.Terminal.` sites
   alone); into tigerbeetle's `stdx` hub from outside it 837 → 1500 (136
   static calls, 289 annotations). Method reachability: tigerbeetle
   2249 → 2272 of 3544, ghostty 2766 → 2865 of 5016, mach 1047 → 1051 of
   2967 (mach's hub publishes generic instantiations, `pub const Quat =
   q.Quat(f32)`, a shape this commit does not cover).
   `findExportedDefIncludingImportedNames` reads the finalized channel
   (origin import / namespace / reexport, def already resolved to the
   declaring file), refusing a name bound to two distinct defs. Opt-in per
   provider (`namespaceExportsIncludeImportedNames`): a module's imports are
   not its exports in most languages; Zig opts in because a hub member a
   consumer can name is public by construction. Used by receiver-bound Case
   1, Case 3, the compound resolver's namespace branch, and a new Case 2
   route that resolves a namespace-qualified class receiver (`stdx.PRNG`)
   through the same lookup.

2. Enum variants as receivers. `Operation.create_accounts.event_max()`
   (147 sites in tigerbeetle): a variant has no written type, but it has
   one — the enum itself. `emitZigScopeCaptures` now emits a field type
   binding per enum variant, so the field walk that already handles
   `self.session.name()` types `Op.create` as `Op`.

3. Receivers named after their type. `self` is a convention, not a rule:
   tigerbeetle writes `replica: *Replica` (777 of 1127 methods), mach
   `pool: *@This()` (764 of 833). Reading only `self` as the receiver
   labelled all of them `isStatic: true`, counted the receiver in their
   arity (`Counter.incr#1`) and sourced the scope binding as a plain
   parameter. `zigReceiverParameter` is the single rule for both phases:
   the FIRST parameter when named `self`, or typed as the enclosing
   container (`@This()`, its binding name, a `const X = @This();` alias),
   pointers / const / optionals stripped.

Fixtures `zig-hub` and `zig-receivers` pin each shape, including the
refusals: a private hub import does not leak, a foreign-typed first
parameter is not a receiver, a factory stays static.

* fix(zig): address thirteenth gitnexus-check review pass

- File-struct receivers named after the file stem were always static: the
  method builder called `isStatic` / `extractReceiverType` /
  `extractParameters` without the extractor context's `filePath`, so
  `zigReceiverParameter` could not name a file-struct (`fn add(ledger:
  *Ledger)` in `Ledger.zig`, no `Self` alias) and the fn came out static
  with the receiver in its arity (`Ledger.add#2`) — an id the scope side,
  which always has the path, never produces, so its CALLS edges went
  nowhere. `MethodExtractionConfig` now passes `filePath` as an optional
  trailing argument to those three hooks (same shape as
  `extractOwnerName`); the Zig config threads it through, every other
  config ignores it. Regression tests in `zig-extractors.test.ts` (unit)
  and `resolvers/zig.test.ts` (new `Ledger.zig` in the `zig-receivers`
  fixture: ids, `isStatic`, and the three CALLS edges); both fail on the
  previous source.
- `LINKABLE_LABELS` comment: the remaining `CLASS_KINDS` entries include
  `Namespace`.

Not re-fixed:
- "Ownerless-method assertion regex cannot match `.zig` graph IDs": the
  `[^:]+` segment consumes the whole file path (dots included) up to the
  second colon, and `[^.]+#\d+$` then matches only an owner-less name —
  `Method:src/Sorter.zig:lessThan#3` → true, `…:Sorter.sortBoth$1.lessThan#3`
  → false, checked with node.
- "Public namespace imports are never marked as re-exports": the shared
  `ParsedImport` namespace variant has no `reexportsName` field and
  `contributesReexportEdge` excludes namespace drafts on `base.kind` by
  contract; a `pub const X = @import("x.zig")` hub member is exposed
  through `findExportedDefIncludingImportedNames` instead, which is what
  the audit commit added for exactly that shape.
- "Private Zig namespace imports are treated as public hub exports": a
  private import cannot be named through the hub in code that compiles,
  and `findExportedDef` applies the same no-visibility rule to local
  defs; the finalized binding channel carries no `pub` bit to check.
- "Range binding mutates finalized scopes" and "unconditionally adds an
  optional Zig grammar to the fixture suite": refuted in the tenth and
  eleventh pass notes of the PR body, unchanged since.

* fix(zig): close the adversarial review's ten findings (8.2–8.12)

PR #1432 review 5095267917 on 34c53473 retained eight P1 and two P2
findings; each is reproduced on the new `zig-chains` / `zig-buildmodules`
fixtures with the decoy that made the old answer wrong, and pinned by a
test named after its number.

- 8.2 per-build-module import tables (`parseZigBuildModules`): a source
  resolves a bare name through its own module's `addImport` table (root
  file, else deepest root directory), fails closed when same-directory
  modules disagree, and follows `addImport("api", dep.module("core"))`
  through the dep's `addModule`; repo-wide names and zon deps remain the
  fallback.
- 8.3 module-level value receivers (`zigHostValueNames`) prepend the
  implicit `self` like fn-locals, so `global_runner.run(cb)` joins `cb@1`.
- 8.4 deep member aliases (`@import("lib.zig").B.work`, `lib.B.work`)
  keep the written owner: the module is bound as a namespace and the
  alias's use sites are rewritten to `receiver . member`; only one-level
  aliases are promoted to named imports.
- 8.5 container-hosted containers get owner-qualified identities
  (`A.Item`, `B.Item`, `Outer.Inner`), minted by the bare-container rule,
  while the scope keeps the lexical binding.
- 8.6 result-location `.init(…)` / `.{…}` under an annotation, a return
  type or a field type emit the call / construction site with the
  expected type as receiver.
- 8.7 Zig arm in bench/import-target (five dispatchers, config-free
  fingerprint) + baselines row; `--check` passes.
- 8.9 fn-local `@import` bindings and their uses are keyed per callable
  (`m$f_sib_a`), so sibling fns no longer share one namespace bucket.
- 8.10 `ScopeResolver.resolveNamespaceChains` (opt-in, Zig only): Case 1
  / Case 2 / Case 3 and the compound resolver walk a qualified receiver
  segment by segment — republished modules, nested types, enum variants
  through the module — refusing ambiguous hops. Off, every lookup keeps
  its one-hop split; the 70 resolver suites are unchanged.
- 8.11 `@import("a.zig").Thing{}` binds the module as a namespace in type
  position; `List(u8){}` / `lists.List(u8){}` get constructor sites.
- 8.12 a fieldless file whose top-level fn takes the file's own type
  (`self: *@This()`, `self: *Self`) is a file-struct; two over-matching
  ZIG_QUERIES rules are filtered by `shouldSkipDefinitionCapture`.

Also asserts the committed `opmod.Op.lookup.event_max()` call in zig-hub.

* fix(zig): address gitnexus-check findings on 215f70e3

- receiver-bound Case 3 wraps its reason in constructionSiteReason, like
  Case 1 and the nested-type route of Case 2 (one vocabulary per provider)
- resolveZigImportInternal rejects drive-qualified absolute imports
  (C:\foo.zig), the same test normalizeZigDepPath applies; unit case added
- the compound resolver's chain seed also tries the whole receiver as the
  qualified class (opmod.Op), as a bare class-name head already does
- markConstructionSites contract text names the receiver-bound routes

The return_type field claim is refuted: tree-sitter-zig exposes a fn's
return type as the type field (checked on the grammar).

* fix(zig): a build-module alias bound to an unindexed root fails closed

resolveThroughBuildModules returned undefined when the containing module
bound the alias to a file that is not indexed, which let the repo-wide
addModule map answer under the same name (gitnexus-check on 5299c552).
The module's table is the authority for its aliases: bound-but-unindexed
is null, only an unbound name falls through. Unit case with a same-named
repo-wide decoy, plus the outside-module file that still reaches it.

* fix(zig): an unindexed root module fails closed, never a same-named zon dep

The root build.zig's addModule declaration is authoritative for a bare
name when it binds it; a root that is not indexed used to fall through
to a build.zig.zon path dep of the same name — a different declaration
answering under the name (gitnexus-check on fe24b37f). Same rule as the
build-module tables. Unit case added.

* Address PR review feedback (#1432)

Tighten Zig build-module parsing, receiver/merge helpers, and container queries that gitnexus-check flagged on the open threads.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Address PR review feedback (#1432)

Attach the paren-matcher doc comment to findZigParenEnd instead of zigTopLevelStaticRoot.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Simplify Zig review-feedback helpers after #1432.

Reuse ZON brace/string walkers for top-level root_source_file, drop the dead bind flag and one-off staticRoot wrapper, and merge bindings via a first-wins map.

Co-authored-by: Cursor <cursoragent@cursor.com>

* bench(receiver-resolution): rebaseline for the Zig lang-resolution fixtures

The receiver-resolution gate (#2856/#2899) landed on main after this branch
forked and counts call drops over test/fixtures/lang-resolution, which this
branch extends with the zig-* fixture projects. Regenerated with
`measure.mjs --update-baseline`: callDrops 102 -> 113, all 11 new drops in
.zig files, shape `no-chain`.

Every new drop is a call whose callee has no node in the corpus, not a
resolver regression: `std.Build.Module.addImport` in the three build.zig
fixtures (7, classified in-program), `std.sort.pdq` in Sorter.zig (2,
unknown), `std.Thread.Mutex{}` / `std.mem.Allocator{}` literals in
zig-qualified-literal (2, unknown, the fixture asserts std stays external),
and one `.init()` decl literal on a generic instantiation
(`const u: Stack(u16) = .init()`, in-program). Shape arm unchanged.

---------

Co-authored-by: Garrett Griffin-Morales <grgisme@gmail.com>
Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-03 13:29:42 +01:00
glier
b15ff2d888
feat(ingestion): mint Destination nodes from AsyncAPI 3.x documents (#3140)
* feat(ingestion): read AsyncAPI 3.x documents into broker addresses

Adds a format-driven reader that turns the `operations[]` entries of an
AsyncAPI 3.x document into (broker, address, direction) triples, plus the
protocol-to-broker map behind it. Nothing consumes it yet.

The reader lives outside `frameworks/spring/` on purpose, like
`destination-key.ts` and for the same reason: an AsyncAPI document is a
published artifact emitted by generators across several language toolchains
and written by hand as often as generated. The entry criterion is therefore
the document format -- a root `asyncapi` key -- and never the generator.

AsyncAPI 2.x is refused under its own countable reason rather than mapped.
Its `publish`/`subscribe` are inverted relative to 3.x `send`/`receive`, so
a naive mapping reverses every direction in the async graph while leaving it
connected: nothing fails, the arrows simply point the wrong way. A silent
skip would be indistinguishable from "this service publishes no document",
which is the one thing the refusal count has to be able to tell us.

The broker is read twice over -- from the operation's bindings and from its
channel's server protocol -- and the two readings must agree. A destination
keyed on the wrong broker joins a stranger, and with the document
contradicting itself there is no way to tell which reading is right, so the
operation is refused rather than decided by a coin flip.

An unmapped protocol passes through as its own literal instead of being
dropped, because `destinationNodeKey` takes a plain string precisely so a
non-Spring caller can attest to a broker Spring has no member for.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* feat(cli): add --asyncapi-spec, an explicit path to AsyncAPI documents

Threads an `asyncApiSpecPath` option from the CLI, the server analyze
endpoint, and the programmatic entry through to `PipelineOptions`. Nothing
reads it yet; the reader added in the previous commit is still unwired.

Shaped deliberately after `springActuatorPath`, the existing option for an
out-of-band artifact: an explicit local path, accepting a directory or a
single file, resolved against the repository root so a committed
`docs/asyncapi` and an absolute cache populated by something else are both
natural, and `undefined` keeping the feature entirely off. Mirroring that
option rather than inventing a mechanism is what lets a downstream consumer
point the reader at documents fetched out of band without patching a file
here.

`analyze --watch` REJECTS the flag, exactly as it rejects --spring-actuator.
The watcher reacts to source changes and nothing watches a document
directory, so honouring it there would read the documents once and then
serve a stale answer for the rest of the session -- worse than refusing,
because it looks like it worked.

Additive only: 49 inserted lines, no deletions and no modified lines. Every
new interface member is optional and every forward is an object-literal
spread of an undefined value, so with the option unset the analyzer takes
byte-identical paths.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* feat(ingestion): mint Destination nodes from AsyncAPI documents

Wires the reader into the destinations phase. With `--asyncapi-spec` set,
every `send` operation emits PUBLISHES_TO and every `receive` emits
CONSUMES_FROM against the ordinary resolved `Destination` node -- same key,
same `address` property -- so a document and a source site that name one
address on one broker land on ONE node and the two halves of a conversation
meet. Verified end to end: an address named only in a document is minted
with the right broker and direction, and an address a source site already
resolved stays a single node with its own `literal` provenance while the
same address on a different broker stays separate.

This claims only what a document states -- that the service talks to that
address, on that broker, in that direction -- and never which method does
it. The addresses in one document partition by (broker, action) into buckets
that usually hold more than one operation, so any assignment past a bucket
of size one is a heuristic, and a wrong one attaches a real address to the
wrong handler: a false connection wearing the clothes of a resolved one. The
edge therefore starts at the document, not at a callable. That is weaker
than a source-derived edge and worth having anyway, because it is available
where the source supplies nothing at all -- a programmatically registered
listener, a broker with no patterns here, a language whose messaging idiom
nobody has taught this codebase yet.

Documents are read even when the source pass found no messaging, which is
why the early return had to move: a repository whose brokers are invisible
to the patterns is precisely the case a published document covers, and an
early return keyed on source sites skipped the documents exactly there.

Their counters are kept in their own block rather than folded into the
existing ones. `refusalsByReason` is the denominator of the SOURCE
unresolved fraction, and a mistyped specification directory must not be able
to make the source look worse than it is. The block is absent -- not zeroed
-- when no path was configured, so "not asked for" stays distinguishable
from "asked for and found nothing"; those need different answers from an
operator and one zero cannot say which happened.

The direction assertion is the one that matters and it is pinned by type,
not by existence: inverting the mapping in the source tree fails exactly one
test, because every other assertion passes identically under both readings.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs: document --asyncapi-spec and why step 4 of the cascade stays empty

Adds the flag to both READMEs and to the three byte-identical copies of the
CLI skill, which a sync test pins together.

Also rewrites the note on the `specification` seam in the address cascade.
It said "nothing supplies it today", which was true and is now misleading:
a reader exists, and the hook is still unsupplied because of a decision
rather than for want of one.

A document names addresses; it does not name the method that uses one. To
hand an address to a particular candidate something must choose which of the
document's operations belongs to it, and the only division both sides agree
on -- (broker, action) -- leaves buckets that usually hold more than one
operation. On a real generated document exactly one bucket of four was
unambiguous. Every assignment past a bucket of size one is a heuristic, and
a wrong one puts a REAL address on a joining node under the wrong site: a
false connection wearing the clothes of a resolved one, which is the outcome
the keying rule exists to prevent.

The note also records the two things that would change that and are not
heuristics -- a document carrying the implementing symbol, or a
configuration source answering the `${key}` the candidate already recorded
-- and that the second wants its own resolver, since what it needs is the
placeholder key rather than the candidate.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(ingestion): refuse the document shapes that would forge a join

Four ways a conformant AsyncAPI document could mint a Destination that
connects two services which have said nothing about each other. Every one is
reachable from ordinary 3.x vocabulary, not from malformed input, and each is
now a countable refusal.

A PARAMETERIZED ADDRESS is a pattern, not a place. Two services that publish
`{env}.orders` share a template; one deploys with env=prod and the other with
env=staging, and keying on the template text merges them into a single node
with a publisher on one side and a subscriber on the other. This is the
document-side twin of `overridable-config-default`, which argues the same
thing about `${key:default}` in source. A channel declaring `parameters` is
the specification's own statement that its address is a template, so the
detector is a reading rather than a guess; the `{` test catches generators
that template without declaring.

ANY BINDINGS KEY WAS TAKEN AS A PROTOCOL. AsyncAPI allows `bindings` to be a
Reference Object, so the map's own key can be `$ref` -- and passed through,
that becomes half of a join key carrying no broker information at all. Two
services that both reference shared bindings and both name `orders` then land
on one node, defeating the broker-in-key rule that keeps `kafka orders` and
`rabbit orders` apart. A broker must now be spelled like a protocol name.

A BROKER CONTAINING A SPACE COLLIDES, because the node key joins with one:
("kafka orders", "x") and ("kafka", "orders x") are the same key. That was
latent while every broker came from Spring's closed union. This module is the
first caller to feed the shared helper text that a document wrote, which is
exactly the condition under which it stops being latent, so it is closed here
-- at the producer -- rather than by changing an encoding that `routeNodeKey`
shares.

THE ADDRESS WAS TRIMMED, while the source cascade keeps an address exactly as
written so `" orders "` stays its own node. Two producers of one key held
opposite whitespace policies and the document side erred toward joining.

Also: fold the transport-security protocol variants (`kafka-secure`,
`secure-mqtt`, `wss`, `stomps`, `https`) onto their base protocol. The
`amqp`->`rabbit` argument already in this file demands it -- AsyncAPI's server
vocabulary distinguishes them and its bindings vocabulary does not, so a
secured cluster's own document was being read as self-contradictory. Treat a
channel with no `servers` as available on all of the document's servers, which
is the specification's default and was costing every single-server document
its destinations. Bound the address and operation-id lengths, because
`generateId` concatenates rather than hashes, and bound total operations
across the run rather than only per document.

Read each file through ONE handle for both the size gate and the read, as
`actuator-runtime.ts` does and for the reason its comment gives (CodeQL
js/file-system-race): re-resolving the path lets a swapped file bypass the
cap, and the out-of-band cache this option reads is written by other tooling
by definition. Open it with O_NONBLOCK: the type check that rejects a FIFO is
unreachable without it, because opening a FIFO for reading blocks in open(2)
until a writer appears -- found by writing the test first and watching it time
out rather than fail.

Count symlinked entries and walk truncation instead of dropping them in
silence. A symlinked cache and a wrong path were producing identical results.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(analyze): rebuild when documents are configured, and report what was read

An AsyncAPI document is external to git freshness in exactly the way an
Actuator snapshot is: replacing one moves no commit and dirties no file. The
option's own README paragraph advertises an absolute cache written by other
tooling, and on the second run of that workflow the already-up-to-date fast
path fired, no document was ever opened, and the previous run's addresses were
served as current. Measured, not reasoned: editing a document and re-running
printed "Already up to date" and left the old address in the graph; with this
change the same probe re-reads and the new address replaces it.

So an enabled run forces a rebuild and dropping the option forces one more, to
clear document-derived evidence -- the treatment `springActuatorPath` already
gets, for the same reason. Only the FLAG is recorded in index metadata, not
the path: Actuator retains its inputs so future scans keep excluding them,
whereas a committed document is deliberately NOT excluded (it wants its real
`File` node), so there is nothing to retain and recording the path would put
an operator's directory layout into metadata for no consumer.

That also settles a defect it would have been tempting to patch separately. A
synthetic `File` node for an out-of-tree document carries a path that is in no
write set and is not covered by `isGraphWideNode`, so an incremental writeback
dropped the node while keeping its edges -- which then COPY against a row that
was never written, and fail into an IGNORE_ERRORS retry that reports success.
A forced rebuild has no incremental subgraph to get that wrong.

Distinguish the two meanings of `resolution: 'specification'`. That value
belongs to the address cascade and means a CODE candidate was resolved through
the step-4 hook; a node minted from a document has no code site and now says
`asyncapi-document`. Reusing one string would leave a query that groups by
provenance unable to separate an address a document states from one a document
was used to resolve, and only the second is a claim about source.

Report what was read. The stats block was justified on the grounds that an
operator must be able to tell a mistyped directory from a repository with no
documents -- and nothing surfaced it, so the justification was aspirational. A
configured path that yields nothing, or a walk that hit a bound, now warns
unconditionally, as `spring-auto-configuration.ts` does for the same class of
input. The phase summary carries the refusal breakdown rather than only the
totals, because the unresolved fraction is the number this work is judged on
and a bare count says how big the gap is without saying what would close it.

Tests for the three wiring lines that were individually deletable with a green
suite, following the templates already in the repository: a row in the
`--watch` rejection table, the CLI-threading assertion beside the Actuator
one, and the shipped-skill fragment that pins the flag in all three copies.
Also pin `filePath: ''` on a spec-minted destination -- the half of the keying
rule that stops a shared node becoming collateral damage of one document's
next change -- and the in-repo `File` branch, which was dead-code-able.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(ingestion): close the join-forging paths a second review round found

The `$ref` exclusion added last round fixed one instance of a class and left
the class open. A `bindings` map key of `x-scs-function` -- an ordinary
Specification Extension, which generators emit -- still became the broker, so
two unrelated services carrying one vendor annotation and one address landed
on ONE node whose broker half said nothing about any broker. And
`{ kafka: {}, x-internal: {} }` read as two brokers, losing a conformant
document and reporting it as self-contradictory, which also made any document
author a one-line saboteur of their own cross-service links.

So the two readers are now separate functions with opposite defaults, and the
header says why they must be. `servers[].protocol` is a FIELD DECLARED to hold
a protocol: an unrecognized value there is the document's own claim and passes
through, because refusing it would lose a destination the document states
plainly. A `bindings` MAP KEY is not that -- the specification puts `$ref` and
`x-` in the same namespace -- so a non-protocol key is the EXPECTED case and
only AsyncAPI's binding vocabulary may answer. The syntactic test that was
applied to both was the right rule for one of them.

The walk fix from last round introduced something worse than it reported. A
shared abort flag meant depth exhaustion in ONE branch terminated the whole
traversal, so ten good documents beside a twelve-deep unrelated subtree were
kept or lost depending on whether that subtree sorted before or after them.
Truncation and budget-exhaustion are now separate: depth returns from its own
branch, and only a genuinely global bound stops the walk.

Rewriting `protocol.ts` dropped the whitespace check from the server-protocol
path and made the node-key collision reachable again. The test written for
that collision last round caught it within the minute; the comment now records
that it was learned twice.

Everything else measured this round:

- The broker is the THIRD string that reaches a graph identifier, and it was
  unbounded while the header claimed there were two. A one-megabyte protocol in
  a document satisfying every other cap was measured producing a gigabyte of
  resident identifier strings, because `generateId` concatenates rather than
  hashes and the phase mints one id per node and per edge.
- The run-wide operation budget counted ACCEPTED operations, reproducing at the
  run level the exact defect the per-document cap was corrected for last round:
  a run whose every operation is refused never decrements it. Both now count
  operations EXAMINED, and `operation-cap` sets `truncated` -- it is a bound
  that stopped the operation count, which is what that flag is documented to
  mean.
- The channel-inherits-all-servers rule ran per operation. Hoisted: it depends
  only on the servers.
- A subdirectory that cannot be listed is counted rather than dropped, so a
  mixed-permission cache cannot report a clean, complete read.
- The read LOOPS, like the Actuator reader this claims to follow. A single read
  was never short across seven hundred probes on APFS, but POSIX permits it and
  FUSE mounts with `direct_io` -- the deployment this option targets -- return
  short counts. A document truncated at a line boundary still parses, so the
  failure is silent: operations vanish with `refusals: {}`.
- `parameters: {}` no longer refuses a literal address; an empty container
  states nothing and generators emit them.
- A channel that is itself a Reference Object gets its own reason instead of
  `no-address`, which was telling operators their documents omit addresses when
  the reader simply stops one hop short.
- A multi-protocol document resolves from its operation's own bindings; only
  when those are silent does an inherited multi-protocol server set refuse, and
  under `ambiguous-server-default` rather than a reason that says the document
  contradicts itself. It does not.
- HTTP and WebSocket are refused for destination minting. For a broker the
  topic is the namespace; for HTTP the host is, so keying on the path alone
  would make every service exposing `/events` one node. A `Route` already
  models an HTTP endpoint, with its method in the key.
- The sniff window is a parse gate, not a read gate, and four kilobytes refused
  a good document behind a licence header.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* test(ingestion): pin the wiring and the reporting that were deletable

Three lines could be deleted with the whole suite green, and each of them
makes the feature partly or wholly inert: the forward from `run-analyze` into
`PipelineOptions`, the forced rebuild while documents are configured, and the
cleanup rebuild when the option is dropped. None is visible one layer up,
where the CLI test asserts on a mock's arguments.

One integration test closes all three. It drives the real `runFullAnalysis`
against a real repository and asserts, in order: the enabled run does not take
the up-to-date fast path and logs the rebuild; the destination reaches the
graph, which only happens if the option is forwarded; a document edited with
the tree clean and the commit unchanged is re-read; dropping the option
rebuilds once and removes the document-derived evidence; and the run after
that is up to date again -- the "rebuilds once" half, which rests on the
metadata being written as a fresh literal rather than merged, and which
nothing pinned.

The document lives OUTSIDE the repository on purpose. That is the workflow the
option is documented for, and it is the only one where the hazard exists:
editing a tracked file dirties the tree and forces a rebuild anyway, so an
in-repo fixture would pass with the freshness fix reverted. Verified by
reverting both: deleting the forward fails on the empty destination list,
deleting the forced rebuild fails on the missing log line.

Also pinned, each because deleting the code it covers left the suite green:
the `parameters` half of the templated-address refusal (its old test supplied
a braced address too, so the `{` half alone satisfied it); the phase actually
forwarding `symlinksSkipped`; the unconditional warning, whose whole argument
is that a tally nobody can see is not a tally -- captured through the
repository's own `_captureLogger`; a `.yml` document; a character device,
which is the case the `isFile` check exists for and which the FIFO test does
not reach; and the bound that stops a walk.

Reject an empty `--asyncapi-spec` at the CLI. It resolved to the repository
root and walked the whole tree, defeating this module's own rule that there is
no glob-based auto-discovery -- and the HTTP entry point already rejected the
identical value. Two doors onto one option must not hold different rules.

Surface the flag in the MCP context resource beside `spring_actuator`. It
matters more there than for its neighbour: Actuator annotates nodes the source
pass already found, while document reading mints destinations and edges with
no code site, and nothing said where they came from.

Log the configured path relative to the repository. The same change refuses to
persist that path to index metadata because it would record an operator's
directory layout; holding that rule for metadata and not for logs was holding
it in one place.

Both test files now clean up their temporary directories.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* style(ingestion): apply the repository's prettier contract

`quality / format` runs `npx prettier --check .`, and three files added by this
branch were not formatted to it. No behaviour changes: the reader's line
breaks and two test literals move, nothing else.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* test(ingestion): release the mini-repo handle the document test allocated

`setupMiniRepo` documents that the caller owns cleanup, and every other test
in this file calls `repo.cleanup()` in its `finally`. The AsyncAPI document
test removed only the document directory, so each run left a temporary
repository behind.

The two owners are separate on purpose: the document directory is a SIBLING of
the repository, placed outside the working tree so that editing it cannot
dirty the tree and force a rebuild on its own. The repo's cleanup therefore
does not reach it, and both calls belong in the same block.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(ingestion): stop partial server evidence from reading as unanimous

Six review findings, every one a way this reader could name a broker the
document does not name. They share a shape: something is DROPPED rather than
refused, the remaining evidence agrees with itself, and an operation is
attributed with confidence to a broker its document never settled on. A wrong
broker is half a join key, so it does not produce a missing edge -- it produces
an edge to a stranger, reported as a fact.

CAPPED SERVER MAPS. A channel with no `servers` inherits all of them, and that
map is capped at 1,000. A document whose first thousand servers are Kafka and
whose thousand-and-first is JMS read as unanimously Kafka, because unanimity
was tested on the slice. Counted `server-cap` and set `truncated`, but neither
stopped the attribution. The inherited path now refuses under
`capped-server-default` -- checked BEFORE agreement, since a subset agrees with
itself for free.

ROOT SERVER REFERENCE OBJECTS. The Servers Object patterned field is
`Server Object | Reference Object`, so `{ $ref: '#/components/servers/prod' }`
is conformant. Reading `protocol` off the raw value dropped every one: an
all-reference document had no protocol at all, and -- worse -- a MIXED set lost
its disagreeing half and became unanimous. One hop is now followed, through
`#/servers` and `#/components/servers`; anything else is refused under
`unresolved-server-reference` rather than skipped.

CHANNEL BINDINGS. Only the operation's bindings were read. A conformant channel
carrying `bindings: { kafka: {} }` with no operation binding was dropped as
`protocol-unknown` while the document said plainly which broker it meant, and a
disagreement between the two levels was invisible. Both are read; a conflict is
`protocol-disagreement`.

EMPTY `servers`. "If `servers` is absent or empty, this channel MUST be
available on all the servers defined in the Servers Object" -- one sentence,
both cases. A zero-iteration loop returned `explicit: true`, which blocked the
inherited fallback and dropped valid operations.

POINTER DECODING ORDER. RFC 6901 percent-decodes the fragment BEFORE splitting
on `/`. The raw token was tested for a separator first, so `#/channels/orders%2Fv1`
passed a check it should have failed and then decoded into two segments -- a
pointer addressing `channels.orders.v1` was read as a channel named `orders/v1`,
inventing a channel the document never declared. A malformed escape is now
refused rather than resolved against its undecoded text. `~1` still resolves; it
is the pointer's own escape and belongs after segmentation.

THE SNIFF WINDOW. A fixed window decides by where the root key sits rather than
whether it is there, so every window is a false negative waiting for a longer
preamble -- 4 KiB was replaced by 64 KiB for that reason and inherited the same
defect. The whole text is scanned; it is already bounded and already in memory,
and the gate exists to skip the PARSE, which is the expensive half. A leading
UTF-8 BOM is stripped before both sniff and parse.

Twelve of the fourteen new tests were run against the unfixed reader and all
twelve failed; the other two are controls that must pass either way.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* refactor(ingestion): simplify AsyncAPI pointer and binding resolution

Decode each $ref once, union binding evidence, and stop walking capped
server maps whose brokers are unused on the inherit path.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-02 22:01:15 +00:00
Yayler
3aa62be717
feat: add gitnexus auto-sync for scheduled remote clone and analyze (#2493)
* adds an opt-in auto sync and analysis loop for GitNexus

* adds an opt-in auto sync and analysis loop for GitNexus,gitnexus watch [init|start|restart|stop|status]

* adds an opt-in auto sync and analysis loop for GitNexus,gitnexus watch [init|start|restart|stop|status]

* fix: address PR review cleanup

* Prettier code style

* merge main

* fix(watch): protect local repos and cancel active analysis

* fix(watch): harden auto-sync lifecycle and locking

- validate watch process identity before lifecycle operations\n- serialize registry, analysis, and LadybugDB access with recoverable locks\n- harden clone paths, symlinks, hooks, quarantine, and worker timeouts\n- install procps in the CLI image for reliable Docker watch control\n- add focused regression coverage for lifecycle, locks, clone, and registry behavior

* update agents & claude md

* merge main

* fix(watch): harden auto-sync lifecycle

* fix(watch): normalize SSH repo identity paths

* fix(watch): normalize SSH repo identity paths

* fix(watch): safely cancel analysis across platforms

* fix(auto-sync): close worker and group sync failure paths

* fix(auto-sync): drop retired allowStale from group sync

allowStale was removed from SyncOptions, which broke typecheck and CI on this PR.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(watch): satisfy prefer-const and Prettier in auto-sync

The watch timers are assigned exactly once, so prefer-const rejected the
deferred `let` declarations. They are only read from `stop()` and the control
poll, both of which run after the assignments, so binding them at creation is
safe and drops the now-dead undefined guards.

Remaining files are formatting only.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(watch): make lock identity absolute and stop three fail-open paths

Lock owner identity was rendered by `ps -o lstart=` through localtime and the
active locale, so the same live process produced a different string under a
different TZ. A mismatch reads as PID reuse, so one daemon could reclaim a
mutex another still held. Pin TZ=UTC and LC_ALL=C.

The owner record also carried no hostname, so a holder on another machine was
judged by this kernel's view of its PID — always "stale" — and its lock stolen
whenever GITNEXUS_HOME is a shared volume. Record and compare the hostname, as
the index lock already does.

Ownership verification threw unconditionally on win32, which is reached once
per project per tick, so watch reported `running` and then failed every repo
forever. POSIX uid/mode cannot be checked there; skip those two assertions and
keep the dangerous-root, symlink, containment and internal-root guards.

Also: quarantine sweep now refuses a symlinked root instead of deleting
through it; an unreadable state file propagates instead of being rewritten as
empty state, which used to erase every repo's analyzed commit and failure
count; a failed staging cleanup no longer strands a published lock with no
release handle; and the concurrency runner settles every worker before
surfacing a failure so cancellation cannot orphan a live analyze fork.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(watch): land the deferred review findings

Six findings that were deferred from the review backlog, plus the docs they
change.

Worker heap: admission allowed `floor(availableMemoryGB / 2)` slots while every
fork was handed the whole machine's heap cap, so the budget meant nothing as
soon as an operator raised max_concurrency. Divide the cap by the repos
actually analyzed in parallel. The default single-project path is unchanged.

Registration: the parent registered without a branch, so it always took the
primary/flat arm and relabelled a pinned branch entry on the branch-fallback
path. Reproduce the worker's own resolveBranchPlacement decision instead.

Cancellation: requestCancellation cleared the only timer and settled nothing,
so a worker wedged past its safe point left the promise pending forever,
wedging activeRun and hanging `watch stop`. Add a 5s grace after which the
parent stops waiting and releases the IPC channel's hold on its event loop.
The child is still never killed — it may be inside native work.

overwrite_local_changes: `checkout --force` rewrites tracked files only, so
untracked sources survived and were indexed as if they came from the remote.
`git clean -fd -e /.gitnexus` after checkout; no -x/-X, so ignored paths and
GitNexus's own storage survive.

Quarantine: age alone never bounds a repo that fails every tick, since each
partial clone is younger than the retention window. Keep the five newest per
repo.

Validation: repo_git_timeout is now bounded by the lesser of an hour and the
sync interval, which is also the guard for the bare-number-means-seconds slip
(`600000` meant ~7 days and cleared the timer ceiling). And the remote URL's
final segment is validated at config load rather than failing once per tick
inside the sync loop.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(watch): release an errored worker, and stop rejecting dotted repo names

Three findings from the latest review pass.

The 'error' handler settles immediately rather than waiting out the grace, so
cleanup() clears the grace timer that would otherwise have released the child.
An errored IPC channel does not mean the worker stopped, so release it on that
path too — still no kill.

The traversal guard tested the raw path for '..', which also rejected an
ordinary name like owner/foo..bar that the repository-name rule accepts.
Traversal is a whole segment, so test segments.

The heap-cap test left two runs and their real timers pending; it now stubs
timers and settles both promises. Registration coverage now pins the branch
slot rather than leaving it implicit.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(watch): validate namespace segments and pin the stopped process identity

Replacing the raw-string `..` test with a per-segment one dropped a guard: a
segment like `..\..\outside` is not literally `..`, so it passed, and those
segments build the clone path — on Windows the backslashes are separators.
Hold every namespace segment to the same charset as the repo name, which keeps
a separator out of a segment while still allowing an ordinary `foo..bar`. The
final segment keeps its own check so a bad repo name keeps its own message.

The stop wait polled liveness by pid alone, so a pid reused mid-wait would
have it wait on an unrelated process and then report the watch stopped. Compare
the process start time recorded for the owner, which also returns sooner.

Registration now omits `branch` for a primary index instead of passing it as
undefined, so that call keeps the shape it had before this branch.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cli): ship auto-sync as the remote daemon, reserve gitnexus watch.

Keep analyze --watch for local incremental re-index and stop the top-level watch verb from starting a clone/pull loop.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(auto-sync): reject invalid branch refs and verify status identity (#2493)

Reject leading slashes and per-component trailing dots in configured branches, and verify the live watch owner before trusting a stored error status.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(auto-sync): reject ownerIds that can escape the watch directory (#2493)

Stop interpolating a tampered ownerId into the stop-request filename; only basename-safe values are treated as owners.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(auto-sync): recognize auto-sync in the watch-process identity check (#2493)

Stop/status were still looking for a standalone watch token after the command rename, so a live gitnexus auto-sync start process would be refused as unrelated.

Co-authored-by: Cursor <cursoragent@cursor.com>

* chore(autofix): apply prettier + eslint fixes via /autofix command

* fix(auto-sync): reject boolean max_concurrency instead of coercing it to 1 (#2493)

Number(true) is 1, so a YAML boolean would have passed the integer check and silently meant one worker.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(auto-sync): swallow status errors in the watch finally path (#2493)

An uncaught updateStatus rejection in finally became an unhandled
rejection. Skip the clone-root symlink test on Windows, where directory
symlinks need privileges. Align the group-lock comment with fail-closed
registry timeouts.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(auto-sync): catch cancelling status-write failures (#2493)

Fire-and-forget updateStatus('cancelling') could become an unhandled
rejection, the same class as the finally-path status write.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(auto-sync): ignore queued interval ticks after stop (#2493)

clearInterval does not cancel a timer callback already queued. Guard
runSafely on stopping so shutdown cannot start a new un-cancellable run.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(auto-sync): report stored watch status timestamps (#2493)

status should show when the watch last entered a state, not when the
CLI queried it. The failure-count test still expects 1 after a new
commit resets the streak; rename it so that reset is explicit.

Co-authored-by: Cursor <cursoragent@cursor.com>

* style(auto-sync): apply prettier to starter status logger (#2493)

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: weiyf <weiyf3634@163.com>
Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-09-02 19:40:18 +01:00
Gergő Magyar
52924ef12c
perf(emit): drop six equivalent redundancies on Java-scale index path (#3129)
* perf(scope): reuse deferred sites and signatures for callable-value-flow

Pass the Phase 4 deferred-site collection and signature map into emitCallableValueFlow so the emit path does not rescan the same call sites.

Co-authored-by: Cursor <cursoragent@cursor.com>

* perf(ingestion): nest owner registries instead of composite string keys

Look up methods, fields, and nested types via Map<owner, Map<name, defs>> while keeping EMPTY identity and TypeRegistry miss [] semantics.

Co-authored-by: Cursor <cursoragent@cursor.com>

* perf(scope): memoize resolveDefGraphId per nodeLookup

Cache graph ids on a WeakMap keyed by lookup identity so heritage rebuild invalidates, with an env opt-out that skips the cache.

Co-authored-by: Cursor <cursoragent@cursor.com>

* perf(csv): prepare file content once and LRU-touch via Map order

Cache split lines and binary flags per source file so snippet and FTS extraction do not re-scan the same bytes.

Co-authored-by: Cursor <cursoragent@cursor.com>

* perf(identity): allow in-process cache guards only when opted in or unwritable

Keep the ≥128-guard subprocess default on writable installs; use direct snapshots when the env flag is set or W_OK fails on the analyzer tree.

Co-authored-by: Cursor <cursoragent@cursor.com>

* refactor: share empty CSV payload and restore spacing after memo key (U4, U3)

Co-authored-by: Cursor <cursoragent@cursor.com>

* test(scope): assert resolveDefGraphId memo skips a second lookup walk

The ID-equality checks still passed with the memo disabled. Count Map#get on the lookup so a repeat call must hit the WeakMap cache. Also apply prettier on the remaining emit-path files CI flagged.

Co-authored-by: Cursor <cursoragent@cursor.com>

* test: pin identity W_OK probes and memo env isolation

Co-authored-by: Cursor <cursoragent@cursor.com>

* docs: document emit-path env knobs and fix registry headers

CONTRIBUTING requires README rows for new GITNEXUS_* variables; also drop stale owner\0name comments after the nested-map change.

---------

Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-01 11:22:55 +00:00
MyShining
c217a0f257
feat(spring): import optional Actuator runtime data with Kotlin JVM mapping (#3107) 2026-09-01 06:22:38 +01:00
FenjuFu
dc5c816a02
fix(serve): protect MCP route with optional bearer auth (#3100)
* fix(serve): protect MCP route with optional bearer auth

Signed-off-by: FenjuFu <92919259+FenjuFu@users.noreply.github.com>

* fix(serve): clarify MCP auth proxy boundaries

Document the Render token incompatibility, expose serve auth in CLI help, and replace source-order assertions with live middleware coverage.

Note: full test suite has pre-existing worktree failures because generated parse-worker.js is absent; targeted auth and proxy suites pass.
Co-authored-by: Cursor <cursoragent@cursor.com>

* chore(docs): preserve existing table formatting

Keep the auth clarifications focused without reformatting unrelated Markdown tables.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(proxy): inject backend MCP credentials

Replace the consumed edge credential with the configured protocol token only for MCP routes so proxied serve authentication remains composable.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Signed-off-by: FenjuFu <92919259+FenjuFu@users.noreply.github.com>
Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-30 17:12:46 +01:00
Gergő Magyar
9718e1247a
fix(parse): stabilize parse-cache chunks and cheapen ParsedFile loads (#3093)
* fix(parse): stabilize parse-cache chunks and cheapen ParsedFile loads

Hash-bucket membership so worker count and add/delete no longer reshuffle cache keys; GC and path sidecars keep small-shard scope-resolution from full-store JSON and empty GCs.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(review): apply review findings

Drop the unused pool argument from cache-budget resolution, reuse path compare helpers, and copy durable sidecars via full shard paths.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(store): copy durable path sidecars via full shard paths

Keep restore destinations relative to the run store even when sidecar names are derived from absolute json paths.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(store): fail closed on truncated ParsedFile path sidecars

Skip JSON only when a sidecar is complete (NUL-free, trailing newline). Truncated listings without a NUL were able to omit wanted paths.

Co-authored-by: Cursor <cursoragent@cursor.com>

* style: apply prettier to ParsedFile store and tests

Match the PR autofix formatter so CI quality does not flag wrap-only diffs.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(store): tighten ParsedFile path sidecars from review

Skip sidecar writes when a path contains CR/LF, and assert the skip path does not open non-intersecting JSON shards.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(store): yield on sidecar skips and assert restore copies listing bytes

Skipped shards now count toward the 128-shard event-loop yield, and restore tests check sidecar contents rather than existence only.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(store): treat path sidecars as best-effort after a JSON shard write

A sidecar ENOSPC/EACCES must not fail persist; load already falls back to the JSON shard when the listing is missing.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(store): drop stale path sidecars when a shard is no longer listing-safe

Rewriting a shard with a newline-bearing path must unlink the old listing so load does not skip the JSON payload.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(parse): keep worker-integration tests aligned with hash buckets

Quarantine cache-skip asserts the poison pack hash, clone-skip keeps poison and survivors in one bucket, and restore unlinks a stale dest sidecar when the durable source has none.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(parse): address review follow-ups for cache packs and sidecars

Record SCHEMA_BUMP 80, pin pack locality and sidecar load/restore tests, and keep sidecar I/O best-effort with shared ENOENT handling.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(store): drop stale path sidecars after a failed listing write

A leftover .paths file after ENOSPC (or similar) made load skip the new JSON shard. Hash expected packs with the same env budget production uses.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(store): drop path sidecars before overwriting parsed-file JSON

Load trusts a leftover .paths listing, so rewriting a shard must unlink that listing first. Otherwise an interrupted sidecar refresh can hide newly written files.

Co-authored-by: Cursor <cursoragent@cursor.com>

* chore(autofix): apply prettier + eslint fixes via /autofix command

* fix(store): fail closed on truncated or CR path sidecars

Count-prefix listings so a newline-terminated partial sidecar cannot skip the JSON shard, and reject CR instead of stripping it.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(ci): expect single-file watch refresh telemetry

The production analyze --watch e2e was still pinned to the old pack-cascade
"8 re-parsed" line, so shard 1/3 timed out after a correct 1-file refresh.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(ci): expect one reparsed file on a non-bean incremental touch

Pack-cascade leftover: the drift-skip test still required 7 reparsed files
after logger.ts-only edits. Cheap ParsedFile loads now reparse just that file.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-08-30 08:31:47 +00:00
简律纯
5bad2d8b0b
fix(mcp): resolve omitted repo from cwd (#3085)
* fix(mcp): resolve omitted repo from cwd

* test(mcp): cover cwd repository routing gaps

* fix(mcp): harden cwd repository routing

* docs(mcp): clarify cwd repository boundary

* fix(mcp): preserve resolver compatibility

* fix(mcp): align restricted repository routing

---------

Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
2026-08-29 21:07:43 +00:00
azizur100389
bf7dcf98ca
feat(analyze): add incremental watch mode (#3072)
* feat(analyze): add incremental watch mode

* fix(watch): harden control file reads

* fix(watch): contain refresh errors and bound reads

* fix(watch): stream strict control file reads

* fix(watch): harden refresh recovery and lifecycle

* fix(watch): report ignored repository defaults

* fix(analyze): preserve signal exit semantics

* style(analyze): format signal exit helper

* test(config): exercise descriptor growth guard

* test(watch): await source event before rename

* fix(watch): keep live-index retries honest and ignore analyzer writes

Hold retry backoff when events merge, stop only after a live-index mutation, skip .gitnexus self-writes, and reject the remaining one-shot watch flags. Export impact-risk scoring from gitnexus-shared so consumers can share the same scale.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(watch): contain queue edge cases after review

Preserve overflow-only refreshes, contain synchronous refresh failures, and mark successful atomic publication before later operations can fail.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
2026-08-29 10:40:56 +00:00
John R. Eakin
38a0837e4b
feat(wiki): add grok local CLI provider (#3069)
* feat(wiki): add grok local CLI provider

Wiki generation can use `gitnexus wiki --provider grok` to spawn the
authenticated Grok Build CLI (`grok --prompt-file`) instead of an HTTP API key.

* style(wiki): prettier grok-client for CI format check

CI quality/format failed on grok-client.ts. Auto-format matches repo prettier so the GitNexus /autofix comment is applied locally.

* Update Grok CLI configuration to use empty allowlist and increase max tu

* Replace Grok tool allowlist with explicit denylist and strict sandbox

* Increase Grok max turns to 15 to accommodate prompt variance

* chore(wiki): drop Unreleased CHANGELOG hunk and restore lockfile libc selectors

Feature PRs do not own CHANGELOG.md. Restore the 16 libc platform selectors
deleted from package-lock.json with no dependency change.

* fix(wiki): resolve grok CLI through Windows cmd.exe shims

Extract resolveWindowsCliCommand from the local CLI client and use it for
grok detect/spawn so npm .cmd installs work without a shell. Keep
detectGrokCLI() returning the display name for the wiki menu.

* fix(wiki): wait for grok child close before timeout cleanup

Do not reject the grok spawn promise on the timeout timer. Kill the child,
escalate SIGKILL after 2s, and reject only on close (or a second 2s hard
deadline) so callGrokLLM cannot rm the sandbox while the process is alive.

* fix(wiki): reject incomplete grok stopReason and distinct parse errors

Honor JSON stopReason (end_turn or omitted succeeds; anything else throws).
Split empty-output / non-JSON / missing-text messages and include a truncated
stdout excerpt. Drop unused GrokConfig.workingDirectory.

* fix(wiki): keep grok temp dir on hung timeout and ignore stdin

Hard-deadline reject no longer removes --cwd while the child may still
be running. Spawn stdin is ignored so grok's unused pipe cannot EPIPE
the wiki process.

Co-Authored-By: Grok 4.6 <grok4.6@x.ai>

* fix(wiki): require grok stopReason=end_turn for a finished page

Live grok 1.0.5 with wiki spawn flags returns stopReason end_turn.
Omitted, null, or empty stopReason is no longer treated as success, so
generateLeafPage cannot write a page that never completed.

Co-Authored-By: Grok 4.6 <grok4.6@x.ai>

* style(wiki): deslop grok parse nesting and extra comments

Flatten parseGrokOutput with early returns and drop narrative comments
that restated the timeout/stdin/stopReason constraints. Behavior unchanged.

Co-Authored-By: Grok 4.6 <grok4.6@x.ai>

* test(wiki): make grok Windows spawn tests match real cmd.exe

On Windows CI, detectGrokCLI also calls where.exe, ComSpec is an
absolute cmd.exe path, and waitForSpawn must wait for real fs I/O.

Co-Authored-By: Grok 4.6 <grok4.6@x.ai>

* test(wiki): expect taskkill on Windows grok timeout, not child.kill

killChildTree uses taskkill /T /F on win32 and only falls back to
child.kill() if that fails.

Co-Authored-By: Grok 4.6 <grok4.6@x.ai>

* test(wiki): remove grok temp dir after hard-deadline leak assertion

The hard-deadline test must keep the dir until close, then emit close so
late cleanup runs and the temp directory is not left behind.

Co-Authored-By: Grok 4.6 <grok4.6@x.ai>

* test(wiki): wait for grok temp dir rm after late close

Windows CI failed the hard-deadline test because 30 setImmediate ticks
cannot observe fire-and-forget fs.rm. Poll with real timers after close.

---------

Co-authored-by: Grok 4.6 <grok4.6@x.ai>
2026-08-29 08:46:30 +01:00
Subham Kundu
5708db87d3
Change project title in README (#2986)
Updated project title to include 'Akon Labs'.
2026-08-18 01:55:30 -07:00
Shifra Williams
f2717c6a7c
feat(render): add one-click deploy to render support (#2804)
Some checks are pending
Scorecard / Scorecard analysis (push) Waiting to run
CodeQL / Analyze (javascript-typescript) (push) Waiting to run
CodeQL / Analyze (python) (push) Waiting to run
Gitleaks / gitleaks (push) Waiting to run
Publish / Classify release event (push) Waiting to run
Publish / RC guard (marker + release-PR skip) (push) Blocked by required conditions
Publish / ci (push) Blocked by required conditions
Publish / Publish to npm (push) Blocked by required conditions
Publish / Build & Push RC Docker images (push) Blocked by required conditions
Skill copy sync / shipped skills drift guard (push) Waiting to run
Trivy Image Scan / Trivy (gitnexus-cli) (push) Waiting to run
Trivy Image Scan / Trivy (gitnexus-web) (push) Waiting to run
2026-08-06 00:19:44 +00:00
Shifra Williams
a6a8aa788c
feat(serve): validate and port-scope the origin/proxy configuration surface (#2820) 2026-08-05 06:52:39 +01:00
ArgonarioD
39e9dc8b25 Merge remote-tracking branch 'upstream/main' 2026-07-23 14:50:09 +08:00
Gergő Magyar
9538be957d
fix(lbug): scale the buffer-pool budget by the OS page-size granule ratio (#2631) (#2636)
* fix(lbug): scale the buffer-pool budget by the OS-page discard-granule ratio (#2631)

LadybugDB bills buffer-pool budget per discard granule, not per 4 KiB frame:
the engine's vm_region.cpp sets discardGranuleSize = max(frameSize, osPageSize),
claimFrame charges the whole granule when its first frame becomes resident, and
releaseFrame refunds only when the granule's last frame leaves — while
BufferManager::reserve measures eviction progress in refunded bytes and throws
'The buffer pool is full and no memory could be freed!' after three zero-refund
passes. On a 64 KiB-page kernel (Ascend/aarch64 openEuler — the #2631
reporter's host) that is 16 frames per granule: the same COPY bills up to 16×
the budget it needs on x86, and whole eviction passes can evict frames yet
refund nothing. Apple Silicon macOS (16 KiB pages) is the same mechanism at 4×.

Measured with the reporter's exact command and version: vllm-ascend needs a
(128, 256] MiB pool on 4 KiB pages — 64/128 MiB reproduce the reporter's
byte-identical error, 256 MiB and the 576 MiB adaptive pool succeed — so their
64 KiB host cannot survive on a page-size-blind budget.

Scale every derived pool size by granuleRatio = max(1, osPageSize/4096):
the per-element estimate, the COPY-safety floor, and the default cap (still
bounded by 80% of RAM). 4 KiB hosts are byte-identical to before — proven by
pinning the existing sizing tests to an explicit 4096 page size, which also
stops them drifting on 16 KiB Apple Silicon runners. GITNEXUS_LBUG_BUFFER_POOL_SIZE
keeps absolute precedence and 0 still restores the native default.

Also: bufferPoolExhaustionRemedy() gives the exhaustion error an actionable
cause→consequence→remedy message; the isLbugPageSizeFrameError comment that
called pool exhaustion 'a sizing problem, not a page-size one' is corrected —
that framing inverted when #2582 made pool size a function of a page-size-blind
estimate. Cannot execute on a 64 KiB kernel here: the scaled path is proven by
unit stubs plus the engine-source math above; the env override remains the
field escape hatch.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(cli): actionable pool-exhaustion remedies at the COPY sites and a doctor pool line (#2631)

The node-COPY throw and the relationship-COPY warning now append
bufferPoolExhaustionRemedy() when the failure is the engine's pool-exhaustion
class: the raw binder text gave the operator nothing to act on, and on
non-4K-page hosts the pool bills up to pageSize/4KiB × faster than the sizing
was calibrated for. The relationship path appends the remedy once per bulk
load, not once per failed pair. doctor prints the effective pool size next to
the page-size line ('pool size 2048 MiB', with an '(×N page-size scaling)'
suffix on non-4K hosts) so support triage sees the sizing inputs at a glance.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(lbug): re-anchor getEffectiveBufferPoolSize's placement and reuse granuleRatio in doctor

Self-review fixes: the getter's insertion had orphaned resolveBufferManagerSize's
doc comment (it read as documenting the wrong function), and doctor's scale note
duplicated the granule math with a hardcoded 4096. granuleRatio is now exported
(it already carried the test-seam default param) and doctor consumes it.
No behavioral change — the sizing suite pins byte-identical outputs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(lbug): keep the hintless pool default unscaled and make both remedies visible (#2631)

Review fixes:
- Scale only the analyze-path cap (scaledAnalyzePoolCap), not
  defaultBufferPoolSize: the pool is an eager native allocation at DB open
  (measured, see POOL_BYTES_PER_ELEMENT), so a page-size-scaled hintless
  default would hand a long-lived MCP process up to 80% of RAM — the #2557
  OOM exposure the 2 GiB cap removed. Fix the MAP_NORESERVE claim that
  contradicted that measurement.
- Log the rel-pair pool remedy (loadGraphToLbug returns warnings that no
  call site reads) and dedup it with a local boolean instead of matching
  the remedy's own wording.
- Label the GITNEXUS_LBUG_BUFFER_POOL_SIZE=0 sentinel as the native
  80%-of-RAM default in both the remedy and doctor instead of '0 MiB'.
- Extract poolSizeDoctorLine (pageSizeDoctorLines convention): mark env
  overrides, drop the scaling suffix that misdescribed absolute values.
- Fold _resetOsPageSizeCacheForTest into _setOsPageSizeForTests(undefined).
- Document the analyze-path scaling in both README env tables.

---------

Co-authored-by: Gergo Magyar <abhigyan1.patwari@gmail.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 20:09:48 +01:00
ArgonarioD
38d0256474 Merge remote-tracking branch 'upstream/main' 2026-07-22 11:00:10 +08:00
Ko
2a85425ad8
Merge pull request #2542 from GenKoKo/fix/worker-stdout-and-ready-timeout
fix(ingestion): pipe worker stdout and make ready timeout configurable
2026-07-21 13:54:34 +01:00
ArgonarioD
1c98e7c6dd fix(cli): make .agents/ skill mirror best-effort + exclude from dirty check
Address review findings on PR #2488:

- skill-gen.ts: wrap mirror-root mkdir and per-skill mirror writes in
  try/catch + warn, so a mirror failure (e.g. .agents/skills is a file)
  no longer aborts canonical community-skill generation or destroys prior
  output. Mirroring is now a weak side-flow, matching ai-context.ts.
- git.ts: exclude .agents/ + .agents/** from isWorkingTreeDirty so a
  tracked .agents/ dir doesn't permanently defeat the up-to-date fast path.
- README + --skip-skills help (en/zh): note skills also mirror to
  .agents/skills/ when .agents/ exists, and --skip-skills skips both.

Tests: +18 covering mirror failure paths (root-is-file, per-skill fail,
delete-then-rewrite ordering, namespace-scoped cleanup), dirty-check
excludes (real-edit regression, prefix collision, subdir .agents/,
non-git/git-missing conservative fallback), gate on file-not-dir, and
idempotency.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-20 16:19:59 +08:00
Gergő Magyar
8b5057f325
feat(skills): GitNexus Engineering Tool Kits (#2566)
* feat(skills): add ce-plan — GitNexus+PDG implementation-planning skill

Adds .claude/skills/ce-plan: a planning-only skill that builds
implementation-ready plans from GitNexus graph navigation (query/context/
impact/trace), bounded statement-level PDG slices (pdg_query, impact
mode:pdg, explain), and targeted source verification, with a context
ledger to prevent repeated reads and a machine-readable implementation
context pack (stable contract for a future ce-implement). Whitelisted in
.gitignore and registered in AGENTS.md and CLAUDE.md outside the
auto-managed gitnexus block.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(skills): apply ce-plan validation findings (tool contract, consistency, conventions)

Tool contract: impact mode:'pdg' shape now includes the schema-required
direction param; CDG branch sense documented as the result 'label' field
(reason is cypher/raw-edge only); explain caveats corrected to its real
false-negative classes (cross-function TAINT_PATH is modeled).

Consistency: PDG slice homed in working memory (ledger keeps one-liners);
depth knob defined and category-overrides-baseline ordering stated;
call_depth (consumed by nothing) and content-hash bookkeeping dropped;
Never section folded into Hard rules; Phase 3 deduplicated to a pointer;
allowed-repeat escalations defined; budget/discard accounting clarified;
verification-commands gathering added to Phase 4; open_questions added to
the context pack.

From scenario runs: plans now pin the verified-at HEAD commit and index
freshness in a header, tag claims [verified]/[graph]/[inferred]/[assumed],
quote load-bearing tool output, prefer pre-hook-carrying npm scripts, and
support an out:<path> destination override; output path defined as the
Phase 1 target repo root.

Conventions: AGENTS.md 1.9.0 / CLAUDE.md 1.4.0 changelog rows + metadata
bumps; future ce-implement qualified as future.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(skills): rename ce-plan → gitnexus-plan; add cross-CLI (Codex) entrypoints

Renames the skill dir, frontmatter, output filename convention, plan H1
(GitNexus Engineering Plan), the future executor handle
(gitnexus-implement), the .gitignore whitelist entry, and all
AGENTS.md/CLAUDE.md references. Follows the pr-swarm-review cross-CLI
pattern: SKILL.md is the canonical CLI-neutral spec, AGENTS.md § Engineering
planning is the Codex/any-agent entrypoint, and the README documents the
optional user-level ~/.codex/prompts/gitnexus-plan.md slash command plus an
invocation matrix. Skill prose de-branded from Claude Code (agent-neutral
verification layer).

Also fixes two post-review README contradictions: the anti-reread claim now
names the ledger's allowed escalations, and 'read-only by contract' is now
'planning-only' (the skill writes exactly one repo file — the plan); the
scope-creep rule and template §12 now agree on where deferred follow-ups
land. Drops the stale plugin-collision limitation.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs(skills): document Codex user-level install path for gitnexus-plan

Codex discovers SKILL.md skills from ~/.agents/skills (same path the other
gitnexus-* skills install to); README now documents the cp install plus the
optional ~/.codex/prompts slash-command file, with the prompt body preferring
the repo copy and falling back to the user-level install.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(skills): gitnexus-plan freshness gate + active PDG-layer refresh

Freshness is now a Phase 1 gate, not advisory: under the default
freshness:strict, a stale index is refreshed once per planning session via
node .gitnexus/run.cjs analyze --index-only (appending --pdg when the task
will reach the PDG phase), then the context resource is re-read. A missing
PDG layer likewise triggers the one permitted --index-only --pdg refresh
and re-probe instead of a passive recommendation. freshness:accept (or a
failed/impractical refresh) preserves the old behavior: plan on the stale
graph, source-weighted, labelled in the plan header. --index-only is the
load-bearing flag choice — it suppresses all file generation, so the
planning-only contract holds (only the .gitnexus store changes). Ledger
gains an index_refresh record; plan header states fresh / refreshed /
refresh-skipped-with-reason.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(skills): gitnexus-plan runner build check before freshness refresh

When the target repo builds the analyzer from its own source (bin → dist/
mapping, as gitnexus/ does), the Phase 1 freshness gate now verifies dist/
is current before running the analyze refresh — rebuilding via the
package's build script when any analyzer source file is newer than the
built entrypoint — and prefers that freshly built CLI. Otherwise a stale
dist re-indexes with outdated extraction logic and the 'fresh' index lies.
Rebuilds are recorded in the ledger's index_refresh; the PDG-phase refresh
inherits the same check.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(skills): add gitnexus-work executor and gitnexus-lfg pipeline

gitnexus-work executes a gitnexus-plan as verified atomic commits: consumes
the §11 implementation_context pack, drift-checks the plan's evidence pin
against HEAD, re-verifies assumptions before relying on them, runs impact
before every symbol edit and detect_changes before every commit (repo
mandates), builds tests from the plan's scenarios, and routes structural
drift back to gitnexus-plan Deepen mode instead of coding around it.

gitnexus-lfg is a thin orchestrator: gitnexus-plan → blocking user gate
(deepen / proceed / stop, deepen loops allowed) → gitnexus-work → review
via the existing gitnexus-pr-review skill (open PR, else branch diff vs
default). One bounded fix cycle for review findings; never pushes or opens
a PR on its own.

gitnexus-plan gains a Deepen mode (re-run freshness gate, escalate to
depth:deep, re-verify graph/inferred/assumed claims toward verified,
rewrite the same file); its 'future gitnexus-implement' placeholder is
retired in favor of gitnexus-work. Registered via .gitignore whitelists,
AGENTS.md 1.10.0 (section renamed to Engineering planning & execution),
CLAUDE.md 1.5.0.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(skills): apply cross-skill review findings to the gitnexus skill family

Two P1s: gitnexus-plan Deepen mode now re-anchors before re-pinning
(diffs the old evidence pin over every [verified]-claim file and re-reads
or downgrades before the header moves — moving the pin without this
laundered stale claims as verified); the index-refresh budget is stated
once in Phase 1 (one --index-only refresh plus at most one Phase 3 --pdg
upgrade per session, Deepen = its own session) with ledger and pdg-slice
deferring to it.

Contract fixes: gitnexus-work's drift check now covers every file the
pack cites (not just files_to_modify) and parses the full pack incl.
primary/related symbols and acceptance_criteria (walked in Phase 4
alongside §13); a pre-completed check skips §7 steps already landed and
Deepen gains a reconcile-execution-state step, closing the mid-execution
route-back loop; pack assumptions must name what to check and how.

lfg: Lane 4 passes the merge-base to detect_changes compare (two-dot
diff misattributes upstream commits when default advanced), branch-diff
is the stated normal case, oversized review findings route to the plan
gate instead of overflowing direct mode, the one-fix-cycle cap is
explicit on re-run, and headless runs end at the plan gate with the plan
as deliverable. work: blank mode narrowed to *gitnexus-plan*.md with a
re-execution guard, direct-mode discipline spelled out, branch
meaningfulness defined against the plan slug, and the plan document is
committed as the branch's docs commit (review diff includes it).
Planning-only contract now names the dist/ rebuild as the second
permitted state change; Phase 5.1 names the four claim tags; stale
AGENTS.md anchors fixed.

Known latent issue left untouched: gitnexus/gitnexus-pr-review pairs a
three-dot example with a two-dot detect_changes compare — that skill is
also shipped by the plugin, so fixing it here would drift the copies;
lfg compensates by passing the merge-base.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(skills): ship the engineering skill family with the gitnexus package

npm i -g gitnexus users now get gitnexus-plan / gitnexus-work / gitnexus-lfg:
the three skills are added to gitnexus/skills/ in directory form (SKILL.md +
references/), which installSkillsTo already enumerates dynamically and copies
recursively to every editor target (~/.agents/skills for Codex, Cursor,
OpenCode, Qoder, ...) on gitnexus setup — uninstall enumerates the same root,
so removal stays clean. The Claude Code plugin channel
(gitnexus-claude-plugin/skills/) carries the same copies plus the standard
per-skill mcp.json.

Global-install support in the skill text: gitnexus-plan Phase 1 now resolves
the analyzer runner explicitly — node .gitnexus/run.cjs analyze when the
project has a runner, else gitnexus analyze (installed CLI), else
npx gitnexus analyze — and all analyze mentions route through it, satisfying
the skills-steering policy (#1939/#1945) which sweeps the plugin copies.

New drift guard test/unit/shipped-skills-sync.test.ts asserts the npm and
plugin copies stay byte-identical to the canonical .claude/skills/ family
(plugin = canonical + mcp.json), same discipline as run.cjs ↔
resolve-invocation.ts. skills-steering + shipped-skills-sync: 11/11 green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(eval): workflow_bench — measure the skill workflow's token savings

Benchmarks gitnexus-plan → gitnexus-work against a baseline agent
(--disallowedTools Skill) on identical tasks, in fresh detached worktrees,
using real headless Claude Code sessions; every number comes from the CLI's
--output-format json usage report (field names validated against a live
2.1.207 session). Reports per-arm medians (input/cache/output tokens, cost,
wall time, turns), a savings row, and resolve status from a per-task verify
command — savings on failed tasks are flagged, not celebrated. Per-task
setup hook prepares fresh worktrees (deps); --permission-mode
bypassPermissions (default) lets sessions run unattended in the throwaway
trees.

Free-model support: --base-url/--auth-token/--model route headless sessions
through any Anthropic-compatible endpoint; free-model.litellm.yaml is a
ready litellm-proxy template for OpenRouter :free variants or local Ollama,
so benchmarking burns no paid tokens (README documents rate limits and the
small-model skill-following caveat).

Harness validated end-to-end with a stub CLI (worktree lifecycle, both
arms, plan→work chaining, verify, aggregation, report) and 4 pytest units
for the pure aggregation/savings/report helpers. AGENTS.md 1.11.0.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs(eval): record first workflow_bench calibration run

Trivial-task calibration (add -V alias): both arms resolved; workflow arm
~4.3x baseline cost — the documented overhead-dominated regime, recorded so
the regime boundary is empirical rather than asserted.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(eval): workflow_bench scenario matrix — arm variants, task classes, churn

Ground-base measurement across scenarios: tasks.scenarios.yaml spans four
labeled classes (trivial → investigation-bug → investigation-feature →
cross-module) with deterministic verifies (prescribed test files). New arms:
workflow_direct (gitnexus-work direct mode — the middle option that locates
the routing boundary lfg's gate and work's triage encode) and baseline_nomcp
(no skills AND no graph tools — separates workflow-discipline value from
GitNexus-tool value; off by default). Records now carry task class and diff
churn (files/+ins/−del vs the starting commit) as an over-engineering proxy;
the report renders a class column and per-arm savings rows vs baseline.
5 pytest units + stub-CLI e2e of the full three-arm matrix.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs(eval): record workflow_bench ground base; fix churn measurement bias

Ground base (3 classes x 3 arms, n=1/cell): every arm resolved every task —
pass/fail quality saturates at this difficulty, making the comparison pure
cost. Full plan→work never amortized its ~$9-11 fixed cost on tasks a
baseline finishes in ≤35 turns (−211% to −333% cost); workflow_direct sits
near baseline (−15% to −55%, once faster wall) with more test coverage.
Routing implication recorded: direct mode/plain agent below this scale,
full workflow for cross-module / multi-session / plan-as-deliverable work.
The cross-module cell and multi-run variance are the next measurements.

Churn fix: git add --intent-to-add -A before diffing (arms that never
commit no longer undercount new files) and :(exclude)docs/plans (the
committed plan doc no longer inflates workflow churn); this run's churn
numbers predate the fix and are omitted from the recorded table.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* perf(skills): cost-optimize the workflow from measured ground base

Every optimization targets a measured fixed-cost component
(eval/workflow_bench ground base: workflow arm −211% to −333% vs baseline,
all tasks resolved):

- Plan form is category-priced: compact form (core sections w/ § anchors
  preserved, ≤80 lines excl. pack, mini-pack subset of the context pack)
  for narrow/default categories; the full 13 sections only for deep work
  (refactor/security/performance/concurrency/architecture). A compact plan
  outgrowing its cap reclassifies to full rather than overflowing.
- Freshness gate is category-priced: compact categories default to accept
  (source-weighted, refresh only when a graph claim becomes load-bearing);
  strict stays the default for full-plan categories — the rebuild+re-index
  was the largest single fixed cost.
- Turn economy: per-category tool-call budgets (~10 to ~45; architecture
  uncapped); budget exhaustion routes open questions to §12 instead of
  more digging.
- gitnexus-work fast path: HEAD == evidence pin → skip all citation
  re-reading (the pin's entire point); mini-pack fields tolerated.
- lfg Lane 1 boundary triage: tasks below the measured ~35-turn boundary
  get offered gitnexus-work direct mode before the plan lane is spent.

Copies re-synced (npm skills/, plugin, ~/.agents); steering + sync guards
green. Re-measurement of the workflow arm follows to verify the numbers
actually improve.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs(eval): record optimization re-measurement — inv-bug workflow cell −20% cost

Same task, same conditions, post-830a0459 skills: $14.56→$11.70 (−20%),
83→72 turns, cache_read −24%; verified in-transcript that the compact form,
turn budget, and skipped rebuild/re-index all fired. Wall +15% from a work-
session test-debugging tail (n=1 variance). Regime unchanged (~3.5x baseline
on this class) — routing rule stands.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(eval): per-arm clone isolation — worktree ref-namespace leak contaminated an arm

The cross-module workflow_direct cell reported an impossible 28-turn solve
with churn byte-identical to the workflow arm: git worktree add shares the
repo's ref namespace, so the workflow arm's slug branch (created by
gitnexus-work Phase 2) survived worktree removal and the direct arm found
and adopted the completed work. Arms now get isolated git clone --shared
copies (object store via alternates, refs clone-local — agent branches and
stashes die with the clone; origin/<ref> fallback for non-default refs).
Leaked branch deleted; baseline arm verified clean (0 branch references in
its transcript); cell marked invalidated pending re-run.

Records the valid cross-module cells: workflow $18.32 vs baseline $18.03
(premium −1.6%, vs −211%..−333% on smaller classes) — fixed costs amortize
at this scale, with a less destructive diff and a plan artifact as bonus;
resolve rate still tied. Churn fingerprinting is what caught the
contamination — noted in the README as an integrity check.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs(eval): complete cross-module cell — direct mode wins 47% cost / 56% wall

Clean clone-isolated re-run: workflow_direct resolved the hardest class at
$9.53/52 turns/15m vs $18.03/98/34m baseline and $18.32/107/37m full
workflow. The measured story across all four classes: the execution
discipline (gitnexus-work) is the consistent sweet spot and delivers real
token savings on hard tasks; the planning pass buys its artifact, not
same-session savings. Resolve rate tied everywhere (n=1/cell caveat).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(eval): add trajectory-gated skill evolution (#2431)

- Pair prompt candidates with incumbent workflow arms
- Gate promotions on pinned-model quality and efficiency
- Expire router evidence and document its lifecycle

* fix(eval): allow pr-review skill candidates

* feat(skills): rename and generalize GitNexus review

* feat(eval): external-comparator and review arms for workflow_bench

- ce_workflow / ce_workflow_direct: compound-engineering ce-plan/ce-work
  arms prompted with the same structure as the gitnexus arms
- review / ce_review: gitnexus-review vs ce-code-review on an identical
  diff applied by the task's setup
- plan handoff is snapshot-based: committed example plans in docs/plans/
  tie on clone mtimes and broke the name-glob pick (executed a stale plan)
- verify output tail is recorded per run and the final working-tree patch
  is kept, so failed rows are diagnosable after the clone is destroyed

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(skills,eval): address #2431 review — data-safe rename migration, fail-closed bench evidence

- setup: never delete a legacy renamed skill dir — the installer cannot
  prove ownership (users customize or hand-write skills under these
  names); warn with the path instead, and the test now asserts survival
- workflow_bench: fail closed when a session's --output-format json
  report is empty, malformed, or missing usage fields — an exit-0 shell
  with no parseable usage no longer counts as measured evidence
  (5 parametrized regression tests)
- workflow_bench: document the trust model prominently (task setup/verify
  are shell-executed, sessions run bypassPermissions with the parent env,
  candidate overlays are prompt injection surface) in README + docstring
- free-model.litellm.yaml: master_key from LITELLM_MASTER_KEY env instead
  of a static token; loopback-binding warning
- ci: run the eval workflow_bench pytest suite on ubuntu (pytest+pyyaml
  only — no full eval stack)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(eval): demand observed foreground verification in headless work-arm prompts

In a headless -p session there is no later turn: a work arm backgrounded
its slow test run, scheduled wakeups that can never fire, and reported
done while two of its tests failed. All four work-arm prompts (both
skill families, symmetric) now require verification output to be
observed inside the session.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(skills): ask plan depth up front instead of offering deepen afterwards

gitnexus-plan Phase 0 now asks one blocking question in interactive
sessions — quick / standard / deep, mapped onto the existing depth/form/
freshness knobs — when the invocation carries no explicit depth signal.
Explicit knobs and headless runs skip the question (category posture
unchanged, so benchmarks and automation behave as before).

gitnexus-lfg's plan gate slims to proceed/stop: depth was already the
user's up-front choice, so deepening is no longer offered by default —
an explicit deepen request at the gate and executor route-backs still
run Deepen mode, which remains the mechanism for strengthening an
existing plan document.

All shipped copies resynced (npm skills/, Claude plugin); AGENTS.md
1.13.0 and CLAUDE.md 1.7.0 pointers updated, including the analyzer's
regenerated index-stats block at this branch's head.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(skills): taint pass, expert lenses, and post-work index refresh

gitnexus-review gains a PDG-backed taint-and-dependence pass (explain +
pdg_query, --pdg folded into the stale refresh on trust-boundary diffs) and
an Expert lenses section: domain reviewers derived from the graph's
clusters plus four cross-cutting lenses (architectural fit, language
conformance per the repo's own contract, Definition of Done, simplicity),
dispatched once after the evidence-gathering steps and scaled to the diff.
gitnexus-work Phase 4 now refreshes the knowledge graph after the DoD walk
via the resolved-runner ladder with analyze --index-only, so the lfg review
lane and later sessions query the finished work without dirtying the tree.
lfg's threshold-governance paragraph moves to its README; eval citations
are tagged as measured in the GitNexus repo. All shipped copies re-synced.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(cli): remove legacy gitnexus-pr-review on uninstall; cover the rename migration

uninstall's removal set now includes LEGACY_SKILL_DIR_NAMES derived from
RENAMED_SKILL_DIRS, so a pre-rename install is cleaned up instead of
orphaned. The rename warning gains behavioral coverage (fires with a legacy
dir present, silent without), and shipped-skills-sync asserts legacy names
stay absent from every shipped tree.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(eval): metric provenance, error-kind rows, skill-invocation verification, gate noise floor

The promotion gate defaults to cost_usd (the only metric that includes
subagent spend); token metrics carry an explicit main-loop-only warning in
the report and promotion.json. Rows are classified by error_kind
(session-error / verify-failed / infra-error), excluded from efficiency
medians, and the gate requires equal valid-run counts. Each session's
transcript is scanned for the expected Skill invocation and fails closed on
a verified miss; a one-run resolution edge no longer promotes (noise
floor). Per-run timeouts and setup failures record an infra-error row
instead of aborting the sweep. Overlays touching skills no candidate arm
exercises are rejected up front.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs: fix skill routing paths, version headers, and skill rosters

Routing tables point at the tracked direct skill paths (matching the
post-#2434 generator output), AGENTS.md/CLAUDE.md headers match their
latest changelog rows, the 1.12.0 row describes what the migration actually
does, package/cursor READMEs list the full shipped skill roster, and the
swarm READMEs describe /gitnexus-review's expert lenses instead of calling
it single-agent.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* ci: drift-guard workflow for skill copies; pin eval pip deps; track docs/plans

ci.yml ignores '**.md', so an md-only skill edit would merge without the
shipped-skills-sync test running — skill-sync.yml triggers exactly on the
guarded trees. The eval job's pip install is version-pinned, and
docs/plans/ is unignored so gitnexus-plan output can be committed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(ci): keep the runner-invocation literal in gitnexus-review; add concurrency block to skill-sync

skills-steering requires skills with a stale-index hint to carry the exact
'node .gitnexus/run.cjs analyze' form — restore it with the fallback ladder
as a parenthetical instead of replacing it. skill-sync.yml gains the
top-level concurrency block the workflow-convention check enforces.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(skills): token-economy guidance for expert lenses

Merge lenses that ground in the same material into one reviewer, and use
cheaper model/effort tiers for mechanical lenses where the harness offers
them, reserving the strongest engine for adversarial judgment.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test(eval): isolate transcript home on Windows

Ensure workflow_bench transcript tests set USERPROFILE alongside HOME so Path.home() resolves to the temporary test home on Windows.

* docs(skills): fold PR #2522 execution learnings into review/work/plan

Eight incident-backed hardenings from running the full skill cycle
(review -> plan -> work, 28-finding fix series) on PR #2522:

gitnexus-review:
- Expert lenses execute the code under review on candidate failing shapes
  (empirical probe outranks source reading — every HIGH the language
  lenses found came from a probe, not a read).
- Step 7 re-runs the exact CI check for refreshed baselines/fingerprints
  (a stale committed artifact is invisible in the diff; caught a red
  benchmarks arm).
- Step 8 treats version/invalidation constants as review surface
  (INCREMENTAL_SCHEMA_VERSION class recurred verbatim from #2494).

gitnexus-work:
- Step 4 proves regression tests discriminate against the pre-fix tree.
- Step 5 rebuilds executed build output before every verification run
  (parse workers load dist/; a correct fix 'failed' until rebuilt).
- Step 6 makes stage -> detect_changes -> commit one unbroken sequence.

gitnexus-plan:
- Phase 0 seeded-evidence mode: plan FROM a completed review's verified
  findings instead of re-running the graph ladder.
- Template §7: fingerprint/golden-guarded output rebaselines once, at the
  series tip.

All distribution copies resynced; shipped-skills-sync + skills-steering
24/24 locally.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(eval): close the skill-evolution loop with an automated proposer driver

workflow_bench.evolve adds the three arrows the README described as manual:
a proposer session that turns loser trajectories (results.jsonl rows,
transcripts, patches, the learning queue) into ONE bounded candidate
overlay, a driver that iterates propose -> paired benchmark -> deterministic
gate up to --generations, and an --apply step that copies a promoted
overlay onto the canonical skills and shipped mirrors as a working-tree
diff. The trust boundary is unchanged: overlays re-validate through
candidate_overlay_files before any benchmark or apply consumes them, and
committing, CI, and the PR merge stay human.

learnings.jsonl is gitignored: it is machine-local evidence, like the
session transcripts it complements.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs(skills): route live-task friction into the evolution learning queue

Each family skill gains a short 'Skill feedback' section: on friction with
the skill's own instructions, append one JSON line to
eval/workflow_bench/learnings.jsonl (GitNexus repo only) — never self-edit
the skill from a live task. The proposer in workflow_bench.evolve consumes
the queue as hints; a learning reaches a shipped skill only by beating the
incumbent on the paired benchmark. All shipped mirrors re-copied byte-
identical.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* ci(tests): run the evolve helper tests in the eval pytest job

test_evolve.py needs only pytest+pyyaml, same as the harness tests the job
already runs — without this line the new module had no CI coverage.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(ci): comment-triggered GitNexus review agent for PRs

'@gitnexus review' from a maintainer (OWNER/MEMBER/COLLABORATOR; the action
re-validates write access) runs the repo's gitnexus-review skill headlessly
against the PR and posts the review as a sticky comment — remote triggering
with no local setup. Read-only by construction: contents: read token,
Write/Edit and web tools disallowed, Bash allowlisted to git reads and the
gitnexus CLI; analyze parses PR code with tree-sitter, never executes it.
Requires the ANTHROPIC_API_KEY repository secret; activates once the file
is on the default branch.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(ci): dispatch lane + existing OAuth secret for the review agent

Align with claude.yml: same action pin and the CLAUDE_CODE_OAUTH_TOKEN
secret the repo already carries — no new secret to configure. Add a
workflow_dispatch lane (PR number input) so the agent can be triggered from
the Actions UI and tested before the issue_comment trigger reaches the
default branch. Allowlist gh pr view/diff and gh api, which the review
skill uses to pin PR SHAs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(ci): close a fork-PR RCE vector in the review agent's tool allowlist

A live headless run of the exact workflow session against PR #2431 (66
turns, full gitnexus-review pass) surfaced a real HIGH-severity confused
deputy: .gitnexus/ is gitignored, not blocked — a fork PR can commit its
own .gitnexus/run.cjs, issue_comment checks out PR-head content, and the
skill's runner ladder tries 'node .gitnexus/run.cjs analyze' first. That
would execute fork-controlled JS inside a job holding
CLAUDE_CODE_OAUTH_TOKEN and a write-scoped GITHUB_TOKEN — the opposite of
the 'PR code is read, never executed' claim in the workflow's own header.

Fix: drop the run.cjs allowlist entry so analyze always resolves through
npx gitnexus (npm registry, not the checked-out tree); the skill's
documented fallback mode covers the resulting graceful degradation. Also
drop 'gh api' (not read-only — accepts -X POST/PATCH/DELETE) and downgrade
pull-requests: write to read (comment posting only needs issues: write;
the prompt already forbids formal review submission).

Same session flagged a latent evolve.py bug: select_evidence's cost sort
used dict.get's missing-key default, which doesn't cover an explicit JSON
null in a foreign --seed-results row and crashes proposer setup with
TypeError. Guarded with 'or 0.0' and added a regression test.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix: harden PR review and evolution trust boundaries

* ci: follow workflow concurrency convention

* fix(eval): make terminating error paths explicit

* fix: unblock hardened review runtime checks

* test: make containment canaries deterministic

* test: expose Claude canary tool failures

* fix: adapt clean shell environment for Claude

* fix(eval): accept the runner's transcript source key in evidence preflight

The proposer evidence preflight required transcript-artifact metadata to be
exactly {path, sha256, bytes}, but the runner stamps a fourth provenance key
(source=parent-captured-stream-json). Any --seed-results or generation>=2 run
therefore aborted with SandboxError before proposing or promoting. Pin the
producer literal as PARENT_EVENT_STREAM_SOURCE and validate it in the metadata
check, and round-trip real producer output through sum_sessions into the
preflight so the schema can't drift again.

* fix(eval): treat an unmeasured session cost as unavailable, not $0

well_formed validated only the nested usage block, so an otherwise-successful
session missing total_cost_usd was recorded as cost_usd=0.0 — and cost_usd is
the default promotion metric (lower wins), so a cost-less session scored as
free and could win promotion it never earned. Extract cost via measured_cost()
(None on absent/garbage, a measured 0.0 preserved), propagate None through
sum_sessions/aggregate/savings/report, and have the gate refuse to rank on a
metric that was not measured on every run in both arms.

* fix(eval): warn when ranking on the main-loop-only num_turns metric

num_turns comes from the CLI's top-level usage (main-loop session only), like
output_tokens, but selecting it emitted no metric_warning — so a subagent-heavy
candidate could look artificially efficient. Add num_turns to
MAIN_LOOP_ONLY_METRICS and broaden the warning to cover turns.

* fix(eval): fail closed when an overlay adds a file with no committed base

An overlay adding a new .md under gitnexus-{plan,work} passes the structural
overlay checks but has no committed base for committed_destination_base_digests
to bind against, so it raised an uncaught ValueError that crashed the evolve
driver (and runner --candidate-overlay) mid-run. Catch it at both call sites:
evolve reports NOT PROMOTED and exits, runner routes it through parser.error.

* feat(eval): circuit-break the runner sweep on a systemic outage

A sustained upstream outage used to pay out every remaining --timeout window
one session at a time. Track consecutive session/infra/cleanup failures via a
pure systemic_outage_streak helper; after --outage-streak (default 5) in a row,
stop the sweep, still write report.md/promotion.json from partial evidence, and
exit non-zero so evolve.py halts instead of proposing from truncated evidence.
A task's own resolved=False never trips the breaker.

* fix(cli): report a dirty working tree as stale in gitnexus status

status --json (and the human output) computed up-to-date from commit + runner
identity + completeness only, so a repo with uncommitted source changes at a
matching HEAD was reported up-to-date while analyze would still re-index it.
A graph-backed agent gating on that JSON could skip re-analysis on a stale
graph. Extract analyze's dirty-tree check into a shared isWorkingTreeDirty()
in storage/git and fold it into the status freshness decision.

* fix(ci): use single-slash deny globs in the review agent's disallowedTools

github.workspace already expands to an absolute path, so Read(/${{ github.workspace }}/**)
and Read(//proc/**),(//sys/**),(//dev/**) produced double-slash patterns that a
normalizing matcher may not match — silently no-opping the deny layer. Not
exploitable (the allowlist is the primary control and never grants those
paths), but the globs should be well-formed. Update the pinned test strings.

* ci: install gitnexus-shared with npm ci from the committed lockfile

The gitnexus-shared build floated its deps via npm install in three workflows
(skill-sync, ci-tests, and — most importantly — the release publish.yml) while
every other install step uses npm ci. The lockfile is committed and in sync, so
switch all three to npm ci for reproducible, locked installs.

* test(cli): make the shipped-skills drift guard reject symlinks

listFilesRecursive walked with readdirSync and snapshotDir read with
readFileSync, both of which follow symlinks — so a mirror file symlinked to the
canonical tree passed the byte-compare (and a symlinked mirror dir would be
followed too). Reject a symlinked root via lstat and any symlinked entry via
Dirent.isSymbolicLink, with negative tests (skipped on Windows).

* test(eval): guard the candidate-skill vs mirror-root coverage invariant

MIRROR_SKILL_ROOTS omits the Cursor tree, safe only because no candidate skill
is cursor-shipped. Pin that invariant: every CANDIDATE_SKILLS entry must exist
under canonical + every mirror root and must not ship to Cursor, so adding a
cursor-shipped skill to the candidate set (the PR #2488 asymmetric-sync class)
fails loudly instead of syncing three of four trees.

* docs(ci): describe the review agent's staged post-merge rollout

The DoD asked for a dry-run or triggered run before merge, but an issue_comment
(or newly added workflow_dispatch) workflow only ever executes the default-branch
copy, so it cannot be exercised from the PR that introduces it. Reword the DoD
and the activation checklist to a staged rollout: merge registered-but-disabled,
validate same-repo and fork execution post-merge, then enable the variable.

* fix: pin plugin skill mcp.json to the release version via #2445 tooling

The ten plugin skill mcp.json launched `npx -y gitnexus@latest mcp` on every
skill connect — non-reproducible and a supply-chain surface, and (unlike the
persisted setup config) never pinned. Extend sync-plugin-manifests.mjs with an
mcp surface kind that stamps the gitnexus@<version> launch arg, pin all ten to
1.6.9 now, and keep them byte-identical so the drift guard stays green. The
release lifecycle + publish.yml --check now re-stamp them like the four manifest
surfaces; only READMEs stay on @latest as docs.

* test(eval): prove the proposer's built-in file tools are confined

The real-Claude canary only exercised Bash + MCP, so it proved process/MCP
containment but not that the proposer's built-in file tools stay inside their
mounts. Add a canary over the exact PROPOSER_ALLOWED_TOOLS surface and the same
read-only /evidence mount as run_proposer (allowlist extracted to a shared
constant so it can't drift): Read reaches /evidence, a Write into the read-only
evidence mount is denied, and a Write lands in the output tree.

* fix(eval): apply the candidate overlay after task setup for fair arms

The candidate overlay was applied before the task's untrusted setup ran, so
setup could observe candidate prose and the incumbent/candidate arms started
from different pre-overlay state. Reorder within the sandbox: capture the base
(pre-overlay) skill digest, run setup against the base skills, verify setup did
not tamper them, then apply the overlay and capture the post-overlay digest the
model must preserve. apply_candidate_overlay stages path-specific overlay files,
so setup's uncommitted changes stay out of the baseline and churn is unchanged.

Graph freshness for the review arm is handled by the status dirty-tree fix plus
the review skill's stale-triggered re-index, not by reordering the cached
per-task-sha graph materialization (which is mechanically blocked).

* test(eval): end-to-end containment proof of the autonomous proposer

Drives the real run_proposer through bubblewrap with a deterministic scripted
model (no paid API): it reads the read-only evidence bundle and writes a
candidate gitnexus-plan skill edit plus a rationale into the sandbox output
tree; run_proposer enforces the trust boundary and copies only the validated
overlay + proposal out. This exercises the autonomous-proposal stage of the
self-evolution loop end-to-end in the eval/containment CI job (the gate and
apply stages are covered by test_workflow_bench_evolution and
test_promotion_apply). Env-gated on GITNEXUS_REQUIRE_CLAUDE_CANARY, so it runs
only where the pinned Claude binary and user namespaces are available.

* fix(eval): let the proposer author its overlay via Bash

Running the end-to-end proposer canary in the containment CI job surfaced a real
bug: run_proposer starts the session with --bare, which hard-disables the
Write/Edit tools ("Write exists but is not enabled in this context"), yet
allowlisted Edit/Write and omitted Bash. The proposer therefore had no working
way to write its candidate overlay — the self-evolution loop could never produce
a candidate. The sandbox settings already pre-authorize Bash
(autoAllowBashIfSandboxed) and confine writes to workspace/tmp/home, so switch
PROPOSER_ALLOWED_TOOLS to Read/Grep/Glob/Bash and tell the proposer to author
files with Bash. The end-to-end test now drives the real run_proposer through
bubblewrap and asserts a validated overlay + proposal are produced (this also
replaces the earlier file-tool canary, whose Write/Edit premise was moot).

* test(eval): author the proposer overlay with newline-free Bash content

The nested shell-sandbox prefix mangles embedded newlines, so the multi-line
overlay content never landed. Use single-line content for the deterministic
proposer canary.

* test(eval): drop the unverifiable end-to-end proposer canary

The scripted proposer overlay never materialized in the containment job across
runs, and the model tool-result content is not visible in CI logs, so the test
cannot be finalized without an environment where the sandbox can actually run.
Keep the verified production fix (Bash-authoring in run_proposer); the proposer
sandbox/containment stays covered by the existing Bash+MCP and process-tree
canaries.

* test(cli): drop run-analyze.ts from the windowsHide spawn-family list

U7 moved run-analyze.ts's only child_process call (the git status --porcelain
dirty check) into storage/git.ts (already covered by this test, with
windowsHide). run-analyze.ts no longer imports a spawn-family function, so the
windowsHide-regression test's 'must have >=1 spawn call' invariant failed for
it. Remove it from SRC_FILES.

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Zander Raycraft <zanderjraycraft@gmail.com>
Co-authored-by: Azizur Rahman <azizur100389@gmail.com>
2026-07-19 15:07:24 +01:00
Gergő Magyar
249f5c7aab
fix(lbug): bound the LadybugDB buffer pool instead of the native 80%-of-RAM default (#2560)
* fix(lbug): bound the LadybugDB buffer pool instead of the native 80%-of-RAM default (#2557)

createLbugDatabase passed bufferManagerSize=0, which the native runtime
sizes at 80% of physical RAM. A long-lived gitnexus mcp process (or a
large incremental analyze) could balloon to that ceiling — 19.5 GiB
observed against a 105 MiB on-disk index — and OOM-kill the host session.

Resolve the pool at call time: default min(2 GiB, max(64 MiB, 80% of
totalmem)), overridable via GITNEXUS_LBUG_BUFFER_POOL_SIZE (bytes);
0 deliberately restores the native unbounded default; invalid values
warn and fall back, mirroring GITNEXUS_WAL_CHECKPOINT_THRESHOLD.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs(readme): document GITNEXUS_LBUG_BUFFER_POOL_SIZE and GITNEXUS_LBUG_MAX_DB_SIZE (#2557)

Both env tables gain the new buffer-pool ceiling variable and the
previously code-comment-only GITNEXUS_LBUG_MAX_DB_SIZE, with the
mmap-vs-memory distinction the issue had to discover from source.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* refactor(lbug): keep parseBufferPoolSize module-private

Review finding: the export had zero importers — parseWalCheckpointThreshold
earns its export via the CLI flag validation, but the buffer-pool CLI flag
was deliberately deferred. Re-export when a consumer exists.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* chore(autofix): apply prettier + eslint fixes via /autofix command

---------

Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-07-18 20:50:14 +01:00
azizur100389
a333d94a00
feat(wiki): allow explicit HTTP LLM hosts (#2491)
* feat(wiki): allow explicit HTTP LLM hosts

Keep wiki LLM HTTP endpoints fail-closed by default while adding a narrow exact-host opt-in for LAN/self-hosted models.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(wiki): simplify insecure LLM flag name

Rename the wiki HTTP opt-in flag to --allow-insecure-connection per review feedback.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(wiki): simplify insecure connection env

Rename the wiki HTTP allowlist environment variable and align validation errors with the CLI flag naming.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
2026-07-16 13:53:58 +01:00
Gergo Magyar
c836801c4a feat(mcp): add deterministic response budgets (#2460)
Composed with the read-only and repository policies in the CallTool
handler: read-only assert, then budget resolution, then scoped dispatch
with the transport arg stripped.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 08:59:05 +00:00
Gergo Magyar
48a145a47f feat(mcp): enforce repository allowlist and default (#2465)
Merged with the read-only policy: both filters compose in server.ts.
Review hardening: assertResourceUri compares the opaque host case
insensitively (GITNEXUS://GROUP bypass) and gains the fail-closed
fallback for unparseable URIs; the backend proxy now also intercepts
queryClusters/queryProcesses/queryClusterDetail/queryProcessDetail;
scrubGroupDescription is shared from read-only-policy.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 08:50:07 +00:00
Gergo Magyar
a7775de993 feat(mcp): add fail-closed read-only mode (#2464) 2026-07-16 08:42:32 +00:00
Eva
575dc6810a fix(mcp): validate repository policy before embedded serving 2026-07-16 09:39:06 +07:00
Eva
d4eb7560ac fix(mcp): close read-only resource routing bypass 2026-07-16 09:34:34 +07:00
Eva
0506f91eb9 docs(mcp): explain response budget guardrails 2026-07-16 09:26:19 +07:00
Eva
9741c48879 docs(eval): avoid secret-scanner auth fixture 2026-07-16 09:15:08 +07:00
Eva
c9fdab17f2 fix(eval): address auth configuration feedback 2026-07-16 09:13:34 +07:00
Gergő Magyar
a05b501102
fix(cli): make Claude skills discoverable (#2434)
Some checks are pending
CodeQL / Analyze (javascript-typescript) (push) Waiting to run
CodeQL / Analyze (python) (push) Waiting to run
Gitleaks / gitleaks (push) Waiting to run
Publish / Classify release event (push) Waiting to run
Publish / RC guard (marker + release-PR skip) (push) Blocked by required conditions
Publish / ci (push) Blocked by required conditions
Publish / Publish to npm (push) Blocked by required conditions
Publish / Build & Push RC Docker images (push) Blocked by required conditions
Scorecard / Scorecard analysis (push) Waiting to run
Trivy Image Scan / Trivy (gitnexus-cli) (push) Waiting to run
Trivy Image Scan / Trivy (gitnexus-web) (push) Waiting to run
2026-07-15 20:40:20 +05:00
Gergő Magyar
c6445096eb
fix: stop Napi::Error SIGABRT on analyze — index C++ type lookups, terminate workers only at JS-safe points (#2432) (#2436)
Some checks failed
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Gitleaks / gitleaks (push) Has been cancelled
Publish / Classify release event (push) Has been cancelled
Scorecard / Scorecard analysis (push) Has been cancelled
Trivy Image Scan / Trivy (gitnexus-cli) (push) Has been cancelled
Trivy Image Scan / Trivy (gitnexus-web) (push) Has been cancelled
Publish / RC guard (marker + release-PR skip) (push) Has been cancelled
Publish / ci (push) Has been cancelled
Publish / Publish to npm (push) Has been cancelled
Publish / Build & Push RC Docker images (push) Has been cancelled
2026-07-11 18:07:08 +01:00
Gergő Magyar
cdad478c96
fix: proxy-blocked installs survive onnxruntime-node postinstall and self-heal embeddings (#2370) (#2372)
Some checks are pending
CodeQL / Analyze (javascript-typescript) (push) Waiting to run
CodeQL / Analyze (python) (push) Waiting to run
Gitleaks / gitleaks (push) Waiting to run
Publish / Classify release event (push) Waiting to run
Publish / RC guard (marker + release-PR skip) (push) Blocked by required conditions
Publish / ci (push) Blocked by required conditions
Publish / Publish to npm (push) Blocked by required conditions
Publish / Build & Push RC Docker images (push) Blocked by required conditions
Scorecard / Scorecard analysis (push) Waiting to run
Trivy Image Scan / Trivy (gitnexus-cli) (push) Waiting to run
Trivy Image Scan / Trivy (gitnexus-web) (push) Waiting to run
2026-07-05 16:15:10 +01:00
Gergő Magyar
187c162fd8
feat: full Codex support — hooks, plugin marketplace, and setup (#2328, supersedes #1131) (#2369)
Some checks are pending
CodeQL / Analyze (javascript-typescript) (push) Waiting to run
Publish / Classify release event (push) Waiting to run
Publish / ci (push) Blocked by required conditions
CodeQL / Analyze (python) (push) Waiting to run
Gitleaks / gitleaks (push) Waiting to run
Publish / RC guard (marker + release-PR skip) (push) Blocked by required conditions
Publish / Publish to npm (push) Blocked by required conditions
Publish / Build & Push RC Docker images (push) Blocked by required conditions
Scorecard / Scorecard analysis (push) Waiting to run
Trivy Image Scan / Trivy (gitnexus-cli) (push) Waiting to run
Trivy Image Scan / Trivy (gitnexus-web) (push) Waiting to run
* feat(setup): install Codex PreToolUse/PostToolUse hooks (#2328)

Codex CLI supports lifecycle hooks with Claude Code's exact
{hooks: {Event: [...]}} JSON schema, stdin payload, and
hookSpecificOutput response contract, registered in a dedicated
~/.codex/hooks.json (https://developers.openai.com/codex/hooks).

Parameterize installClaudeCodeHooks into installClaudeSchemaHooks
(claude | codex): both runtimes share the installer, the bundled
gitnexus-hook.cjs adapter, and its helpers. A codex HookTarget in
editor-targets.ts makes uninstall and the setup-uninstall round-trip
tripwire cover the new surface with no uninstall.ts changes.

SessionStart is deliberately not registered: Codex reads AGENTS.md
natively, which already carries the GitNexus context block.

Closes #2328. Closes #244 (Codex setup support is now complete:
MCP + skills + hooks).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(plugin): make the GitNexus plugin installable from Codex (#1131)

Codex's plugin system (https://developers.openai.com/codex/plugins/build)
reads a .codex-plugin/plugin.json manifest and a repo-root
.agents/plugins/marketplace.json registry. The existing
gitnexus-claude-plugin/ is already Codex-compatible as-is — Codex sets
CLAUDE_PLUGIN_ROOT for hook-command compatibility, loads the same
SKILL.md skills, hooks/hooks.json, and .mcp.json — so a second manifest
in the same folder replaces PR #1131's duplicated plugin tree with zero
copied skills or hooks. The .gitignore .agents/ scratch rule narrows to
re-include only the registry file.

Install: codex plugin marketplace add abhigyanpatwari/GitNexus

Supersedes #1131.

Co-authored-by: jublin <1799126+jublin@users.noreply.github.com>

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs: document Codex full support (MCP + skills + hooks + plugin)

Promote Codex to Full in both editor tables, document the
~/.codex/hooks.json hook install, and add the Codex plugin
marketplace install path.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test(release): extend the version-lockstep guard to the Codex manifests

The always-on drift guard asserted only the Claude plugin manifests
against gitnexus/package.json, so a release could ship stale versions in
.codex-plugin/plugin.json and .agents/plugins/marketplace.json without
CI noticing. Mirror the Claude lockstep test for the two Codex files and
extend the CONTRIBUTING §Releases lockstep list to match.

Verified guard semantics: a deliberate local version mutation of the
Codex marketplace entry turns the new test red.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(plugin): quote the hook command path for space-containing plugin roots

Both plugin hook commands ran `node ${CLAUDE_PLUGIN_ROOT}/hooks/...`
unquoted, which breaks whenever the substituted plugin root contains a
space — the common case on Windows user profiles. Both Claude Code and
Codex substitute the placeholder before shell execution, and Claude
Code's plugin docs mandate the double-quoted form in shell-form hooks.

No commandWindows entry: Codex source (codex-rs hooks engine) falls back
to `command` on Windows with identical placeholder substitution, so an
identical-content override would be pure duplication.

Verified: space-in-root smoke test (old form exits 1 MODULE_NOT_FOUND,
quoted form exits 0), `claude plugin validate` passes, and a local
`codex plugin marketplace add` parses the marketplace + plugin cleanly.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test(setup): pin fail-closed behavior for unreadable/corrupt Codex hooks.json

The non-ENOENT suite covered Claude settings.json (EACCES) and Codex
config.toml (EACCES) but not the new ~/.codex/hooks.json surface, and the
mergeHooksJsonc "is corrupt" branch had zero coverage for either editor.
A future refactor dropping the isEnoent rethrow or the parse gate could
silently rewrite a user's hooks.json gitnexus-only with no CI tripwire.

Two regression tests: EACCES leaves hooks.json byte-identical and reports
"Codex hooks: EACCES"; corrupt content is preserved and reported via
"Codex hooks: hooks.json is corrupt".

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs(readme): add the Codex plugin-marketplace install path to the npm README

The root README documents the one-step plugin route but the package
README (what npmjs.com renders) only showed the setup-CLI path.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* refactor(setup): rename claudeHook to hookCfg in installClaudeSchemaHooks

The local held a codex HookTarget on the codex branch since the installer
was parameterized, so the claude-specific name misled. Pure local rename,
no behavior change.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs(readme): document Codex SessionStart exclusion, /hooks trust gate, and install-route choice

Three behaviors were only recorded in code comments and the PR body:
SessionStart is deliberately not registered (Codex reads AGENTS.md
natively), setup-installed hooks need one-time /hooks approval in Codex,
and the setup CLI and plugin are alternative install routes whose hooks
load alongside each other if both are used.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* refactor(test): share one logLines helper across setup.test.ts describes

The corrupt-hooks.json test inlined the console.log-flattening
expression that the non-ENOENT describe already defined locally. Hoist a
single file-scope logLines so the two stay in sync.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 13:32:17 +01:00
Gergő Magyar
6252aa745f
feat(setup): add CodeBuddy and Qoder coding-agent integrations (#2368)
* feat(setup): add CodeBuddy and Qoder coding-agent integrations

Adds Tencent CodeBuddy and Alibaba Qoder to gitnexus setup/uninstall,
fitted to the editor-targets registry and --coding-agent selection.

- CodeBuddy: MCP entry written into the first existing file of its
  documented priority chain (~/.codebuddy/.mcp.json recommended,
  ~/.codebuddy/mcp.json deprecated, ~/.codebuddy.json legacy) so a
  populated deprecated config is never shadowed; skills to
  ~/.codebuddy/skills/ (https://www.codebuddy.ai/docs/cli/mcp)
- Qoder: MCP entry in ~/.qoder.json, skills to ~/.qoder/skills/
  (https://docs.qoder.com/cli/using-cli, /extensions/skills)
- editor-targets gains optional legacyFiles; uninstall sweeps them
- roster strings updated (CLI help, i18n en/zh-CN, READMEs); en/zh-CN
  setup descriptions were stale (missing Antigravity) and are refreshed

Supersedes and credits PR #1030 by @zykai0302, re-fitted to the
post-#2168 selective-agent architecture with documented config paths.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test(cli): assert stable zh-CN setup-description fragment

* fix(setup): surface non-ENOENT config read/stat failures instead of clobbering

* fix(setup): report corrupt legacy MCP files informationally during uninstall

* test(setup): cover multi-candidate uninstall sweep combinations

* fix(setup): detect CodeBuddy/Qoder installs via existing MCP config files

* fix(setup): skip empty and non-file candidates in the MCP config chain

* docs: add CodeBuddy and Qoder manual MCP configuration sections

* test(ci): run the setup-uninstall round-trip in the cross-platform matrix

* fix(setup): never claim "not configured" when uninstall recorded errors

* refactor(cli): share the isEnoent predicate via editor-targets

* refactor(setup): share chain-file install detection between CodeBuddy and Qoder

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 10:54:50 +01:00
Gergő Magyar
e46b87f291
feat: flat workspace index follows the checked-out branch (#2364)
Some checks failed
Devcontainer Smoke / Config-transform unit tests (push) Has been cancelled
Devcontainer Smoke / Build devcontainer image (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Waiting to run
CodeQL / Analyze (python) (push) Waiting to run
Gitleaks / gitleaks (push) Waiting to run
Publish / Classify release event (push) Waiting to run
Publish / RC guard (marker + release-PR skip) (push) Blocked by required conditions
Publish / ci (push) Blocked by required conditions
Publish / Publish to npm (push) Blocked by required conditions
Publish / Build & Push RC Docker images (push) Blocked by required conditions
Scorecard / Scorecard analysis (push) Waiting to run
Trivy Image Scan / Trivy (gitnexus-cli) (push) Waiting to run
Trivy Image Scan / Trivy (gitnexus-web) (push) Waiting to run
* feat: flat workspace index follows the checked-out branch (#2354)

A plain `gitnexus analyze` now always targets the flat workspace slot,
updating it incrementally across branch switches instead of auto-routing
non-owner branches into `branches/<slug>/` sub-indexes (disk bloat) or
nagging with the primary-inversion "run gitnexus clean" warning. No new
CLI flag or config key: the smart behavior is the default.

- Placement: only explicit `--branch` consults resolveBranchPlacement;
  plain runs resolve to the flat slot, `meta.branch` becomes an
  informational "last analyzed branch" label restamped each run.
- Fast path: a same-commit clean-tree branch flip restamps the label and
  registry entry (adoptFlatBranchLabel, no-op for unregistered repos).
- Shadow cleanup: when the flat slot adopts a label that has a pinned
  sub-index, the now-unreachable `branches/<slug>/` dir and its registry
  summary are removed together.
- MCP: applyBranchScope always falls back to the on-disk flat meta before
  throwing "not indexed", so long-lived servers resolve a freshly
  restamped workspace branch.
- status: no more "current branch not indexed" dead end — falls through
  to the workspace index with an informational line and the usual
  commit-based staleness verdict.
- Deleted primaryInversionWarning; explicit `--branch` pinning, the
  checkout-mismatch guard, detached-HEAD/CI behavior, and `clean
  --branch` are unchanged.

Supersedes the flag-based approaches in #2358/#2359.
Closes #2354.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(storage): check registry before deleting shadowed sub-index (#2364 review F2)

adoptFlatBranchLabel ran the branches/<slug>/ rm before its own
unregistered-repo no-op check, so a repo in the #2264 half-finalized
state (up to date but unregistered) lost its pinned sub-index on a
same-commit branch flip while the run still failed. The registry
lookup now precedes the deletion, making the no-self-heal rule
(#2264/#1169) cover disk as well as registry state.

The 'never self-heals' unit test now materializes a sub-index dir and
asserts it survives; the run-analyze #2354 fast-path test registers
its repo under an isolated GITNEXUS_HOME (deletion is only legitimate
for registered repos) with a new unregistered variant pinning
dir survival.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(storage): keep branch summary when sub-index rm fails (#2364 review F4)

The shadow-cleanup fs.rm swallowed every error while the registry
summary was dropped unconditionally. On Windows an lbug held open by a
live MCP server fails the rm with EBUSY/EPERM, and once the summary is
gone 'clean --branch' can never target the leftover dir (it resolves
solely via the recorded summary) — stranding the exact un-cleanable
disk bloat adoptFlatBranchLabel exists to prevent.

The summary is now dropped only when the directory is verifiably gone
(post-rm existence check); on failure the summary is retained, a
warning names the path and errno, and the informational branch label
still restamps. Later adopts retry the rm.

New repo-manager-rm-failure.test.ts uses the delegating fs/promises
mock idiom (vi.spyOn cannot intercept ESM namespace exports).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(core): restamp fast path adopt-first and tolerate read-only storage (#2364 review F3)

The fast-path label sync stamped meta before adoptFlatBranchLabel, so
a crash or adopt failure between the two flipped the retry guard
(existingMeta.branch !== branchLabel) and locked in the partial state:
every subsequent same-commit run skipped the cleanup and branch-scoped
queries kept routing to the stale pinned sub-index. The block also sat
outside any try/catch, so a same-commit branch flip on a read-only
.gitnexus mount (the documented Docker :ro workflow, #1549) failed a
byte-for-byte-current analyze over a purely informational label sync.

Adopt now runs first and saveMeta last — any partial failure leaves
the guard true and the next run self-heals — and the whole sync is
best-effort: read-only errors warn citing #1549, anything else warns
and retries next run. Safe because the block only fires on a
same-commit clean tree, where the flat DB content is byte-valid for
both labels. isReadOnlyFilesystemError is now exported.

New run-analyze-adopt-failure.test.ts covers retry-after-partial-
failure, adopt-before-stamp ordering, and EROFS/EACCES/EPERM (gaps 4
and 7); a detached-HEAD fast-path pin lands in run-analyze.test.ts
(gap 6).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(mcp): make flat meta authoritative in applyBranchScope (#2364 review F1)

applyBranchScope trusted two pieces of cached state before its flat-
meta disk fallback, and the handle cache only refreshes on a resolve
miss — never on a hit. Post-#2354 that stale window is the routine
case: (i) the handle.branch early-return served the flat handle under
the OLD label after a workspace flip, silently returning the new
branch's content as the old branch (the pool staleness reinit hot-
swaps content without updating handle.branch); (ii) a stale cached
branches[] summary routed to a branches/<slug>/ dir that
adoptFlatBranchLabel had already deleted (raw 'LadybugDB not found' or
POSIX ghost reads with staleness detection blinded).

The on-disk flat meta is now read before any cached-state trust. A
branches[] summary is served only when its sub-index lbug actually
exists (the lbug is what the pool opens — serviceability truth); the
cached label is trusted only when no readable flat meta contradicts it
(#2106 R4 legacy shapes preserved). One refreshRepos() fires on
detected staleness so subsequent calls see fresh handles. Safe against
mid-analyze reads: dirty stamps spread the existing meta, preserving
the old label until the end-of-run atomic write.

Fixtures now materialize the pinned sub-index lbug; new regressions
cover the stale-old-label error, adopted-summary fall-through to flat,
and the dangling-summary partial-failure window (test gaps 1-2).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(core): make end-of-run branch-label sync best-effort (#2364 review F5)

The end-of-run adoptFlatBranchLabel sat inside the pipeline try whose
catch rethrows, so a registry write failure (ENOSPC, ~/.gitnexus
perms) after a successful multi-minute analyze failed the whole run —
even though the index was complete and registered, the neighbouring
parse-cache save is deliberately wrapped for exactly this reason, and
adopt retries unconditionally on the next plain analyze. It now warns
and continues, mirroring the parse-cache wrapper.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(mcp): correct branch-not-indexed guidance for workspace index (#2364 review F6)

The error told users to 'Run: gitnexus analyze --branch <X>', but
post-#2354 that command hard-errors unless X is checked out — and this
message is now the common goodbye for a formerly-indexed branch whose
sub-index the workspace slot adopted. The guidance now explains that
the workspace index follows the checked-out branch and leads with the
checkout; the '(primary only)' fallback becomes '(workspace only)'.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs: align primary/workspace vocabulary with the #2354 inversion (#2364 review F7)

The review flagged pre-inversion 'primary/non-primary' wording that
now misleads readers about the placement model: the isPrimaryBranch
JSDoc (field name kept — public API surface), the two branches? JSDoc
comments in local-backend, the base_ref gate comment in cli/analyze,
and four branch-scope test names. Comment/JSDoc/test-name edits only;
'Registry-primary' and 'primary key' senses untouched.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(cli): clarify workspace index status wording (#2364 review F8)

'gitnexus analyze follows this branch' was ambiguous about WHICH
branch analyze follows — the recorded one on the line or the current
checkout. Both locales now say a re-run follows the current branch.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(storage): re-read registry after the shadow rm in adoptFlatBranchLabel

The F2 reorder moved the registry read to the top of the function, so
the whole-file writeRegistry at the bottom persisted a snapshot taken
BEFORE the recursive rm of an entire sub-index — widening the unlocked
read-modify-write window from microseconds to the duration of a multi-
hundred-MB delete. A concurrent registerRepo/removeBranchIndex writer
in that window was silently clobbered (the #2106 R9 lost-update class;
registerRepo re-reads before writing for exactly this reason).

The top read is now a cheap membership gate only (the F2 no-op
guarantee); the mutate re-reads its own fresh snapshot after the rm.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix: treat only provably-absent errno as gone in the new existence probes

Both probes added by this series inverted the codebase's provably-
absent polarity (listRegisteredRepos validate prunes only on
ENOENT/ENOTDIR): adoptFlatBranchLabel's dirGone check read ANY
fs.access failure — including a transient EACCES/EIO on a surviving
dir — as 'verifiably gone' and dropped the summary, recreating exactly
the stranded-bloat bug F4 fixed; applyBranchScope's sub-index check
read the same transient errors on a healthy pinned lbug as 'adopted/
deleted', producing a false 'not indexed' error. A resolved force:true
rm now proves absence without a probe; on failure the probe treats
only ENOENT/ENOTDIR as gone, and a non-missing lbug serves the handle
so the pool open surfaces the real error.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(mcp): harden applyBranchScope stale-state coherence

Four residual gaps in the new arm structure, found by post-fix review:

- The stale-label error listed the just-contradicted cached label as
  indexed ('not indexed: main. Indexed branches: main'). The message
  now derives the flat label from the authoritative meta and excludes
  the requested branch from the hint list.
- A branch pinned AFTER the server cached its handle never triggered a
  refresh (resolve hits skip the miss-refresh), erroring until restart.
  Every miss now fires exactly one best-effort refreshRepos() before
  the error, so the next call resolves; a refresh-once guard keeps
  doubly-stale resolutions to a single registry re-scan.
- A registry entry claiming the branch both as flat label and pinned
  summary (the rm-failed adopt-degraded state) could serve the stale-
  vintage pin under a label the flat slot owns; the summary arm now
  requires handle.branch !== branch and the degraded state errors
  honestly.
- The flat-meta match path returned the cached handle's pre-restamp
  branch/commit/stats; the meta that decided routing now also supplies
  the metadata.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(core): keep the real error visible in restamp warnings; correct the end-of-run retry claim

The fast-path catch replaced the actual error with 'storage is
read-only (#1549)' for any EACCES/EPERM — mislabeling ownership
problems and transient Windows locks and discarding the only
diagnostic signal. The warning now carries the real message with the
#1549 hint appended.

The end-of-run best-effort comment claimed adopt 'retries
unconditionally on the next plain analyze'; same-commit runs take the
fast path whose guard compares the already-stamped meta label, so the
retry actually lands on the next content-changing run. The comment now
states the true retry semantics and why the interim state is safe
(flat meta stamped first; applyBranchScope trusts it).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test: unique tmpdir for the branch-scope fixture; drop redundant dynamic imports

The branch-scope describe materialized its sub-index stub under a
FIXED os.tmpdir()/gnx-2106-multi path — concurrent vitest runs on one
host (the documented parallel-agents workflow) could rm each other's
stub between beforeEach and the resolve under test, flaking the
pinned-branch tests. The fixture root is now mkdtemp-unique per run
with afterAll cleanup.

run-analyze.test.ts dynamically imported repo-manager inside test
bodies despite the module being statically imported at the top of the
file (no vi.mock exists there to justify it); the three call sites now
use the static import.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 20:55:27 +01:00
Gergő Magyar
0005574dce
docs: restructure root README, fact-check all READMEs (#2360)
* docs(readme): restructure for readability, fix stale facts

Reorganize the README so the visible page reads as a short narrative
(Quick Start -> Two Ways -> Why -> What Your Agent Gets -> Editor Setup
-> CLI -> How It Works -> Docker -> Enterprise) and move deep
operational detail into 13 collapsible <details> sections: env vars,
.gitnexusrc, Cosign/Kubernetes verification, manual MCP configs,
install troubleshooting, and extended tool examples.

Accuracy fixes verified against gitnexus/src:
- MCP tools: 17 (15 per-repo + 2 group), not 16/11+5; drop
  group_contracts/group_query/group_status (CLI + resources now, not
  tools); add check, trace, explain, pdg_query, route_map, tool_map,
  shape_check, api_impact rows from src/mcp/tools.ts
- Agent skills: 6 installed (adds Guide + CLI), not 4
- Wiki default model: minimax/minimax-m2.5, not gpt-4o-mini
- Resources: add gitnexus://setup and gitnexus://group/{name}/...
- CLI: document group impact, doctor, and the direct terminal query
  commands (query/context/impact/trace/cypher/detect-changes/check);
  note the optional branch param on per-repo tools (#2106)

Structural cleanups: dedupe the two Codex config blocks, move Community
Integrations out of the MCP setup flow, move Star History to the
bottom. No content deleted - verbose material is collapsed, not cut.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs: fact-check and fix the remaining READMEs

Reviewed all 9 tracked non-root READMEs against the source; fixed the
four with stale facts, left the already-accurate ones untouched
(pr-swarm-review, .claude reviewer-swarm adapter, and the three
bench/ methodology docs).

gitnexus/README.md (npm package page):
- MCP tools table: 17 tools (15 per-repo + 2 group), was 7 rows
- Resources: add gitnexus://setup and gitnexus://group/{name}/...
- Skills: 6 bundled (adds Guide + CLI) plus --skills generated ones
- Languages: add Dart (14 total) to the list and feature matrix
- Wiki default model: minimax/minimax-m2.5, not gpt-4o-mini
- Requirements: Node >= 22 (package.json engines), not >= 18
- Claude Code hooks: PreToolUse + PostToolUse
- CLI: add --skills/--skip-skills/--skip-git/--workers, doctor,
  trace, check, group impact
- Optional grammars note: include Proto, mention
  GITNEXUS_SKIP_OPTIONAL_GRAMMARS=1

gitnexus-cursor-integration/README.md:
- 17 MCP tools, was 16; skills list: all 9 bundled skills, was 5

eval/README.md:
- Model list matches configs/models/: Claude Haiku 4.5 (was
  '3.5 Haiku'), adds MiniMax M2.5 and DeepSeek
- Node.js 22+ for GitNexus, was 18+

.devcontainer/README.md:
- Add a table of contents (364 lines, ~15 sections, no navigation)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* chore: empty commit to retrigger CI

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 08:46:59 +01:00
Tanishq Khatri
fb40d15a16
Add Kilo Code + GitNexus MCP setup guide (#2259)
* docs: add Kilo Code MCP workflow

* Fixed Space

Fixed 1 deleted blank line

* Fixed Readme Link

Fixed guide link from docs to documentation folder fixing 404 error

* Added Image and linked to .md file

* Removed Trailing Spaces

---------

Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
2026-07-02 11:04:22 +01:00
Parafee41
a7df8f861a
fix(search): make FTS stemmer configurable (#2307)
Some checks are pending
CodeQL / Analyze (javascript-typescript) (push) Waiting to run
CodeQL / Analyze (python) (push) Waiting to run
Gitleaks / gitleaks (push) Waiting to run
Publish / Classify release event (push) Waiting to run
Publish / RC guard (marker + release-PR skip) (push) Blocked by required conditions
Publish / ci (push) Blocked by required conditions
Publish / Publish to npm (push) Blocked by required conditions
Publish / Build & Push RC Docker images (push) Blocked by required conditions
Scorecard / Scorecard analysis (push) Waiting to run
Trivy Image Scan / Trivy (gitnexus-cli) (push) Waiting to run
Trivy Image Scan / Trivy (gitnexus-web) (push) Waiting to run
2026-06-29 05:13:31 +01:00
Gergő Magyar
df08ecc397
perf(lbug): cut graph-DB emit/persistence wall time (#2203) (#2215)
* perf(lbug): add PROF_LBUG_LOAD persistence-path timing breakdown (#2203 U1)

loadGraphToLbug is un-timed today; the analyze 'emit' number is the
scope-resolution emit bucket, not the CSV->COPY persistence path. Add a
zero-cost-when-off per-stage breakdown (csv-emit/copy-nodes/rel-split/
copy-rels/fallback/total + node/rel counts) gated by PROF_LBUG_LOAD=1,
mirroring the PROF_SCOPE_RESOLUTION pattern. Document the flag in README.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* perf(lbug): route relationships to per-pair CSVs in the emit pass (#2203 U2)

Relationships were written once to a monolithic relations.csv, then re-read
line-by-line (regex per edge) and re-split into per-FROM->TO-label-pair files
before COPY — writing and reading the entire ~1M-edge set twice. Route each
edge to its pair file directly during the single emit pass via a shared
RelPairRouter, eliminating the monolithic write + re-read + per-edge regex.

The router applies the SAME getNodeLabel + validTables filter as the legacy
splitRelCsvByLabelPair, which is retained as a differential oracle. A new
differential test asserts the direct-emit per-pair files are byte-for-byte
identical to the oracle's, with identical skip/total accounting. The prof
line (U1) drops its rel-split stage (routing now folds into csv-emit).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* perf(lbug): skip per-row microtask tick in BufferedCSVWriter (#2203 U3)

addRow awaited an already-resolved promise on every buffered row, scheduling
a microtask per node even when nothing flushed (millions at scale). It now
returns a promise ONLY when it flushes; the node-emit loop awaits once per
iteration after the switch. Flush/drain semantics are unchanged, so
backpressure on the rows that actually write is preserved and the emitted
CSV bytes are byte-identical (covered by the determinism + differential tests).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* bench(lbug): emit throughput + byte-identity gate for the persistence path (#2203 U4)

Build-free bench (bench/emit-persistence/measure.mjs) times streamAllCSVsToDisk
on a synthetic graph at two scales and gates: (1) an order-independent sha256
fingerprint over every emitted CSV line — the byte-identity guard for the U2/U3
emit optimisations — and (2) a scaling-ratio budget catching an O(n^2) emit
re-regression. Wired into ci-tests.yml alongside the cfg/scope-capture benches.
The LadybugDB COPY half needs a real DB, so its timing stays in PROF_LBUG_LOAD
+ the integration round-trip tests (documented in the bench README, with the
deferred COPY-parallelism follow-up).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(review): apply autofix feedback (#2203)

- P1: router backpressure drain-await rejected with a generic AbortError,
  masking the real EMFILE/disk-full error. Expose RelPairRouter.lastError and
  rethrow it in the emit catch — mirrors the oracle's throw streamError ?? err.
- P1: cover RelPairRouter error + backpressure + teardown paths with a new
  unit test (test/unit/rel-pair-routing.test.ts) using an injected mock stream.
- P2: wrap streamAllCSVsToDisk body in try/finally so the setMaxListeners bump
  is always restored (the U2 rel-routing throw path could leak it).
- P2: dedup WriteStreamFactory — re-export the canonical type from
  rel-pair-routing instead of a second identical declaration.
- P2: annotate splitRelCsvByLabelPair @internal as the retained differential
  oracle so a future dead-code sweep doesn't delete the byte-identity guard.
- P3: differential test now covers the proc_ prefix + clears
  GITNEXUS_SORT_GRAPH_OUTPUT to prevent env-leak desync.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(lbug): scope byte-identity to quote-free ids + lock the quote-in-id divergence (#2215 review)

The 'byte-identical' claim was unconditional, but the router derives labels from
the raw id while the retained splitRelCsvByLabelPair oracle re-derives them via a
regex over the escaped row — so for an id containing a double-quote they diverge
(the router is the more-correct path). Soften the wording in rel-pair-routing.ts,
the bench README, and the differential-test comment to document the exception,
and add a differential test asserting the intended divergence (router routes the
quote-in-id edge; oracle drops it) so a future change can't silently revert to
the buggy regex semantics.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* bench(lbug): per-file fingerprint so the gate catches pair-file mis-routing (#2215 review)

fingerprintEmit flattened every line of every per-pair file into one array,
sorted globally, and hashed — losing file boundaries, so a row routed to the
WRONG pair file produced an identical fingerprint. Hash a per-file digest
(filename + sha256(file bytes)) and combine the sorted entry list, so mis-routing
(and within-file row reordering) now changes the fingerprint. Baseline
regenerated; the new scheme yields a different hash on byte-identical emit,
confirming it is sensitive to file structure the old flatten ignored.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* bench(lbug): add absolute large-scale wall-time backstop to the emit gate (#2215 review)

The scaling-ratio gate only compares large/small, so a uniform Nx slowdown at
both scales passes with ratio ~1.0. Add an opt-in max_ms_large ceiling (1000ms
vs observed ~200ms — generous, host-noise-tolerant) that --check enforces
alongside the ratio, catching a gross absolute regression the ratio misses.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(lbug): cover the sorted-output path in the byte-identity differential (#2215 review)

The differential test only exercised the default insertion-order emit path. Add
a case under GITNEXUS_SORT_GRAPH_OUTPUT=1 that feeds the oracle the same
id-sorted order orderedRelationships() uses and asserts per-pair byte-identity,
so within-pair row reordering on the sorted path can't slip past the gate.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(lbug): cover the invalid-TO-label skip branch (#2215 review)

Only an invalid-FROM label was exercised; the validTables skip is an OR over
both endpoints, so the invalid-TO branch was untested (an inverted && would
have slipped through). Add a valid-FROM/invalid-TO edge to the differential
test and the router unit test, asserting it's skipped identically by router and
oracle.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(lbug): exercise the BufferedCSVWriter FLUSH_EVERY boundary in vitest (#2215 review)

The U3 addRow change (returns a flush promise only on flush; undefined when
buffered) and the loop's `if (pending) await pending` were only crossed by the
bench, never vitest (all fixtures are <500 nodes). Add a 600-node graph through
streamAllCSVsToDisk asserting all rows land exactly once across the 500-row
flush boundary — no drops, dups, or corruption.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(lbug): drop redundant step cast in buildRelRow (#2215 review)

GraphRelationship.step is already typed number?, so (rel as { step?: number }).step
was a no-op structural cast that obscured the shared-type coupling. Use rel.step
directly. Byte-identical — bench fingerprint unchanged, differential test green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(lbug): make the unknown-label node drop explicit (#2215 review)

With the U3 `let pending` switch idiom, a node whose label matches neither
codeWriterMap nor multiLangWriters left `pending` undefined and was silently
dropped — a footgun for a future node type. Add an explicit else with a comment
documenting that unknown labels are intentionally not persisted and that a new
type must be wired into a writer map. No behavior change (byte-identity + tests
unchanged).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(lbug): drop the unused WriteStreamFactory re-export (#2215 review)

The type was re-exported from lbug-adapter 'to preserve this module's surface,'
but no external code imports it by name from here (the only test reference is a
comment). Keep the import from rel-pair-routing.ts (its canonical home, still
used by splitRelCsvByLabelPair's signature) and drop the dead re-export.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 19:40:59 +01:00
Gergő Magyar
9ff7337f1e
fix(mcp): rename query/cypher params so Claude Code can call them (#2186)
* fix(mcp): advertise search_query/statement params for query/cypher tools (#2175)

Claude Code drops a tool-call argument named exactly 'query', making the
query and cypher tools unusable from it. Rename the advertised required
parameters to search_query and statement so the client transmits them.
Handler-side backward-compat for the legacy 'query' key follows in the
next commit.

* fix(mcp): accept search_query/statement with legacy query fallback (#2175)

Resolve the new advertised param names in the backend while still accepting
the legacy 'query' key, so curl/HTTP, other MCP clients, the CLI, the group
path, and the internal executeCypher() all keep working. Alias is normalized
once at the callTool chokepoint (covers group-forward + search alias); query()
and cypher() dual-read defensively. New name wins when both are supplied.
Updates the required-error message and adds dual-accept unit + integration
coverage.

* fix(cli): pass canonical search_query/statement params to query/cypher tools (#2175)

Stop the CLI from depending on the deprecated 'query' alias. No user-facing
change — the positional args are unchanged and the backend accepts both keys.

* fix(mcp): generators advertise search_query in query() examples (#2175)

Update the three doc/example generators (ai-context AGENTS/CLAUDE block,
skill-gen community skills, resources repo hint) so future analyze runs emit
query({search_query: ...}) — the param name Claude Code actually transmits.
Tests assert the new form is present and the legacy query({query: form is
absent (the #2059 generator-test pattern).

* docs(mcp): advertise search_query/statement in skill & guidance examples (#2175)

Sync the committed agent-facing docs to the renamed params so a Claude Code
agent following them emits the transmittable key: AGENTS.md/CLAUDE.md gitnexus
block, the canonical gitnexus/skills/* source and its installed/plugin/cursor
mirrors, and the README examples. Scoped rewrite of the two call prefixes only
(query({query: -> search_query, cypher({query: -> statement).

* style(mcp): prettier line-wrap for #2175 alias-resolution edits

* fix(review): uniform search_query precedence + cypher empty guard (#2175)

Code-review findings (correctness/adversarial/api-contract/maintainability
consensus):
- Group-mode query inverted the 'new name wins' rule: the callTool chokepoint
  backfilled params.query only when empty and the @group-forward read
  params.query directly, so a both-keys (or whitespace-legacy) group call let
  the legacy value win — unlike the local path. Replace the hidden param
  mutation with a self-contained 'search_query ?? query' resolve at the
  group-forward; precedence is now uniformly new-wins at every consumer site.
- cypher() now returns the same friendly required-param error as query() when
  neither statement nor query is supplied, instead of a raw DB prepare error.
- Document the legacy alias as permanent (third-party clients may send query=).
Adds group-forward alias tests (both-keys + legacy-only), empty/whitespace
search_query, the search-alias path, and the cypher empty-statement guard.

* fix(review): non-string alias safety + drop stale chokepoint comment (#2175)

Tri-review findings (correctness/adversarial/security + maintainability):
- Non-string statement/search_query/query (the MCP envelope is not
  schema-validated) hit .trim() and threw TypeError to the server boundary
  instead of a friendly required-param error. Introduce resolveAliasString()
  (new name wins; non-string -> undefined) used by query(), cypher(), and the
  group-forward, so all three return the structured error. Empirically verified
  (123 ?? '' -> 123, (123).trim() throws) — this overrides a critic refutation
  that mis-read ?? as a string coercion.
- Remove the stale query() comment claiming alias resolution happens at a
  callTool chokepoint; that mutation was removed earlier in this PR — each site
  resolves the alias itself.
- Document GroupToolPort.query's intentionally-narrower required type vs the
  wider LocalBackend impl.
Adds non-string and empty-new-key precedence tests.

* fix(mcp): alias falls back to legacy value when new key is blank (#2175)

PR #2186 review finding: resolveAliasString used `canonical ?? legacy`
(nullish), so an explicitly empty/whitespace new-name value (e.g.
{search_query:'', query:'real'}) won and was rejected — discarding a valid
legacy value, contradicting the 'new name wins when both supplied' intent.
Resolve to the first NON-BLANK string instead (new preferred when it carries
a real value, else legacy). Covers query(), cypher(), and the group-forward
(all route through the helper); non-string still resolves to a friendly error.
Flips the presence-based test and adds whitespace/cypher/group fallback cases.

* fix(mcp): drop legacy "query" mention from query/cypher schema descriptions (#2175)

PR #2186 review finding: the search_query/statement inputSchema descriptions
named the legacy "query" key — the exact arg Claude Code drops — and
description text is read by an LLM choosing arguments, weakly nudging it to
send "query". Trim the descriptions to their clean form and move the
legacy-alias note to a code comment next to the schema (preserved for
maintainers / non-CC clients). properties/required unchanged (no `query`).
2026-06-13 10:24:16 +01:00
azizur100389
50cda61a43
feat(setup): select coding agent integrations (#2168)
* feat(setup): select coding agent integrations

* style: format setup agent selection

* fix(setup): validate explicit agent selection

---------

Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
2026-06-13 08:06:50 +01:00
Gergő Magyar
5bf8a17cd5
feat(ingestion): add control-flow-graph layer for TS/JS (#2081) (#2099)
* feat(cfg): language-agnostic CFG construction core (#2081)

U1 of M1 (CFG layer). Plain JSON-serializable CFG data model (BasicBlockData/
CfgEdgeData/FunctionCfg — must survive the worker→main boundary + ParsedFile
store), a CfgBuilder accumulator (leaders→blocks→edges, synthetic ENTRY/EXIT,
idempotent edges), a ControlFlowContext (break/continue/switch + labeled-jump
target stacks), and a TraversalResult ({entry, dangling exits}). AST-agnostic
and unit-tested on the classic control-flow topologies (if/else, while back-edge,
mid-block return, labeled break/continue) the S2 spike validated; reachability
helper backs the R9 property test.

* feat(ingestion): U2 — TS/JS CFG visitor over tree-sitter AST (#2081)

Add the TS/JS CfgVisitor that walks a function's tree-sitter AST and drives
the U1 CfgBuilder to produce a serializable FunctionCfg. One visitor covers
both languages (shared grammar family).

Handles the classic CFG hazards explicitly (R2, R10):
- loops allocate a dedicated loop-exit block so `break` has a concrete target
  before the loop's successor is known; `continue`/back-edge close the loop
  (while, do-while, C-for with init-once + increment-as-continue-target,
  for-in, for-of)
- switch fallthrough falls out naturally: a non-breaking case yields exits we
  wire to the next case as `fallthrough`; a breaking case wires to the switch
  exit via ControlFlowContext
- try/catch/finally: normal completion AND exceptional flow both route through
  finally (post-domination); a conservative exceptional edge models that the
  protected region may raise to its handler (not just explicit `throw`)
- labeled break/continue resolve against the labeled loop's frame
- early return/throw wire to EXIT/handler and terminate their block

19 hazard tests (one per construct) + AC1 10-function fixture; all green.
No change to the committed U1 core or ControlFlowContext.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(ingestion): U3 — worker CFG build + cfgSideChannel + cache coherence (#2081)

Run the CFG visitor in the parse worker (where the AST lives), serialize the
per-function CFG onto a new ParsedFile.cfgSideChannel, and keep it coherent
across the disk-backed store and the warm/durable parse cache (R3, R4).

- gitnexus-shared parsed-file.ts: add `cfgSideChannel?: unknown` as a DISTINCT
  field from captureSideChannel (different producer/consumer/lifecycle; plain
  JSON data — blocks/edges deliberately lack the `nodeId` the store's interning
  reviver keys on, so no mis-interning).
- cfg/types.ts + visitors/typescript.ts: add CfgVisitor.isFunction so the worker
  enumerates functions (and applies the line budget) by a cheap node-type test.
- cfg/collect.ts (new): collectFunctionCfgs walks the tree, builds one CFG per
  function (nested included), applies maxFunctionLines (over-cap = skipped).
- language-provider.ts: add `cfgVisitor?: CfgVisitor<SyntaxNode>` hook;
  typescript.ts attaches it to both the TS and JS providers (shared grammar).
- parse-worker.ts: read pdg + pdgMaxFunctionLines from workerData (read once at
  init — the worker never sees PipelineOptions), gate the build, attach
  cfgSideChannel alongside captureSideChannel.
- parse-cache.ts: bump SCHEMA_BUMP 4→5 (ParsedFile shape changed) and fold the
  pdg flag into computeChunkHash so a pdg-off cached chunk is NOT reused on a
  --pdg run (the #2038-class warm-cache trap). Default path keeps its keys.
- worker-pool.ts + parse-impl.ts + pipeline.ts: thread pdg/pdgMaxFunctionLines
  PipelineOptions → WorkerPoolOptions → workerData, and into the chunk-hash key.

9 boundary tests: collect contract, JSON round-trip identity (no AST leakage),
the pdg cache-key guard, the line-cap skip, and the no-visitor gate. Full CFG
suite (U1+U2+U3) green; build clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(ingestion): U4 — emit BasicBlock + CFG within scope-resolution (#2081)

Emit persisted BasicBlock nodes + CFG edges from each ParsedFile's worker-built
cfgSideChannel, INSIDE scope-resolution's Phase-4 graph emission — the last
point where the worker-built CFGs are loaded (emitParsedFiles carries the
channel; the disk store is cleared right after the orchestrator returns). This
is the architecture the doc-review corrected to: a standalone post-`mro` phase
(the issue's literal subtask) provably reads empty data (KTD1).

- cfg/emit.ts (new): pure emitFileCfgs(graph, cfgs, maxEdgesPerFunction, onWarn).
  BasicBlock id = `BasicBlock:<filePath>:<functionStartLine>:<blockIndex>`
  (KTD3 — funcStart disambiguates blocks across functions in one file; no
  `name` column). CFG edge = CodeRelation type 'CFG' with the edge KIND
  (seq/cond-true/…) in `reason` (kinds can't be their own edge type). Per-
  function edge cap stops at the cap and warns with the dropped count — no
  silent truncation (R6/KTD6).
- run.ts: pdg-gated emit pass over emitParsedFiles after emitPostResolutionEdges
  (store still live); RunScopeResolutionInput gains pdg + pdgMaxEdgesPerFunction.
- phase.ts: thread ctx.options.pdg / pdgMaxEdgesPerFunction into the call.
- pipeline.ts: PipelineOptions.pdgMaxEdgesPerFunction.

6 tests: node/edge shape (KTD3 id, no name, type='CFG', kind in reason),
cross-function id uniqueness, AC2 reachability-from-ENTRY property, the edge
cap's no-silent-truncation contract, and empty-input no-op. Flag-off
byte-identity + full runPipelineFromRepo round-trip land in U7. Build clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(cli): U5 — `--pdg` opt-in plumbing (CLI + .gitnexusrc → both sinks) (#2081)

Expose the CFG/PDG substrate as an opt-in and thread it from CLI/.gitnexusrc to
the single source of truth (PipelineOptions.pdg), which fans out to BOTH sinks
already wired in U3/U4: the worker build gate (workerData.pdg) and the
scope-resolution emit gate. Off by default (R7).

- cli/index.ts: `--pdg` commander flag.
- cli/analyze.ts: AnalyzeOptions.pdg + pass `pdg` into runFullAnalysis options.
- cli/analyze-config.ts: KEY_SPECS `pdg` (boolean) so `.gitnexusrc { "pdg": true }`
  normalizes and a non-boolean value fails closed with GitNexusRcError.
- core/run-analyze.ts: AnalyzeOptions.pdg → runPipelineFromRepo({ pdg }).

(The internal PipelineOptions/WorkerPoolOptions/workerData fields + the
parse-cache key fold landed in U3/U4; this unit adds the user-facing surface.
The budget knobs stay at internal defaults for M1.)

Tests: analyze-config pdg normalization + non-boolean rejection; opt-in.test.ts
covers the CLI/file merge precedence and that pdg perturbs the chunk-dispatch
key. The full worker-build + main-emit round-trip is the U7 integration test.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(ingestion): U7 — CFG acceptance fixtures, parity, end-to-end + docs (#2081)

Acceptance criteria for the M1 CFG layer:
- AC1: a 10-function TS fixture's CFG node/edge set matches a committed snapshot
  (cfg-snapshot.test.ts).
- AC2: every BasicBlock is reachable from its function ENTRY (property test over
  the emitted graph; the fixture has no dead code).
- AC3: hazard fixtures lock the classic-bug coverage — try/throw/finally
  post-domination + labeled break/continue resolution.
- AC4: the existing pipeline-graph-golden test stays byte-identical with --pdg
  off (verified; no UPDATE_GOLDEN), proving the opt-in adds zero default-run
  drift.
- End-to-end (pipeline-pdg.test.ts): runPipelineFromRepo({ pdg: true }) on a
  tiny repo emits BasicBlock nodes + CFG edges with both endpoints present —
  the true both-sinks proof (worker builds → store → scope-resolution emits);
  the default run emits zero.

Docs: CHANGELOG M1 entry, ARCHITECTURE "Optional CFG/PDG emission" subsection
(why emit is in-phase, not post-mro), README CFG language-support note.

Full CFG suite (U1–U7): 56 tests green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(ingestion): drop unused helper in cfg-snapshot test (#2081)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(review): apply ce-code-review autofix feedback (#2081)

Review (10 reviewers) confirmed OFF-path byte-identity (adversarial + golden)
and found defects all within the --pdg path. Fixes:

- P1 same-line BasicBlock id collision: add a start-column disambiguator to
  FunctionCfg + the id (`BasicBlock:<file>:<line>:<col>:<idx>`) so two functions
  sharing a start line no longer collide under first-writer-wins addNode.
- P1 worker crash-cascade: per-file try/catch around collectFunctionCfgs so a
  CFG-build throw cannot escape to the language-group catch and silently drop
  every remaining file in the group.
- P2 edge-cap drop now logs unconditionally (input.onWarn is validator-gated/
  silent in prod) — upholds the no-silent-truncation guarantee.
- P2 Array.isArray guard before the cfgSideChannel cast in run.ts.
- P2 maxFunctionLines default: worker applies DEFAULT_PDG_MAX_FUNCTION_LINES=2000
  when unset; caps forwarded through run-analyze AnalyzeOptions (closes the
  server-path drop).
- P3 README duplicate paragraph removed; `0`-vs-default docstrings corrected;
  CLI --pdg flag made language-neutral; reachableBlocks JSDoc corrected.
- Documented the break-through-finally + stacked-label CFG limitations.
- Tests: same-line id-collision regression, standalone throw→EXIT, dead-code-
  after-return, async/generator/method coverage, strengthened labeled-continue.

Refuted: the HTTP-500 getNodeQuery finding — M0 already shipped the BasicBlock
branch + name-floor (R12/web-safety handled).

CFG + analyze-config suites: 95 tests green; golden parity (AC4) byte-identical.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* perf(ingestion): benchmark CFG construction + O(n) block-text accumulation (#2081)

Closes the M1 review's requires_verification perf gap ("no benchmark for
collectFunctionCfgs; a wall-time + cfgSideChannel byte-size regression gate
would catch the extendBlock concatenation before kernel scale").

- bench/cfg/measure.mjs (new): build-free tsx harness timing collectFunctionCfgs
  (parse once, reuse the tree) across three scaling scenarios — straight-line
  (extendBlock path), many-functions (collect walk), branchy (block/edge growth)
  — at 500→2000. Reports a wall-time scaling ratio AND a cfgSideChannel
  byte-size ratio, plus an order-independent sha256 over the emitted blocks/edges
  as the behavior gate. `--check` compares both ratios + the fingerprint against
  bench/cfg/baselines.json; mirrors the scope-capture / python-scope harnesses.
- .github/workflows/ci-tests.yml: run the gate on every test job (build-free,
  alongside the existing scope-capture guards) so an O(n^2) re-regression fails CI.
- cfg-builder.ts: structural fix for the one real hotspot the bench surfaced —
  accumulate basic-block text as fragments joined once in finish(), instead of
  concatenating onto a growing string per coalesced statement (O(n^2) → O(n)).
  Behavior-identical (the CFG fingerprint + the AC1 snapshot are unchanged).

Measured (post-fix): time ratios straight-line ~1.3, many-functions ~1.0,
branchy ~1.1 (all sub-quadratic; a true O(n^2) would be ~4.0). cfgSideChannel
bytes scale linearly (~1.0-1.04). 60 CFG tests green; build clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* perf(ingestion): add memory + disk growth gates to the CFG benchmark (#2081)

Extend bench/cfg/measure.mjs beyond wall-time to the two other scalability
dimensions that matter at kernel scale:

- DISK growth: utf8 byte size of the serialized cfgSideChannel — exactly what a
  --pdg run writes onto every ParsedFile shard (durable store + parse cache).
- MEMORY growth: retained JS heap of the cfgSideChannel payload, measured by the
  release-delta method (heap held minus heap after dropping it) — robust to
  pre-existing garbage and dead-stable run-to-run. Needs `node --expose-gc`;
  without it the heap metric is null and its gate is skipped (local runs still
  work). ci-tests.yml now passes --expose-gc so the heap gate runs in CI.

Both gated on linear scaling in baselines.json (disk_bytes_budget / heap_budget
1.2-1.3). Measured: disk ~1.0-1.04, retained heap ~0.87-1.0 — both linear
(~1KB/function each; ~2MB heap / 1.6MB disk at 2000 functions, --pdg only).
Bumped REPS 7->15 to stabilize the noisier time signal and widened the coarse
time tripwire budgets (the disk/heap gates carry the tight regression detection).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(ingestion): address tri-review + CFG-expert findings (#2081)

Corroborated findings from the tri-review (Codex + CE personas + GitNexus swarm
+ a CFG/program-analysis domain-expert lane). The OFF-path stays byte-identical;
all fixes are within the --pdg path or the benchmark.

- [Codex+CFG-expert] Exceptional `throw` edges now wire EVERY block in a try's
  protected region to the handler, not just the body ENTRY. A branched try body
  (`try { if (x) { use(t); } } catch`) previously left interior blocks with no
  path to `catch` — a taint false-negative into the handler for the M2 PDG pass.
- [Codex+CFG-expert] An unresolved labeled jump (a stacked outer label or a
  labeled non-loop block) now routes to the function EXIT instead of leaving a
  dangling sink — restores the single-exit invariant post-dominator/PDG
  computation needs.
- [Codex] computeChunkHash now folds pdgMaxFunctionLines/pdgMaxEdgesPerFunction
  into the chunk key (not just the pdg boolean), so a warm cache built under one
  cap is never served to a run with a different cap (#2038 class, extended to
  the budgets). Adds PdgCacheKey; boolean form kept for back-compat.
- [perf] visitTry resolves catch/finally in a single namedChild pass (the double
  `namedChildren.find` allocated two throwaway arrays).
- [adversarial] The bench `straight-line` scenario now runs at 2000->8000:
  output is a constant 4 blocks so disk/heap can't see the concat path, and at
  the old N a genuine O(n²) was masked by V8 cons-strings. Verified at the new N:
  the array-join impl ~1.0, a rope-optimized `+=` ~1.0 (correctly not flagged),
  a real O(n²) (re-join-every-append) ~3.8 — budget tightened 2.0->1.5.
- [adversarial+Codex] The bench `--check` now FAILS LOUDLY when run without
  `--expose-gc` instead of silently skipping the retained-heap gate.
- Doc: re-labeled the finally-bypass as a SOUNDNESS (false-negative) limitation
  tracked for M2, not mere "precision."

3 new regression tests (branched-try interior→handler, stacked-label→EXIT,
cap-fold key). 99 CFG tests pass; build clean; bench gate green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(parse-cache): clarify that SCHEMA_BUMP still invalidates caches once (#2099 F6)

The computeChunkHash comment claimed pdg-off warm caches "survive this
change untouched" — true for the key FORMAT, but misleading as an
upgrade-behavior promise: SCHEMA_BUMP 4→5 changes PARSE_CACHE_VERSION
and both stores hard-invalidate on it. Separate the two facts so the
next cache change isn't reasoned about from a false premise.

Review finding F6 (P3) of PR #2099 tri-review.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(cfg): correct for-loop back-edge kinds when no increment clause (#2099 F5)

A for with a body but no increment emitted an unconditional
header→header 'loop-back' self-edge (a path that never executes the
body) while the real back-edge body→header was labeled 'seq'. Any
consumer identifying loops via reason='loop-back' picked the phantom
edge and excluded the body from the natural loop.

Gate the self-edge on the body being absent (the one case where the
header genuinely re-tests itself) and carry 'loop-back' on the body's
exits when they ARE the back-edge, matching visitWhile/visitForIn.

Review finding F5 (P3) of PR #2099 tri-review.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(cfg): treat an empty catch clause as a real handler (#2099 F2)

visitTry keyed handler semantics off the traversal result — null for an
empty body, since visitSeq([]) returns null — instead of the syntactic
clause. An empty `catch {}` was therefore treated as NO catch: the
swallowed exception escaped to the outer handler/EXIT, the no-catch
re-propagation misfired past finally, and code after a try whose body
always throws became unreachable from ENTRY — a hard false-negative
source for the M2 taint pass, on an extremely common pattern.

Synthesize one empty block spanning the clause (entry == sole exit)
when the catch body traverses to null, before the protected region is
walked. Exception flow lands in it and rejoins the normal continuation;
all downstream wiring (handler selection, finally routing, the !catchRes
re-propagation gate) operates on the syntactically-correct shape.

Review finding F2 (P2, reproduced) of PR #2099 tri-review.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(cfg): guard CFG emission per element, not just per outer array (#2099 F4)

The cfgSideChannel guard checked only Array.isArray before casting to
FunctionCfg[] — its own comment promised a wrong-shape value would
'skip emission, not throw a TypeError mid-graph-build', but a malformed
ELEMENT sailed through. Worse, the obvious-looking failure shape never
throws at all: emitFileCfgs string-templates any edge endpoint into the
BasicBlock id and graph inserts are no-throw, so a non-integer endpoint
silently became a dangling 'BasicBlock:…:undefined' edge that degrades
the DB rel-pair COPY to row-by-row fallback inserts much later.

Layered fix matching house precedents (parsedfile-store reviver,
worker-side per-file catch): a per-element shape+content predicate
(arrays + integer edge endpoints) that warns and skips malformed
elements while valid siblings still emit, plus a per-file try/catch
backstop for shapes that genuinely throw (e.g. a null inside blocks).

Review finding F4 (P3) of PR #2099 tri-review.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(parse-cache): drop emit-time edge cap from the pdg chunk key (#2099 F3)

pdgMaxEdgesPerFunction is applied exclusively in emitFileCfgs during
scope-resolution on the main thread — the worker never receives it
(workerData carries only pdg + pdgMaxFunctionLines), so the cached
worker output is byte-identical across cap values. Folding it into the
chunk key (added by a prior review round) only converted a free knob
into a repo-sized cost: every cap change forced a full re-parse and a
durable-store rewrite of unchanged data.

Keep pdg + maxFunctionLines (genuinely worker-visible, shape the cached
cfgSideChannel) and document the classification test in the PdgCacheKey
doc comment so the next option gets sorted deliberately: worker-shard
inputs go in this key; persisted-graph-only inputs belong in the
RepoMeta pdg stamp (F1). Chunks written under the old ns string miss
once and prune — no migration needed.

Review finding F3 (P2) of PR #2099 tri-review.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(analyze): record pdg config in RepoMeta; force full writeback on mode flip (#2099 F1)

Running --pdg against an already-indexed repo silently persisted ~zero
CFG: incremental eligibility had no pdg term, RepoMeta recorded no
mode, and extractChangedSubgraph keeps only changed-file nodes — on a
no-change --pdg re-run every freshly built BasicBlock was dropped from
the written subgraph ('Incremental: changed=0', run succeeds, zero
rows). The converse flip left zombie mixed-coverage blocks only --force
could clean. Worse, a clean-tree flip hit the alreadyUpToDate fast path
and never ran the pipeline at all.

- RepoMeta gains an additive-optional pdg stamp ({maxFunctionLines,
  maxEdgesPerFunction}, resolved values; absent ≡ pdg-off, which covers
  every legacy meta). No INCREMENTAL_SCHEMA_VERSION bump — that would
  force a one-time full rebuild for everyone. The end-of-run meta is a
  fresh literal, so omitting the field on a pdg-off run is what clears
  the stamp after an on→off flip.
- pdgModeMismatch (pure, exported) compares the resolved triple; the
  flip check sits before the fast path and always logs its notice (not
  gated on options.force — --skills implies force with no message of
  its own), naming the .gitnexusrc pdg key that pins the mode.
- The full-rebuild branch now writes the incrementalInProgress dirty
  flag (toWriteCount: 0 sentinel) before the wipe whenever a prior meta
  exists, mirroring the incremental branch. This closes the crash
  window where a rebuild dying between the bulk load and saveMeta left
  meta/DB inconsistent and the fast path certified zombie (or missing)
  CFG rows indefinitely — and incidentally closes the same pre-existing
  hole for user --force runs. Recovery log reworded accordingly.

Tests: pdg-mode-flip.test.ts (real git + LadybugDB; primary assertion
is a direct BasicBlock table count — meta.stats aggregates
nondeterministic Community/Process rows) covering off→on, steady-state
fast path, on→off zombie cleanup, cap-change rebuild, and dirty-flag +
flip composition; pure-helper tests for default resolution and the
0=unlimited carve-out.

Review finding F1 (P1) of PR #2099 tri-review.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 19:26:45 +01:00
Gergő Magyar
4682a477d8
feat(mcp): paginate list_repos to avoid client token truncation (#2119) (#2120)
* feat(mcp): paginate list_repos to avoid client token truncation (#2119)

list_repos returned every indexed repository in one unpaginated array,
which large/LLM MCP clients truncate by token limit — so agents with
hundreds of indexed repos could not enumerate them all (the data
transmits fully; the consuming client drops it).

Add bounded limit/offset pagination to the list_repos tool:
- result changes from a bare array to
  { repositories, pagination: { total, limit, offset, returned,
  hasMore, nextOffset } }; default page 50, max 200 (shared constants)
- reject malformed limit/offset; clamp limit above the max
- deterministic order (lower-cased name, then path) over one registry
  snapshot per call, so paging never skips or duplicates an entry
- covers both stdio and remote /api/mcp (shared createMCPServer/callTool)

The internal listRepos() method (5 callers), GET /api/repos, and the
`gitnexus list` CLI are unchanged. The array->object tool-result shape
is a deliberate contract change, documented in CHANGELOG.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(mcp): reject list_repos limit above the max instead of clamping (#2119)

parseListReposPagination silently clamped limit>max to the maximum while
throwing on every other out-of-bounds value (limit<1, offset<0, non-integer,
NaN). A client that advanced offset by its requested limit (rather than
pagination.nextOffset) then silently skipped repositories and saw
hasMore:false — defeating the "never skips" guarantee. Reject an over-max
limit too, so validation is symmetric and a caller never gets a smaller page
than it asked for without a clear error. Updates the schema/description, the
helper + ListReposPagination JSDoc, the guide note, and the two clamp tests.

Resolves the cross-engine-corroborated P2 (Codex + adversarial lane) and the
maintainability lane's clamp-vs-throw inconsistency from the PR #2120 review.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(mcp): name the list_repos return type and mark the parser @internal

Extract the inline listRepos() element shape into an exported RepoListing
interface and use it for both listRepos() and listReposPage().repositories,
replacing the opaque Awaited<ReturnType<LocalBackend['listRepos']>> expression
the maintainability review flagged. Tag parseListReposPagination @internal
(it is exported only for unit testing). Pure type/JSDoc change; no behavior.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(eval-server): type formatListReposResult to the paginated shape

Narrow formatListReposResult's parameter from `any` to
{ repositories: RepoListing[]; pagination?: ListReposPagination } and drop the
dead bare-array branch — after #2119 callTool('list_repos') always returns the
paginated object, so the Array.isArray shim was unreachable. Add a list_repos
continuation hint to the eval-server's getNextStepHint (parity with the MCP
server), and cover the previously-untested non-empty + hasMore:false formatter
branch. Migrates the two bare-array formatter tests to the object shape.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(mcp): harden list_repos pagination coverage

- Exercise the #2054 sibling-clone guarantee through the real callTool tool
  path (in the #2054 describe, which has temp-dir cleanup), proving siblings
  and remoteUrl survive listReposPage's sort+slice — not only listRepos().
- Assert total + limit on the middle-page test (a total miscalculation at a
  non-zero offset would otherwise slip past it).
- Cover the benign boundaries: negative-zero offset (accepted as page 0) and a
  MAX_SAFE_INTEGER offset (empty page).
- Replace the integration test's '\n\n---' split with a string-aware brace
  scan, so a repo path containing braces can never truncate the JSON parse.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(skills): sync the list_repos pagination example to the guide mirrors

The .claude and gitnexus-claude-plugin guide mirrors only carried the one-line
table note; add the full "Paginating list_repos" section (shape + multi-page
traversal example + notes) so all three guide copies are byte-consistent with
the canonical gitnexus/skills/gitnexus-guide.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* chore: drop list_repos CHANGELOG entries from this PR

Restore gitnexus/CHANGELOG.md to match main so this PR contributes no
changelog change; the changelog is curated separately from feature PRs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 19:59:54 +01:00
Gergő Magyar
cef63dd044
feat(install): toolchain-free tree-sitter via vendored prebuilds (#2113)
* feat(install): toolchain-free tree-sitter via vendored GitNexus-built prebuilds

Eliminate the C/C++-toolchain requirement at install for the at-risk grammars
(dart, proto, kotlin) by generating + vendoring native prebuilds, mirroring the
existing vendored tree-sitter-swift. The 10 grammars that already ship 6 upstream
prebuilds stay npm dependencies (toolchain-free AND dependency-review-tracked).

- .github/workflows/build-tree-sitter-prebuilds.yml: a registry-parameterized
  workflow that builds {dart,proto,kotlin} x {linux,darwin,win32}-{x64,arm64}
  prebuilds natively, validates each loads + parses on its arch, and opens a PR
  vendoring them. A `guard` job gates the heavy matrix to run ONLY on dispatch
  or a real grammar-version change — ordinary code PRs cost zero matrix minutes.
- dart/proto: prefer a committed prebuild; fall back to today's source build
  when none matches (no behavior change until prebuilds are vendored).
- kotlin: vendor it (Swift parity) instead of compiling the third-party
  optionalDependency from source at the user's install — supersedes #2110's
  optionalDependency mechanism. The ~23 MB parser.c is NOT vendored (the
  workflow builds from the published package); only node-types + bindings +
  prebuilds are. Removed from optionalDependencies; lock regenerated; probe,
  parser-loader note, README/.devcontainer docs, and the #2110 tests updated.

DO NOT MERGE until vendor/tree-sitter-kotlin/prebuilds/ is populated by the
build-tree-sitter-prebuilds workflow: until then Kotlin is unavailable (vendored
with no source-build fallback). dart/proto remain fully functional throughout.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(install): guard 6/6 N-API prebuild coverage for every grammar

Regression guard so a toolchain-less install can never silently lose a tree-sitter
language on a supported platform-arch:

- Vendored grammars (vendor/tree-sitter-*): every one MUST ship a loadable N-API
  prebuild for all 6 tuples {linux,darwin,win32}-{x64,arm64}. Asserts the
  napi_register_module_v1 entry symbol in each .node (cross-platform, no need to
  run the binary). Currently RED for dart/proto/kotlin until the
  build-tree-sitter-prebuilds workflow populates their prebuilds/ — this is the
  must-fill-before-merge gate (swift already passes 6/6).
- npm-dependency grammars: asserts upstream ships 6/6 N-API too, catching a
  future platform drop. tree-sitter-c is allow-listed at 4/6 (missing
  linux-arm64/win32-arm64) pending #2116; the guard also fails if that gap is
  silently closed (prompting allow-list removal).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(install): vendor tree-sitter-c at 0.21.4 with GitNexus-built prebuilds (#2116)

tree-sitter-c is the one grammar dependency upstream ships incomplete prebuilds
for (4/6 — no linux-arm64/win32-arm64), AND it is a REQUIRED grammar: its own
`install` (node-gyp-build) compiles from source when no prebuild matches and
exits non-zero, so on a toolchain-less ARM host `npm install gitnexus` HARD-FAILS
at the c step — during npm's dependency phase, before any GitNexus postinstall
runs (so a postinstall "supplement" can't help).

Fix: vendor c prebuild-only at the pinned 0.21.4 (Kotlin pattern), with all six
prebuilds GitNexus-cross-built, and drop it from `dependencies`:
- vendor/tree-sitter-c/ (bindings + node-types + manifest + prebuilds); build
  probe scripts/build-tree-sitter-c.cjs; added to the build workflow registry
  (kind 'npm' — built from c@0.21.4 source).
- materialize-vendor-grammars.cjs: c is REQUIRED, so it is always materialized,
  even under GITNEXUS_SKIP_OPTIONAL_GRAMMARS (it needs no toolchain).
- Removed from package.json dependencies + lockfile (nothing else needs npm c —
  tree-sitter-cpp's dep on c is dev-only and not installed). Preserves the #1242
  ABI pin: vendoring 0.21.4 keeps the good ABI while closing the ARM gap.
- parser-loader note + the prebuild-coverage guard + a cli-commands assertion
  updated; c moves from the npm-gap allow-list into the vendored 6/6 cohort.

Verified: tsc clean, 31 unit tests pass, c loads/parses; the guard is RED for
c/dart/proto/kotlin until the workflow populates prebuilds (the must-fill gate).
Closes the operational risk in #2116.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(ci): source-build fallback for vendored c/kotlin so CI is healthy pre-prebuilds

The vendored prebuild-only grammars (c, kotlin) had empty prebuilds/ until the
build-tree-sitter-prebuilds workflow runs, so they could not load in CI — and
C is hard-required by cross-platform tests (tree-sitter-languages/parsing on
ubuntu+macos+windows), which I cannot pre-build for macos/windows locally. The
robust fix is a source-build fallback that works on every CI runner (all have a
toolchain), mirroring dart/proto:

- Vendor the grammar source (binding.gyp + src/) for c and kotlin; their build
  scripts now PREFER a committed prebuild (toolchain-free) and fall back to
  `node-gyp rebuild` from the vendored source when no prebuild matches. Verified
  both compile against the hoisted node-addon-api@^8 and the runtime loads.
- prebuild-coverage guard is now bootstrap-tolerant: a grammar that vendors its
  source (binding.gyp) may have an incomplete prebuild set (the workflow fills
  it); a prebuild-only grammar (swift) still must ship all six. Any present
  prebuild must still be N-API. Guard goes green; it re-tightens per-grammar as
  the workflow populates prebuilds.
- actionlint: silence a false-positive SC2016 (JS template literals inside the
  single-quoted `node -e` validate block).

Note: kotlin's generated parser.c is large (~23 MB on disk; compresses heavily
in git). Once the workflow populates all six kotlin prebuilds, the source serves
only as the fallback and could be slimmed if desired.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(docker): re-materialize+rebuild vendored grammars after npm prune

`npm prune --omit=dev` in the gitnexus CLI image drops anything not in
package.json's dependency tree — including the VENDORED tree-sitter grammars
(materialized by postinstall, not declared deps) and their built bindings. The
`serve` image analyzes/parses repos at runtime, so re-run the grammar postinstall
after the prune (in the toolchain-equipped builder) to restore them. Load-bearing
for tree-sitter-c, a core REQUIRED grammar now vendored (#2116): as a former
dependency it survived prune; vendored, it would not. Also restores
swift/dart/proto/kotlin, which were silently pruned from the image before.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(grammars): unify tree-sitter-swift with the vendored-source build pipeline

Swift was the last grammar handled differently — it shipped only upstream
prebuilds, while c/dart/proto/kotlin vendor their grammar source and use a
prefer-prebuild -> source-build-fallback activation script. Vendor swift's
source so all five are handled identically (one uniform build path).

- vendor/tree-sitter-swift: add binding.gyp (win-hardened), bindings/node/
  binding.cc, src/parser.c (ABI-14 default, ~18 MB), src/scanner.c, and
  src/tree_sitter/ headers. The 6/6 prebuilds are retained. The legacy
  parser_abi13.c alternate is intentionally not vendored.
- build-tree-sitter-swift.cjs: rewrite the prebuild probe into the dart-style
  prefer-prebuild then source-build fallback (keeps the GITNEXUS_SKIP gate and
  the never-exit-non-zero postinstall invariant).
- build-tree-sitter-prebuilds.yml: register swift (kind 'vendored'); add its
  package.json to the version-gated pull_request paths and a validate snippet.
- prebuild-coverage guard auto-moves swift into the source-fallback cohort
  (binding.gyp now present); refresh the stale "swift is prebuild-only" comments.
- tests: add build-tree-sitter-swift-probe.test.ts; fix the pre-existing
  build-tree-sitter-kotlin-probe.test.ts breakage (it still asserted the old
  probe strings after kotlin's dart-style conversion); assert swift's vendored
  source in cli-commands.test.ts.
- docs: README / .devcontainer / kotlin vendor README — swift's prebuilds are
  now GitNexus-cross-built from vendored source like the rest, not upstream-only.

Verified: swift source-builds against node-addon-api@8 -> N-API binary -> loads
against the pinned tree-sitter@0.21.1 (ABI 14) -> parses cleanly.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(publish): gate a lean prebuilds-only npm tarball behind a coverage guard

Vendoring grammar source (parser.c) alongside the prebuilds means the npm
tarball now carries ~50 MB of generated source it almost never compiles (every
supported platform-arch has a prebuild). Prepare to drop it from the published
package once all prebuilds exist — safely.

- .npmignore: add a GATED, commented-out "lean publish" block that excludes the
  source-build inputs (parser.c/scanner.c/tree_sitter/binding.gyp/binding.cc) but
  keeps prebuilds/ + the runtime files. Uncommenting ships prebuilds-only.
- scripts/assert-publish-grammar-coverage.cjs: a prepack guard that refuses to
  pack/publish if the source exclusion is active while any vendored grammar still
  lacks 6/6 prebuilds (which would ship a grammar with no loadable binding). Wired
  into `prepack` (runs on npm pack + publish, incl. the publish.yml dry-run) and
  exposed as `npm run assert-publish-coverage`.
- test: pure-core decision cases + a real-repo publish-safety check that fails CI
  if .npmignore is activated prematurely.

Net: the prebuilds already publish today (files: ["vendor"]); this makes the
future switch to a prebuilds-only tarball a one-line uncomment that can't ship a
dead grammar. The guard currently reports "source + prebuilds" (only swift has
6/6 prebuilds so far) and passes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(grammars): consolidate the 5 build-tree-sitter-*.cjs into one

The per-grammar activation scripts (c/dart/proto/swift/kotlin) were ~95%
identical — same prefer-prebuild → source-build → never-fail flow, differing only
in name, target_name, required-vs-optional, and the display label in warnings.

- scripts/build-tree-sitter-grammars.cjs: one registry-driven script. Bare call
  builds all (postinstall); `... <name>` builds only the named grammars (so the
  probe test can isolate one). c is `required: true` (ignores the opt-out gate);
  the rest honor GITNEXUS_SKIP_OPTIONAL_GRAMMARS. Per-grammar try/catch + a final
  process.exit(0) preserve the postinstall never-exit-non-zero invariant.
- package.json: postinstall is now `materialize && build-tree-sitter-grammars.cjs`
  (was five chained `build-tree-sitter-<name>.cjs` calls).
- tests: replace the two near-identical *-probe.test.ts files with one
  parameterized build-tree-sitter-grammars-probe.test.ts that also covers the
  required-vs-optional opt-out split and an unknown-grammar arg.
- update cli-commands.test.ts postinstall assertions + the vendor c/kotlin/swift
  README + swift provenance to reference the consolidated script.

Behavior is preserved (warnings normalized to one consistent format). Removes 5
scripts + 1 test file; adds 1 script + 1 test.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(ingestion): lazy-load tree-sitter-c to prevent module-load crash

tree-sitter-c is now vendored prebuild-only (#2116) with 0/6 committed
prebuilds, so on a toolchain-less or `--ignore-scripts` install C has no native
binding. Three modules loaded it via a hard top-level `import C from
'tree-sitter-c'`, which throws ERR_MODULE_NOT_FOUND at module-load — crashing
`analyze` before parser-loader's optional/severity:error degradation can run.
This is the #2091/#2093 bug class (previously fixed for swift/dart/kotlin); C was
left static because it used to be an always-present npm dependency.

- languages/c/query.ts: load via the lazy guarded getLanguageGrammar(C), mirroring
  swift/query.ts; the main-thread isLanguageAvailable filter ensures the getters
  are reached only when C is present.
- workers/parse-worker.ts: guarded `_require('tree-sitter-c')` + conditional
  languageMap spread, like swift/dart/kotlin.
- group/extractors/include-extractor.ts: guarded `_require`; getLanguageForFile
  returns null for .c/.h when absent, so C include-extraction degrades to a no-op
  (C++ unaffected).
- extend the registry-import-closure regression test (#2091/#2093) to assert C
  also loads lazily at registry static-import time.

* fix(ci): repin attest-build-provenance to the real v2.4.0 SHA

The workflow pinned actions/attest-build-provenance@bd77c077… commented
`# v2.4.0`, but v2.4.0 is e8998f94… (verified via the GitHub API); bd77c077…
is an untagged mid-stream commit, so the SLSA-attestation step ran unvetted
action code and the comment misrepresented what runs. Repin to the real
v2.4.0 commit and drop the `# PLACEHOLDER-PIN` markers on both this line and
the setup-python pin (a26af69b… is already the correct v5.6.0 — only its
comment was stale). Update the header NOTE accordingly.

* fix(ci): skip the prebuild-PR aggregate when release App secrets are absent

The aggregate job mints a GitHub App token as its first step; with
RELEASE_APP_ID/RELEASE_APP_PRIVATE_KEY unset it hard-failed AFTER a full
(up-to-6-runner) native build. Since the `secrets` context isn't available in
a job-level `if:`, the guard job now computes a `release_app` boolean output
(a step can read secrets) and emits an actionable `::notice::`; aggregate
gates on it and skips cleanly, while the build job's artifacts still upload
(run with open_pr=false for artifacts-only).

* chore(ci): drop package-lock.json from the prebuild paths filter; widen build timeout

`gitnexus/package-lock.json` changes on nearly every dependency PR, so it
fired the prebuild workflow's guard job on unrelated churn (the matrix stayed
correctly skipped — `gitnexus/package.json` already covers the transition-window
pin, so removing the lock only drops guard noise). Also bump the native build
job timeout 30 -> 45 min for headroom compiling the 23 MB kotlin / 18 MB swift
parser.c, especially under arm emulation.

* fix(ci): event-gate the aggregate open-PR condition explicitly

`inputs.open_pr` is null on pull_request events, and the prior
`inputs.open_pr != false` leg relied on GHA's direction-ambiguous null
coercion (Codex F4) to decide whether to open the prebuild PR. Gate
explicitly on the event: a non-fork pull_request that bumped a grammar
version opens the prebuild PR (the documented flow), and `open_pr` is only
consulted on workflow_dispatch — so a manual run with open_pr=false stays
artifacts-only and no event's behavior rests on coercion.

* fix(publish): validate the effective npm-pack contents in the coverage guard

The publish guard inferred "is source shipped?" from a single .npmignore toggle
line, which a partial/out-of-order edit could defeat (exclude binding.gyp but
leave parser.c → unbuildable yet "source-shipping"). It now inspects the
EFFECTIVE tarball via `npm pack --dry-run --ignore-scripts --json` (the
--ignore-scripts avoids re-entering this guard through prepack): a grammar
"ships source" only when EVERY on-disk source-build input (binding.gyp +
binding.cc + parser.c + scanner.c when present + a tree_sitter header) is
actually in the packed file list.

This also surfaced that the gated lean-publish .npmignore block was inert:
package.json's `files: ["vendor"]` allow-list overrides .npmignore for the
vendored subtree, so those exclusion lines never dropped anything. Replace the
dead toggle with documentation of the real mechanism (narrow the `files` field)
and note the guard enforces safety on the effective pack regardless of how the
slim is done.

* test(prebuild): hard-gate declared-fully-prebuilt grammars on 6/6 coverage

The strict 6/6 prebuild assertion was dormant whenever a grammar vendors source
(binding.gyp) — which is every grammar — so a dropped prebuild passed CI
silently. Add a FULLY_PREBUILT allowlist of grammars GitNexus has committed 6/6
for (today: swift); those must keep all six even with a source fallback, so
losing one now fails CI. Grammars graduate into the set as the
build-tree-sitter-prebuilds workflow lands their binaries. (The static-import
degradation smoke is covered by the registry-import-closure regression test
extended in the C lazy-load commit.)

* chore(deps): promote node-gyp-build/node-addon-api to regular dependencies

Every vendored grammar's index.js does `require("node-gyp-build")` at runtime
to load even a prebuilt .node, so node-gyp-build is runtime-load-critical (and
node-addon-api is needed for the source-build fallback). They were
optionalDependencies, surviving `--omit=optional` only via the required
tree-sitter's transitive edge — correct today but fragile. Promote both to
regular dependencies so the contract is explicit (optionalDependencies is now
empty and removed). Lock the contract with a cli-commands assertion.

* chore(vendor): add Windows cflags parity block to tree-sitter-c/binding.gyp

c's binding.gyp used an unconditional `cflags_c: ["-std=c11"]`, while
kotlin/swift gate MSVC flags behind an `OS=='win'` condition (/std:c11 /utf-8).
Inert today (no non-ASCII bytes in c's parser.c, and node-gyp ignores cflags_c
on MSVC anyway), but align the three so a future source-build fallback on
Windows behaves consistently.

* docs(agents): correct stale optional-grammar / postinstall notes

AGENTS.md still said postinstall "patches tree-sitter-swift, builds
tree-sitter-proto" and that only kotlin/swift are "optional". Update to the
vendored-uniform model: postinstall materializes the vendored grammars and
prefers a committed prebuild (source-build only when none matches); c is
required while dart/proto/swift/kotlin are optional + skippable via
GITNEXUS_SKIP_OPTIONAL_GRAMMARS=1, with non-fatal warnings only on a
toolchain-less host with no matching prebuild.

* fix(install): preserve the backup and warn loudly on a failed materialize rollback

If renameSync(partial, dest) failed AND the rollback renameSync(backup, dest)
also failed, the grammar was left unmaterialized (node_modules/<name> missing)
with only a generic "could not materialize" warning — the recoverable backup at
<dest>.materialize-bak was unmentioned. Emit a CRITICAL warning naming the
backup path and the recovery command on that double-failure, and document that
the fail-soft catch removes only the scratch `partial`, never the `backup`
(which may be the sole recoverable copy). Never-throw / exit-0 contract intact.

* fix(publish): make the coverage guard's npm-pack inspection script-safe

The prepack guard shelled out to `npm pack --dry-run --ignore-scripts --json`,
but the `--ignore-scripts` flag is not reliably honored by npm pack's
prepare/prepack lifecycle on the CI npm — so build.js ran, polluted the --json
stdout with `[build] …`, and the guard's JSON.parse threw. That broke every
`npm pack` (packaged-install-smoke on ubuntu+windows) and failed the guard's own
real-repo unit test (the only coverage-job failure). Force script-skipping via
the reliable `npm_config_ignore_scripts` env config (also removes the prepack
re-entry/recursion risk) and parse defensively from the JSON-array start.

* fix(publish): make the coverage guard deterministic — read `files`, not `npm pack`

The npm-pack-based guard timed out in CI: `npm pack`'s prepare/prepack lifecycle
is not skipped by `--ignore-scripts` (flag or env config) on the CI npm, so the
inner pack ran the full build (~20s+) — fine for the slow smoke job, but it blew
past vitest's 30s test timeout in the coverage job (and risked re-entering this
prepack guard).

Replace it with a deterministic, fast (~0.1s) check that needs no subprocess:
since `files: ["vendor"]` OVERRIDES `.npmignore` for the vendored subtree (so
`.npmignore` can never drop vendored source — verified), the ONLY lever that can
exclude source is narrowing the package.json `files` field. The guard now reads
`files` directly: a grammar "ships source" iff `files` includes the vendor
subtree AND the grammar carries a buildable source set on disk. A lean publish
that narrows `files` while a grammar lacks 6/6 prebuilds still fails the gate.

* feat(ci): vendored tree-sitter grammar update monitor

Adds a weekly (+ dispatchable) workflow that checks each vendored grammar against
its source-of-origin (npm for swift/kotlin, the GitHub default branch for
dart/proto; c is excluded — held at 0.21.4 for ABI safety) and opens a PR
re-vendoring any update that is ABI-COMPATIBLE with the pinned tree-sitter@0.21.1
(LANGUAGE_VERSION 13-14).

ABI awareness is the point: most upstreams have moved to ABI 15 (newer
tree-sitter), so a blind "bump to latest" would open PRs that can't build. The
monitor fetches the candidate source, reads its parser.c LANGUAGE_VERSION, and
only re-vendors 13/14 — incompatible updates are reported (notice + job summary),
never applied. (Confirmed live: dart/proto upstreams are ABI 15 today and are
correctly held; swift/kotlin are current.)

The re-vendor refreshes only the source-build inputs + runtime entrypoints,
preserving the GitNexus-hardened binding.gyp / README / prebuilds; the version
bump then triggers build-tree-sitter-prebuilds.yml, whose ABI-validation is the
final safety net so a subtly-wrong re-vendor can't silently ship. PR creation is
gated on the RELEASE_APP secret (skips with a notice if absent), mirroring the
build aggregate. Unit test locks the ABI gate; the script is import-safe.

* feat(ci): monitor tree-sitter-c too (report-only, ABI-pinned)

c was excluded from the update monitor, so an upstream c update went unnoticed.
Include it, but as report-only via a `hold`: c is ABI-pinned at 0.21.4
(#1242/#858) and must not auto-bump without a tree-sitter runtime upgrade, so an
available c update is detected + surfaced (notice + job summary) but never
auto-PR'd — even if it were ABI-13/14. `--apply c` refuses defensively. (Live:
upstream c is 0.24.1 / ABI 15 today, so c is doubly held — reported, not applied.)

* fix(ci): drop the shell in the grammar monitor's github fetch (CodeQL)

CodeQL flagged the GitHub-tarball fetch — it used `bash -c "gh api …/tarball/$ref
> src.tgz && tar xzf src.tgz"`, interpolating the API-derived ref into a shell
command (the shell-command-injection family: "this shell command depends on an
uncontrolled file name"). Replace it with a shell-free path: capture `gh api`'s
binary tarball as a Buffer via execFileSync, write it to a fixed file, and
extract with execFileSync('tar', …). No shell, no injection surface. Verified the
dart/proto fetch + ABI read still work.

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 18:16:24 +01:00
Nilotpal Kashyap
1716bf7c1e
feat(cli): add gitnexus uninstall to reverse setup (#2060) (#2062)
* feat(cli): add `gitnexus uninstall` to reverse setup (#2060)

`gitnexus uninstall` was documented in #168 but never implemented, so the
CLI rejected it with "error: unknown command 'uninstall'" (#2060).

Add an `uninstall` command that reverses `gitnexus setup` target-by-target:
removes the GitNexus MCP server entries (Cursor, Claude Code, Antigravity,
OpenCode, Codex), the installed skill directories, and the Claude Code /
Antigravity hook entries plus their bundled hook scripts. Edits are surgical
and idempotent — only gitnexus-owned keys/entries/dirs are touched, and JSONC
comments/indentation are preserved. Defaults to a dry-run preview; `--force`
applies. Per-repo indexes and the global npm package are left alone with
printed hints, since both are destructive in ways setup never caused.

Adds i18n entries (en + zh-CN), help wiring, README/CHANGELOG docs, and unit
tests covering MCP/hook/skill/Codex-TOML removal, dry-run, corrupt-file
safety, and the no-op case.

* changelog changes

* changelog changes

* fix(cli): harden uninstall against data-loss edge cases (review #2062)

Address review findings on the uninstall command:

- Empty derived skill name no longer wipes the whole skills dir: a bare
  '.md' source file would make basename() return '', resolving to the
  skills dir itself. Skip empty names in derivation and reject
  empty/'.'/'..'/separator names in removeSkillsFrom.
- Corrupt settings.json no longer orphans the hook: gate the hook-script
  dir removal on status !== 'corrupt' so we don't delete a script while a
  still-registered entry points at it (Claude + Antigravity blocks).
- Hook removal is now element-granular: delete only the gitnexus command
  inside an entry's hooks[], removing the whole entry only when it becomes
  empty. Preserves a user command co-located in the same entry.
- Fallback TOML stripper: also remove descendant sub-tables
  ([mcp_servers.gitnexus.env]), track multiline strings so a bracketed
  line inside a value isn't treated as a header, and stop reflowing
  unrelated blank lines.
- Set process.exitCode=1 on partial failure; add a 10s timeout to
  'codex mcp remove'.

Tests expanded 7 -> 17: empty-skill guard, corrupt-settings hook
preservation, shared-entry hook removal, OpenCode MCP keyPath,
Antigravity MCP + AfterTool hooks, codex-remove success path, TOML
sub-table + multiline-string cases, dry-run for hooks/skills, and the
directory-layout skill branch.

* refactor(cli): share setup/uninstall target map + harden TOML fallback (review #2062)

Maintainer review follow-ups:

- Extract editor target identities into editor-targets.ts (MCP paths/keyPaths,
  Codex TOML section, skill dirs, hook settings/events/needles/script dirs,
  shared detectIndentation). Both setup.ts and uninstall.ts consume it, so a
  target change updates both sides — killing the silent drift hazard.
- Add a setup -> uninstall round-trip integration test that iterates
  getEditorTargets(): setup writes every target, uninstall removes all of them,
  and a co-located user MCP server + user hook survive. Drift tripwire in both
  directions.
- Preview now prints the exact paths it would remove; command output + README
  state skills are matched by bundled gitnexus skill name. (Provenance marker
  deferred to a tracked follow-up.)

Hardening of the hand-rolled Codex TOML fallback (found in code review):
- Strip a section header that has a trailing inline comment (was matched as a
  header but failed the exact classify check -> section left behind while
  reported removed).
- Preserve CRLF line endings instead of rewriting the whole file to LF.
- Fix multiline-string scan: a line with an odd count of BOTH """ and '''
  no longer mis-picks the delimiter and desyncs the scanner (left->right scan).
- removeSkillsFrom guard also rejects absolute names.

Regression tests added for each. Full setup/uninstall suite green.

---------

Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
2026-06-09 10:50:18 +01:00
Gergő Magyar
f1151660b9
fix(install): graceful Kotlin optional-grammar install + accurate toolchain docs (#2110)
Some checks are pending
Devcontainer Smoke / Config-transform unit tests (push) Waiting to run
Devcontainer Smoke / Build devcontainer image (push) Waiting to run
CodeQL / Analyze (javascript-typescript) (push) Waiting to run
CodeQL / Analyze (python) (push) Waiting to run
Gitleaks / gitleaks (push) Waiting to run
Publish / Classify release event (push) Waiting to run
Publish / RC guard (marker + release-PR skip) (push) Blocked by required conditions
Publish / ci (push) Blocked by required conditions
Publish / Publish to npm (push) Blocked by required conditions
Publish / Build & Push RC Docker images (push) Blocked by required conditions
Scorecard / Scorecard analysis (push) Waiting to run
Trivy Image Scan / Trivy (gitnexus-cli) (push) Waiting to run
Trivy Image Scan / Trivy (gitnexus-web) (push) Waiting to run
* fix(install): document Kotlin optional-grammar toolchain behavior + graceful install probe

tree-sitter-kotlin is a third-party npm optionalDependency that ships
source-only (no upstream prebuilds) and compiles its native binding via
node-gyp at install. It was the only optional grammar without a GitNexus
install-time probe, and the README's GITNEXUS_SKIP_OPTIONAL_GRAMMARS
"no toolchain needed" note omitted Kotlin entirely. This adds a fail-soft
probe (mirroring the Swift one) that warns clearly and always exits 0 so
install never breaks, wires it into postinstall, and corrects the
optional-grammar docs in README.md and .devcontainer/README.md. Shipping
prebuilt .node binaries (the literal request) needs an upstream/CI build
matrix and is intentionally left as follow-up.

Refs #2107

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix: address PR #2110 tri-review findings (Kotlin optional-grammar install)

Addresses the four P2 findings from the PR #2110 tri-review:

- F1: docs no longer imply GITNEXUS_SKIP_OPTIONAL_GRAMMARS=1 skips Kotlin's
  toolchain. npm compiles tree-sitter-kotlin via its own node-gyp-build step
  regardless of that variable; point to `npm install --omit=optional` as the
  real lever (README.md + .devcontainer/README.md).
- F2: the install probe now surfaces its "Kotlin unavailable" guidance on the
  dir-absent branch — the dominant toolchain-less case, where npm prunes the
  failed optional dependency so the package dir is gone at postinstall. Gated on
  npm_config_omit so a deliberate `--omit=optional` stays silent. Still never
  throws or exits non-zero.
- F3: add a behavioral test that executes the probe across its skip /
  dir-absent-warn / dir-absent-omit-silent paths and asserts exit code 0
  (guards the postinstall "never exit non-zero" invariant a static assertion
  cannot).
- F4: reframe prebuilt Kotlin as deferred Swift-parity follow-up — GitNexus
  already vendors its own self-built Swift prebuilds and could do the same for
  Kotlin — tracked in #2107, not an upstream-only blocker.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 09:04:01 +01:00