Commit graph

155 commits

Author SHA1 Message Date
Gergo Magyar
36141609a1 fix(scope): fail closed on namespace factory returns the lookup cannot prove
Review follow-up for the namespace factory return lookup (#3498). Each case
below either resolved to the wrong class or crashed; each now types the
receiver correctly or records a receiver-unresolved drop.

- Disk mode: the namespace-name set walked `scopeTree.byId`, which throws
  after the out-of-core seal, so an inline-only `rp.make().m()` partition
  aborted analyze under GITNEXUS_DISK_SCOPE_INDEX=1. It is now built from the
  finalized `scopes.imports` edges, resolved namespace edges only. This also
  removes the run-to-run edge/drop drift the lazy build caused.
- Hoisted method returns: TS and Dart hoist a method's return annotation to
  the module under the method name, and Go binds receiver methods at package
  scope, so `m.create()` could take `WidgetFactory.create(): Widget`. The
  return binding must now start where a module-local `Function` of that name
  is declared.
- `import pkg.sub` binds `pkg` but targets `pkg/sub.py`; such an edge only
  counts when its target file is the module the local name binds.
- A dotted return (`-> models.Order`) is not followed further: the next step
  would read `models` in the caller's namespace map, not the producer's.
- Zig: the `const x = rp.make()` binding decided namespace-vs-file-struct from
  the filename's case and lost `list.push`-style edges on lowercase
  file-structs. That binding form is reverted; the inline form is unaffected.
- Shared-position drops: a site's identity is now `receiver.member`, so
  `rp.untyped().untyped()` records its outer drop while a call and its own
  callee read still count once.

Tests: temp-repo regression rows for each case in the TypeScript, Go and
Python suites, a lowercase file-struct row in Zig, an awaited Dart row (an
awaited Future is not unwrapped yet, so it records a drop), and data-driven
unit cases for recordSiteMember.

Corpus A/B against origin/main: identical CALLS/ACCESSES edges outside the
new fixtures; the drop set outside them is unchanged from the previous head.
Receiver bench: +1 call drop (the awaited Dart row). Dart scope-capture
fingerprint moves for the same fixture row only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 08:45:32 +00:00
Gergő Magyar
01af833875
fix(scope): preserve lexical bindings in HTTP wrapper detection (#3632)
* docs(scope): clarify lexical binding ownership and wrapper provenance

Checkpoint for #3630. Publishing the implemented source, regression tests, and benchmark harness as a draft PR; full-suite and final scaling validation remain in progress.

* fix(scope): publish #3630 docs/plans

* fix(scope): publish #3630 gitnexus/bench/lexical-binding-provenance

* fix(scope): publish #3630 gitnexus/bench/lexical-binding-provenance/fixtures

* fix(scope): publish #3630 gitnexus/bench/lexical-binding-provenance/results

* fix(scope): publish #3630 gitnexus/src/core/ingestion

* fix(scope): publish #3630 gitnexus/src/core/ingestion/languages/typescript

* fix(scope): publish #3630 gitnexus/src/core/ingestion/languages

* fix(scope): publish #3630 gitnexus/src/core/ingestion/pipeline-phases

* fix(scope): publish #3630 gitnexus/src/core/ingestion/route-extractors

* fix(scope): publish #3630 gitnexus/src/core/ingestion/workers

* fix(scope): publish #3630 gitnexus/test/fixtures/lang-resolution/fetch-wrapper-target-identity/lib

* fix(scope): publish #3630 gitnexus/test/fixtures/lang-resolution/fetch-wrapper-target-identity

* fix(scope): publish #3630 gitnexus/test/fixtures/lang-resolution/fetch-wrapper-target-identity/packages/http

* fix(scope): publish #3630 gitnexus/test/fixtures/lang-resolution/fetch-wrapper-target-identity/packages/http/src

* fix(scope): publish #3630 gitnexus/test/fixtures/lang-resolution/fetch-wrapper-target-identity/server

* fix(scope): publish #3630 gitnexus/test/fixtures/lang-resolution/fetch-wrapper-target-identity/web

* fix(scope): publish #3630 gitnexus/test/integration/resolvers

* fix(scope): publish #3630 gitnexus/test/unit

* fix(scope): publish #3630 gitnexus/test/unit/group

* fix(scope): publish #3630 gitnexus/test/unit/scope-resolution

* fix(scope): publish #3630 gitnexus/test/unit/scope-resolution/typescript

* fix(scope): publish #3630 gitnexus-shared/src/scope-resolution

* fix(scope): treat wrapped assignment targets as writes

`(app) = new Map()`, `(app as T) = x` and `app! = x` assign the same binding as
`app = x`, but route-fact invalidation and the shared pattern walker stopped at
the wrapper, so Express provenance survived the write and produced a false
route. Recurse through parenthesized, as/satisfies and non-null targets.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(routes): resolve wrappers through overloads, declarator lists and barrels

Three wrapper shapes lost their identity:

- Bodyless overload signatures were indexed as separate definitions sharing
  the implementation's binding, so the binding looked ambiguous and both local
  and imported calls were rejected. Signatures now declare no callable.
- Facts for `const base = 0, request = (url) => fetch(url)` followed the first
  declarator. The captured name now selects its own declarator.
- An import resolved to a workspace barrel stopped at index.ts. Workers now
  record named, star and imported-then-exported re-exports, and resolution
  follows them to the declaring module: local and named exports shadow
  `export *`, star exports never carry `default`, and conflicting star exports
  block the name. No repository-wide name fallback is restored.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(scope): assert closure ownership against the declaration

The closure-write assertion compared one lookup with itself, which also passes
when both are undefined. Compare it with the outer declaration and the shadowing
parameter, and keep the memoization check explicit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* refactor(routes): share lexical helpers and release per-file HTTP contexts

- Share binding keys, parameter patterns and transparent expression wrappers
  from lexical-bindings instead of four local copies; drop the unused
  moduleStem export; name the inferred-source predicates in lookup.
- Key the per-file HTTP context by its tree rather than its ParsedFile.
  Workers retain ParsedFiles for the whole chunk, which kept every file's AST
  and traversal maps alive until the chunk flushed.
- Index re-exports by name, memoize barrel walks per starting export, and keep
  facts without import targets as-is instead of copying them.
- Move TS-only wrapper cases into the TS describe.each instead of branching
  inside the per-language loop.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(routes): keep wrapper identity for HOCs, CommonJS, object keys and fetch packages

Four definition shapes that main linked by name lost their wrapper identity:

- `const load = useCallback((url) => fetch(url))` and curried HOCs produced no
  facts because definition lookup stopped at the call; it now follows the
  callable argument, as the structural definition capture does.
- `module.exports = { request }`, `exports.x = …` and `module.exports.x = …`
  were not exports, so destructured `require` callers missed the wrapper.
  CommonJS export assignments are recorded when `module`/`exports` are the
  unshadowed globals, including functions defined inline in the export object.
- An object-literal key or method name was resolved as a lexical binding and
  collided with a same-named module wrapper. Only binding identifiers now name
  a definition's binding.
- `import fetch from 'node-fetch'` no longer counted as fetch. Imports of known
  fetch implementations (node-fetch, cross-fetch, isomorphic-fetch, the fetch
  member of undici and whatwg-fetch) are fetch evidence again; parameters and
  other local names still are not.

Reference identifiers are also resolved only when their name is bound to a
callable or class region; other identifiers cannot add a reachability edge.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(routes): drive the forwarding fallback case from data and cover re-export merges

Replace the boolean-switched forwarding fallback case with table rows instead
of branching inside the test, and assert that barrel re-export facts survive
worker sub-batch merges.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* chore(bench): reject stale or dirty call-graph exports

The committed comparison named a candidate revision that is not on this branch,
and nothing stopped it. Comparison now requires both exports to come from clean
checkouts and the candidate export to come from the checkout running the
comparison.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* chore(bench): regenerate the call-graph comparison on the reviewed head

Clean baseline 565e50276 and clean candidate 8a5f7b738 on the frozen 432-file
corpus (fingerprint 88be1faf...): exactly one CALLS target changes, the
annotated closure probe `declaredOwner.writeAndRead` now calls `Base.m`
instead of `Foo.m`. The other 431 CALLS and all 11 METHOD_OVERRIDES edges are
unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* chore(bench): emit export provenance and review notes from the comparison

The comparison report carried export provenance and the expected-change
explanation only as hand-added fields, which a regeneration dropped. Emit
both exports' revisions, dirty flags and fingerprints, the unchanged CALLS
and METHOD_OVERRIDES counts, and an optional `--note` explanation directly.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* chore(bench): record calibrated scaling reports and the regenerated comparison

Duration-calibrated-v2 runs (5 warmups, 15 measured batches, 500 ms pilot
target) pinned to the same CPU, baseline then candidate back to back:

- Baseline 565e50276 exceeds the 1.8 normalized ceiling on six arms at 4x
  input: position-index 7.92, position-deep-completed 4.65, nested-helpers
  4.82, nested-helper-chain 14.05, reverse-chain 4.31, name-collisions 2.61
  (medians).
- Candidate 94f1a055b passes --check: every arm stays at or below 1.32 at 4x
  for both minimum and median, with correct semantic guards on every arm.

The comparison is regenerated from clean exports with its provenance and
review note emitted by the tool: one expected CALLS target change, 431 CALLS
and 11 METHOD_OVERRIDES unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* refactor(scope): replace recursion with explicit stacks in new traversals

The binding work added five recursive traversals whose depth follows input
shape: region summaries (callable nesting), barrel resolution (re-export
chain length), pattern binding names (destructuring depth), route-fact write
invalidation (pattern depth), and the position index's max-end tree. Each
now uses an explicit stack with the same visiting order and results.

Regression cases cover the inputs the recursive versions could not handle:
a 20,000-deep destructuring pattern and a 50,000-module re-export chain both
threw RangeError before this change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* perf(routes): avoid a full-tree script walk when building lexical bindings

Route-fact extraction ran 1.78x slower than main on a 512-function file
(87 ms vs 49 ms) because createLexicalBindings walked every AST node in script
to detect collapsed parameter/body environments, which only functions with
parameter defaults or computed keys can have.

- Find functions with the native type search, skip plain parameter lists
  without any subtree search, and walk only the formals and bodies of the
  rest. Detection is unchanged, except that functions inside syntax-error
  regions are now also checked, which can only add fail-closed ambiguity.
- Read each node's id and start position once per lookup instead of
  repeatedly crossing into native getters.

The same extraction now takes about 56 ms against 51 ms on main.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* perf(routes): walk wrapper evidence with a tree cursor

The wrapper context visited every node through namedChildren, which
materializes each child as a SyntaxNode and records a parent edge by node
id. A tree cursor reads node types and offsets natively, so only the node
types the handlers inspect become SyntaxNodes. Declared identifiers are
keyed by start offset, and the walk records the ancestor facts later
queries need (stored binding, default export, declarator of a name)
instead of a parent map.

On the 512-wrapper scaling input, context construction drops from about
58 ms to 48 ms.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* chore(bench): regenerate the call-graph comparison on the cursor walk

The candidate export at 6061ca71b has the same fingerprint as the previous
candidate: the traversal rewrite changes no edge. The one expected target
change and the 431 unchanged CALLS and 11 METHOD_OVERRIDES remain.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* chore(bench): record final scaling reports on the cursor walk

Baseline 565e50276 and candidate 61de364ff ran back to back on CPU 23
with the duration-calibrated-v2 protocol. Every candidate check stays at
or below a 1.32 normalized factor against the 1.8 ceiling; the baseline
fails six of nine arms. Workload fingerprints agree except the documented
name-collision false positive in the baseline.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(plan): checkpoint the final review, scaling and suite results for #3630

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(routes): treat export aliases of one wrapper as one star-export target

Star-export candidates were deduplicated by the resolved export name, so
`export { request, request as send }` reached through two barrels counted
as two targets and blocked the import, dropping the consumer's FETCHES
edge. Candidates are now keyed by the declaration each export name names,
matching ECMAScript ResolveExport's binding comparison; distinct
declarations exported under the same name stay ambiguous.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* chore(bench): regenerate scaling and call-graph reports on the alias fix

Baseline 565e50276 and candidate 4d62bd045 ran back to back on CPU 23.
Every candidate check stays at or below 1.22 against the 1.8 ceiling;
the baseline still fails six of nine arms. The CALLS/METHOD_OVERRIDES
export is unchanged (fingerprint 4db24f57), so the comparison only
records the new candidate revision.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(plan): record the export-alias fix and the regenerated reports for #3630

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(scope): keep the type a write gives an undeclared name

A constructor write to a name no scope declares (`client = new ApiClient()`
in sloppy JavaScript) declares nothing, so the lookup stays absent. The
carried constructor fact was dropped on that exit, which lost the receiver
type main had inferred for later reads in the writing scope. The absent
result now carries it, and findReceiverTypeBinding uses it before the
namespace fallbacks. Only complete-claim providers carry such a fact.

Tests pin the carried type per inferred source, the undeclared write, and
preservePosition across a module redirect.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(routes): close the wrapper identity gaps found in review

- CommonJS wrappers captured as their assignment (`exports.x = function`,
  `module.exports.x = () =>`) produce facts again: findFunction follows the
  assignment's right side.
- Name-only fallback considers only definitions that can be the target: an
  unresolvable import names a module-level export, an undeclared global a
  top-level function. Nested and unexported helpers no longer erase a
  wrapper's edges.
- Only possible wrappers and module-level functions are emitted, and the
  routes phase builds the index only when routes and candidate calls exist.
- A rebound binding (later assignment, `var` initializer over a function
  declaration, reassigned export) proves no wrapper across files.
- `(<T>x) = ...` is a write; transparent wrappers skip type arguments.
- Calls inside recovered syntax are still collected; instance-field
  initializers resolve after the module body, so a later declaration is
  visible.
- Local exports are always recorded, so a star export conflicting with a
  non-callable export is ambiguous, and fully resolved barrel frames are
  shared across walks.
- HTTP hooks run per file under a guard; a failing file or one without a
  ParsedFile still records its function names so fallback declines.
- namesFetch is iterative, large patterns no longer spread into push, the
  facts cache is keyed by the name node, and the provider context cache
  checks its ParsedFile.

Tests cover each case; the behavior rows fail on the previous source.
The TS-only call cases no longer branch inside the test file, call targets
are asserted exactly, the function-type list is drift-checked, and the
persistence test joins the cross-platform worker list.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(routes): describe wrapper fallback, barrel and recall boundaries

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* chore(bench): count the new CommonJS fixture's dropped write site

`lib/cjs-member.js` in the fetch-wrapper target-identity fixture writes
`module.exports.arrowRequest`, a member write whose receiver is not typed.
That adds one `receiver-unresolved` write (190 -> 191 all-kind drops,
19 -> 20 writes). Call drops and shape-arm states are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* chore(bench): regenerate scaling and call-graph reports on the review fixes

Baseline 565e50276 and candidate 528ea1478 ran back to back on CPU 23.
Every candidate check stays at or below 1.27 against the 1.8 ceiling; the
baseline still fails six of nine arms. The CALLS/METHOD_OVERRIDES export
is unchanged (fingerprint 4db24f57), so the comparison only records the
new candidate revision.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(plan): checkpoint the review fixes and regenerated reports for #3630

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* refactor(routes): share CommonJS checks and trim per-node work

- `isCommonJsGlobal`/`isModuleExports` in lexical-bindings replace the two
  copies in fetch-wrapper and framework-route-facts (both resolve the same
  scope from the same start position).
- LEXICAL_FUNCTION_TYPES is derived from the TypeScript FUNCTION_NODE_TYPES
  instead of a third hand-kept copy; the drift test still pins it.
- `unwrap` reads UNWRAPPED_TYPES, the set the cursor walk mirrors, and the
  class-body region is no longer shadowed under the name `deferred`.
- framework-route-facts reads each node's type once per loop iteration.
- The position index returns the latest-starting scope when it already
  contains the point, before the max-end tree search; that entry is the
  search's rightmost match.
- carried() returns a shared empty set for argument forms that cannot carry
  a value, the resolution file set is built without intermediate arrays,
  and the routes phase reads the call list once.

No behavior change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* chore(bench): regenerate scaling and call-graph reports on the refactor

Baseline 565e50276 and candidate 3db8657ba ran back to back on CPU 23.
Every candidate check stays at or below 1.32 against the 1.8 ceiling; the
baseline still fails six of nine arms. The CALLS/METHOD_OVERRIDES export
is unchanged (fingerprint 4db24f57). The later main merge (e2a92dab8)
touches no measured file.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(plan): checkpoint the simplification pass and its reports for #3630

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Address PR review feedback (#3632)

- Destructuring defaults (`{ fetch = globalThis.fetch } = {}`, renamed,
  array and local forms) are alias evidence for their leaf binding and
  no longer count a second initialization, which also invalidated
  bindings like `[send = other] = values`.
- `export default (request)` records the default export: the value is
  unwrapped before the identifier check.
- The HTTP fallback records and the collapsed-environment test row now
  say what they stand for.

Regression rows cover each form in JS, TS and TSX and fail on the
previous source; two fixture consumers cover imported wrappers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* chore(bench): refresh lexical binding reports after review fixes

Regenerate clean baseline and candidate measurements at c263c4f72.
All candidate scaling checks pass, the semantic delta remains one CALLS
target, and receiver-resolution counts match the existing baseline.

* docs(plan): checkpoint refreshed review-fix measurements

Record c263c4f72 review fixes, the regenerated scaling and semantic
results, and the passing receiver and CI checks.

---------

Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 11:36:06 +01:00
Gergő Magyar
bd20dde825
fix(taint): report inline and factory handler flows (#3612)
* fix(taint): cover inline and returned handlers (U8)

Join expression-handler CFG summaries to distinct graph functions without changing existing anonymous-owner identities. Keep named function expressions private to their lexical scope.

Related: #3489

* fix(ci): encode inline handler taint paths (U8)

Use codec-safe generated display names while preserving positional function identity. Verify persisted hop continuity and remove the repaired inline-flow accuracy allowance.

* fix: keep anonymous names unbound (U8)

* test: pin inline callback captures (U8)

* ci: run inline taint guard (U8)

* test: align inline handler contracts (U8)

Pin callback-owned call edges while preserving nested callable IDs and module-level HOC attribution. Assert repaired inline flows and the retained captured-closure limitation.

* ci: run native inline proofs on all OSes (U8)

Register real-worker identity/CFG and persisted explain cases on the existing Windows/macOS matrix. Charge explicit scheduling estimates without changing watchdogs or runtime behavior.

* test(taint): reject all enclosing factory sources

---------

Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com>
2026-10-17 20:22:58 +00:00
Nguyễn Đăng Minh Lực
5c3961756b
fix(ingestion): preserve Go method ownership across files (#3559)
* fix(ingestion): re-point Go struct member edges when struct is declared in a sibling file (#3558)

findEnclosingClassInfo mints a Go method's owner id from the file
containing the method (Struct:<methodFile>:<Type>), while the Struct
node is keyed by the file declaring the struct (parse-worker uses
declFile). Same package, different file => the HAS_METHOD edge's
sourceId points at a node that does not exist and the edge is silently
dropped at COPY into LadybugDB.

Add reconcileGoStructOwnerEdges, run at the end of the parse phase
after ALL worker chunks are merged so every package's structs are
visible. It rewrites only edges that would otherwise dangle, and only
when exactly one Struct node shares the dangling id's package directory
and simple name; 0 or >=2 candidates keep the old drop behavior (a
wrong owner is worse than a dropped edge).

Index build is lazy: dangling Struct sources are rare (struct fields
never dangle — they live in the struct's own file), so the common
non-Go or fully-same-file case pays two per-type edge scans and no
whole-node pass.

Regression test: test/integration/go-cross-file-struct-owner.test.ts
(fails-before: 1 HAS_METHOD edge; passes-after: 3).

* fix(ingestion): scope Go struct-owner reconcile by language and package

Address PR #3559 review findings:

- Scope the pass to Go: dangling HAS_METHOD/HAS_PROPERTY owner ids whose
  Struct:<file>:<name> file is not Go source (Rust, Swift, ...) are never
  re-pointed, and only Struct nodes stamped language 'go' (the marker the
  worker sets on every declared symbol) are candidates. Negative test pins
  a Rust-shaped dangling edge staying untouched beside a same-name Go
  struct.

- Key candidate structs by (dir, declared package clause, name) instead of
  (dir, name): one directory can carry 'package pkg' and external-test
  'package pkg_test' files declaring the same type name, which the old key
  saw as ambiguous and dropped fail-closed. Clauses are read via the shared
  inferGoPackageName derivation with a call-scoped cache. Integration test
  covers the 3-file layout (pkg decl, pkg_test decl, pkg_test methods file)
  and asserts the edge lands on the pkg_test.Widget node.

- Keep the direct call in parse-impl with a TODO: the pass needs a
  graph-level post-merge hook, which no LanguageProvider/ScopeResolver hook
  provides today.

* refactor(ingestion): remove Go graph repair from shared parse phase

* fix(ingestion): resolve Go ownership during ingestion (language-provider.ts)

* Add resolveDetachedOwnerName function to Go config

* Refactor method owner population logic in Go

* Refactor receiver binding to handle unnamed receivers

* Enhance route handler method resolution logic

Refactor route handler to improve ownership resolution and method indexing.

* Add emitPostResolutionEdges to scope resolver

* fix(ingestion): resolve Go ownership during ingestion (ownership.ts)

* fix(ingestion): resolve Go ownership during ingestion (ast-helpers.ts)

* Add resolveDetachedOwnerName to provider methods

* Update SCHEMA_BUMP to 141

Increment schema version to 141 for new features and fixes.

* fix(ingestion): resolve Go ownership during ingestion (decl.go)

* fix(ingestion): resolve Go ownership during ingestion (external_decl_test.go)

* fix(ingestion): resolve Go ownership during ingestion (external_methods_test.go)

* fix(ingestion): resolve Go ownership during ingestion (foreign.rs)

* fix(ingestion): resolve Go ownership during ingestion (go.mod)

* fix(ingestion): resolve Go ownership during ingestion (local.go)

* fix(ingestion): resolve Go ownership during ingestion (methods.go)

* fix(ingestion): resolve Go ownership during ingestion (go-cross-file-struct-owner.test.ts)

* Refactor comments and method registration in tests

Updated comments for clarity and modified method registration to include qualified names and ownership.

* fix(ingestion): resolve Go ownership during ingestion (has-method.test.ts)

* Update SCHEMA_BUMP to 141 in tests

* refactor(ingestion): remove obsolete Go struct owner reconciliation

* fix(go): model build constraints for method ownership

* test(go): cover build constraint grammar and platform variants

* fix(go): resolve method owners across compatible build variants

* test(go): verify variant ownership through ingestion pipeline

This test verifies that build-variant owners are consistent across different workers, the semantic registry, and the graph. It sets up various scenarios to ensure that ownership is correctly resolved and that there are no dangling edges in the ownership graph.

* fix(go): reject impossible known constraints when peer is unknown

Address PR #3559 review feedback. Regression coverage follows in this update.

* feat(ingestion): allow providers to group incremental resolution files

Add the optional language-neutral hook used by the Go ownership writeback fix for PR #3559.

* fix(go): group sibling files for incremental ownership resolution

Use the existing Go package-directory key for the provider hook.

* fix(incremental): collect sibling resolution groups for changed paths

Keep group keys local to each provider and include current siblings of deleted files.

* fix(analyze): persist ownership changes in unchanged Go siblings

Expand changed and deleted resolution groups before importer traversal. Use the same effective file set for deletion and writeback so package-wide ownership changes reach the database.

* test(incremental): cover provider-local resolution groups

Cover changed, added, deleted and unrelated paths for the new sibling expansion helper.

* test(go): compare incremental ownership writes with cold rebuilds

Reproduce ownership changes after deleting and restoring a Windows declaration while the shared method file stays unchanged. Verify incremental writes and persisted ownership parity with forced rebuilds.

* test(go): exclude impossible owners with unknown method constraints

Use real parsing to cover impossible owner edges and valid owners hidden by impossible competitors.

* test(go): pin filename rules and unknown constraint overlap

Document Go first-dot filename behavior and verify impossible known constraints in both operand orders.

* test(go): register incremental ownership coverage across platforms

Include the new database-backed Go ownership suite in the cross-platform test list.

* test(go): schedule incremental ownership suite with native database tests

Complete PR #3559 review fixes. Local validation: 47 focused tests across five suites passed; build and source/test type checks passed. The full suite was interrupted without a result.

* refactor(ingestion): centralize worker owner hook forwarding

Pass the language provider into the private cached owner lookup instead of repeating four owner hooks at five call sites. Keep qualified-name selection explicit and preserve the exported helper signature.

Validation: CLI/shared build and test typecheck pass; repository lint has no errors; 591 tests pass with one skipped across 13 ownership and worker suites. Web typecheck is blocked by uninstalled web dependencies. Graph checks are unavailable because this checkout has no registered index.

* test(rename): add Go cross-file fixture module

Part of the cross-file method ownership regression for PR #3559. The completed fixture uses real indexing, public rename preview/apply, warm cache replay and post-rename reindexing. All 14 Go rename integration cases pass locally.

* test(rename): add cross-file fixture before/types.go

* test(rename): add cross-file fixture before/methods.go

* test(rename): add cross-file fixture before/callers.go

* test(rename): add cross-file fixture after/go.mod

* test(rename): add cross-file fixture after/types.go

* test(rename): add cross-file fixture after/methods.go

* test(rename): add cross-file fixture after/callers.go

* Add test case for renaming pointer receiver method

Complete the real persisted-index regression fixture for PR #3559. The type, pointer receiver method and callers are in separate Go files. Public rename preview/apply must make exactly four edits across the structural interface family while preserving an unrelated same-name method, comments and strings.

The fixture checks six exact ownership, interface and call edges before rename, after warm parse-cache replay and after reindexing the edited sources. All 14 Go rename integration cases pass. Formatting and the CLI typecheck pass. Graph impact/detect-changes could not run because this checkout has no registered index.

---------

Co-authored-by: System administrator <root@nixos-infra.home>
Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
2026-10-15 10:36:13 +00:00
Gergő Magyar
8fa8e64388
fix(rename): extend safe refactoring and flow coverage across languages (#3557) 2026-10-13 08:38:17 +03:00
Gergő Magyar
191a954bf6
fix(python): resolve calls through module-returning accessors (#3542)
Some checks failed
Trivy Image Scan / Trivy (gitnexus-cli) (push) Has been cancelled
Trivy Image Scan / Trivy (gitnexus-web) (push) Has been cancelled
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
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
* test(accuracy): retire repaired Python accessor gaps

* fix(python): connect module accessor resolution

* fix(python): resolve calls through module-returning accessors

Bind `_pc().target()` and `m = _pc(); m.target()` to the module's
function when a local accessor returns an imported module, and count
capped-out dropped names in the impact lower bound.

Closes #3497

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

* refactor(python): share module-accessor position key (#3542)

- Reuse the capture-side positionKey in the receiver namespace resolver
  instead of a second line:col helper plus an inline template.
- Note why the synthetic { moduleScope, scopes: [] } input is sufficient
  for createNamespaceTargetCache (sealed disk-backed ParsedFile shape).

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

* refactor(python): reuse subtype-dispatch positionKey (#3542)

- Export the existing line:col positionKey from subtype-dispatch instead of
  defining an identical copy in module-accessors, so both Python capture
  side channels key positions through one helper.
- Apply prettier to the Function-scope indexing line.

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

* fix(ci): unique test titles and rebaseline Python benches (#3542)

- impact-epistemic-lower-bound: `%j` rendered null, NaN and Infinity all as
  `null`, so the test-completeness audit rejected the duplicate identities.
  Use `%s` so every parameterized case has a distinct title.
- python-scope: capture fingerprint changes with the new module-accessor
  capture facts (golden captures were already updated).
- receiver-resolution: +4 Python call drops (125 -> 129). The two-step
  `x = f(); x.m()` form is now counted as an unresolved call instead of
  vanishing: fastapi-depends `router.get/post` (unknown, x3) and
  python-method-chain-binding `user.get_city()` (in-program; `User` has no
  `get_city`, so the site is genuinely unresolvable).

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

---------

Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com>
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-10 18:41:51 +01:00
Gergő Magyar
999cc9f913
fix(deps): upgrade native tree-sitter to 0.25.1 (#3531)
* fix(deps): upgrade native tree-sitter to 0.25.1

* fix(tree-sitter): preserve sync loading and update verified capture baselines

Route C# scope queries through the synchronous native grammar loader so the compiled analyze module remains require-compatible on Node 22.18. Refresh only the audited empty-Python-module and renamed-PHP-fixture captures, and pin the runtime assertion to 0.25.1.

Validated 479 targeted tests, all 16 scope-capture benchmark cases, Python capture/import guards, compiled C# worker parsing, build, typecheck and formatting. Performance budgets are unchanged.

* fix(tree-sitter): refresh upstream vendors and preserve parser semantics

* fix(ci): refresh prebuilds and support Windows ARM

Refresh macOS, Linux ARM, and Windows x64 grammar binaries from the successful native CI builds. Pin node-gyp 12.4.0 so the Windows ARM jobs recognize Visual Studio 2026.

Use local Git filtering for E2E to avoid PR-files API timeouts on generated parser diffs.

Validation: 143 focused tests, Actionlint, and all 16 scope-capture benchmark checks passed. Full npm test run started; native database suites still running.

* fix(ci): refresh Windows ARM grammar prebuilds

Import all seven Windows ARM grammar artifacts from successful native build run 37965761760. Artifact filenames and SHA-256 hashes verified; all 42 native build jobs passed.

Validation: 31 prebuild coverage tests passed.

* fix(bench): stabilize scope capture timing guard

Warm each input size seven times and use 21 median samples while retaining isolated timing blocks. Yield between languages so native finalizers release allocations outside the measured sections. Keep all fixture sizes, fingerprints, scaling formulas, and budgets unchanged.

Validation: all 16 language guards passed with a 2 GB heap limit (1.38 deep-chain ratio; 1518 MB peak RSS). Linear and quadratic controls passed their expected guard outcomes. Formatting and graph change analysis passed. Full npm test is running without failures so far.

* fix(parser): reduce TypeScript capture traversal cost

* fix(test): await parser worker readiness before teardown

* fix(parser): load C# synchronously in workers

* fix(grpc): resolve native proto detections against their own file

With the native proto grammar active, sibling .proto files that declare
the same short service name under different packages tied in the
directory-based resolveProtoConflict heuristic and both providers were
dropped. A proto-origin detection already knows its defining file, so
use that file's entry before falling back to the heuristic.

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

* ci(packaging): assert native grammars load from the installed tarball

The packaged-install smoke only ran `gitnexus --version`, so a grammar
that failed to load from the published package (bundled runtime, patched
peer manifests, vendored prebuilds, C# sync loader, Protobuf wrapper)
would ship silently; the dev-tree ABI smoke also accepts vendored
grammars that report unavailable. Parse a sample for every registered
grammar plus the native Protobuf plugin from the installed package, on
the main thread and in a worker thread.

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

* ci(tree-sitter): track compatibility workarounds instead of 0.25 readiness

The daily readiness report still targeted tree-sitter@0.25.0, so after the
upgrade it would keep reporting "Blocked" and never close its tracking issue.
Repurpose it to track what we carry for the bundled runtime: derive the target
from the package.json pin, list each pack-time peer patch from
prepare-tree-sitter-bundle.cjs with its drop condition (bump, wait for an
upstream release, transitive, or intentionally pinned), and reword the
buckets. The workflow adopts the issue opened under the old title.

Also update the grammar-update-monitor header and auto-PR text, which still
described the 0.21.1 runtime and a tree-sitter-c hold.

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

* test(dart): assert both nested labels target the loop; fix Zig call-shape doc

The nested-label CFG test only jumped to `outer` and checked reachability,
which still passes when a jump is retargeted to the inner loop (its exit
reaches the outer header). Jump to both labels and assert the exact
`continue` edges; dropping the inner label now fails the test.

Update the Zig callable-flow comment: call_expression now wraps actuals in a
required `arguments:` field.

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

---------

Co-authored-by: Codex <codex@openai.com>
Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com>
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-10 13:07:59 +03:00
Gergő Magyar
82347f0f07
fix(scope): resolve local imports by lexical ownership (#3532) 2026-10-10 08:13:55 +03:00
azizur100389
ff922c0a3b
fix(python): resolve calls through aliased package re-exports (#3504) 2026-10-08 13:51:20 +03:00
Parafee41
4ccba12004
fix(scope): keep nested declarations out of module bindings (#3502) 2026-10-08 11:05:41 +03:00
Gergő Magyar
df49f90889
fix(ci): require every test to execute across the CI matrix (#3479) 2026-10-06 07:34:17 +03:00
Gergő Magyar
c474a811ba
fix(test): align fixtures and mocks with current types (#3471) 2026-10-05 04:47:37 +01:00
azizur100389
a8f18f00b9
fix(swift): avoid false calls for injected closure properties (#3434) 2026-10-02 23:31:14 +01:00
Gergő Magyar
e42122a0f6
feat(go): index gin/echo routes and report them in impact (#3402) (#3417) 2026-10-01 10:22:45 +01:00
Gergő Magyar
aa0f41e853
fix(python): model restoring helper calls in decorator identity (#3415)
* fix(python): model restoring helper calls in decorator identity (#3414)

A bare module-level call to a same-file helper whose every global
binding of a descriptor name is an unconditional del or builtins import
now restores the builtin at the call site, matching CPython. A nonlocal
rebind nested in the enclosing function now shadows an owned builtins
import, closing a false builtin. Unprovable call orders stay fail-closed
and are pinned against CPython 3.11.

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

* fix(review): apply review findings

Close three false-builtin paths the review found: a match-pattern
capture now counts as a binding (at any scope, including inside a
restoring helper), a call before the helper's def no longer counts as a
restore, and the helper-name uniqueness check sees match captures.
Pin the helper rejections (conditional restore, async, early and nested
return, wildcard import) against CPython 3.11.

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

* fix(cache): bump parse-cache schema to v122 for #3414

Python decorator identity verdicts changed, so warm v121 ParsedFiles
would replay stale receiver bindings.

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

* fix(python): require an argument-free call to a helper with no required parameters

A call that fails to bind the helper's parameters raises TypeError
before the body runs, so it proves no restore. Keep such calls
fail-closed.

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

* fix(python): count only real captures and module bindings for decorator identity

Match value patterns, class names and keyword keys read a name rather
than capture it, so they no longer shadow a builtin descriptor. A local
of the same name as a restoring helper no longer disqualifies the
module-level helper; only a module binding or a global rebind does.

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

* test(python): state the fail-closed contract of the descriptor identity table

`false` means the resolver does not prove the builtin, not that CPython
shadows it. Cases where CPython keeps the builtin but the resolver fails
closed carry a comment saying so.

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

---------

Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com>
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-29 12:37:27 +01:00
Gergő Magyar
821ffb2fcb
fix(python): resolve decorator identity like CPython (#3411)
* fix(python): resolve decorator identity like CPython

Decorator identity was decided by two different predicates. One read
the raw decorator text, so a trailing comment such as
`@staticmethod  # type: ignore` hid the builtin and turned an explicit
first parameter into a fabricated receiver. The other trusted any
`staticmethod` spelling, including a module-level rebinding and a
`staticmethod(classmethod(f))` stack, which CPython cannot call.

Read the decorator expression node only, and recognize a bare builtin
descriptor only when the file does not rebind that name. Publish subtype
capacity only for a plain function or a single builtin staticmethod or
classmethod wrapper. Drop a no-op coverage guard, move the implicit
classmethod comment next to the code it describes, and bump the parse
cache to v121.

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

* fix(python): shadow builtin descriptors only by visible bindings

The whole-file identifier scan counted plain reads (`staticmethod(f)`),
`from builtins import staticmethod`, and bindings that run after the
decorator as rebindings. CPython evaluates a class-body decorator with
LOAD_NAME when the `def` runs, so none of those change which object the
decorator names. Methods decorated with the real builtin lost their
subtype call shape, and static methods lost their first parameter in
arity metadata.

Move decorator identity into builtin-descriptors.ts and count only
binding occurrences (assignment and loop targets, walrus, def/class,
parameters, import aliases, except/with/match captures, del, type
parameters, and wildcard imports) that are visible where the decorator
runs: the class body or module before the definition, a repeating
enclosing loop, any binding in an enclosing function, and any
global/nonlocal rebind.

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

* fix(python): model global and del like CPython's symbol table

`global x` and `nonlocal x` bind nothing; they redirect the declaring
function's own bindings of `x` to an outer scope. A bare declaration
was treated as an unconditional rebinding, so `@staticmethod` anywhere
in the file lost builtin recognition.

A module- or class-level `del` restores the outer lookup rather than
binding the name. Treat an unconditional `del` that runs after a
binding and before the decorator as undoing that binding. A `del`
inside control flow may not run, and a `del` inside a function still
makes the name local there.

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

* fix(python): order global rebinds and honor builtins re-exports

A function that declares `global staticmethod` and assigns it rebinds
the module name only when called, and it cannot be called before the
top-level statement that defines it runs. Treat such a rebind as
visible only when that statement precedes the decorator, or when the
decorator sits in a deferred class body. `nonlocal` rebinds stay
visible anywhere in the enclosing function.

`from builtins import staticmethod as staticmethod` binds the builtin
to its own name, so it no longer counts as shadowing.

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

* test(python): scope the descriptor-identity CPython claim

The wildcard-import case expects the fail-closed resolver verdict, not a
CPython outcome, because the imported module's exports are unknown.

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

* fix(python): resolve descriptor names as LOAD_NAME does

Model each binding by its effect on the namespace (Language Reference
4.2.1): an import that binds the `builtins` object itself (plain, aliased
to the same name, or `from builtins import *`) restores the builtin, a
module- or class-level `del` unbinds so lookup falls through, and any
other binding shadows. Resolve the decorator like LOAD_NAME (4.2.2):
the class namespace, then module globals, then builtins, each as it
stands when the `def` runs.

A restoring effect counts only as an unconditional simple statement that
runs before the decorator, so an import or `del` under `if`/`try` or a
loop stays fail-closed. A helper's `global` delete depends on whether
the helper is called, which the resolver does not model, so it keeps the
override.

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

* refactor(python): simplify decorator descriptor lookup

Derive the descriptor type and name set from one `as const` list,
replace indexed non-null assertions with destructuring, build the scope
chain without an assertion, and skip the enclosing-function owner lookup
when the decorator has no enclosing function. Key the stacked-decorator
verdicts by case name so a failure names its case.

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

* fix(python): resolve enclosing-function and deferred descriptor lookups

A class body reads a free name from the innermost enclosing function that
binds it (Language Reference 4.2.2). Evaluate that function's namespace
the same way as the class and module ones, so an unconditional
`from builtins import staticmethod` there resolves to the builtin. Any
other binding still shadows, including a local assigned only after the
class, which raises NameError rather than falling back to the builtin.

A class body inside a function runs whenever that function is called,
which can be any time after its top-level statement starts. Read module
state at that statement instead of after the whole module, so an earlier
`del` restores the builtin, and treat any later module override as
possibly visible.

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

---------

Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com>
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-29 08:12:52 +00:00
Gergő Magyar
0ef3f28d0e
fix(python): avoid receiverless subtype targets
Fixes #3396
2026-09-28 17:34:26 +00:00
Gergő Magyar
b20c8b6ef2
fix(python): guard decorated subtype targets
Fixes #3398
2026-09-28 17:01:22 +00:00
Gergő Magyar
c744ce1dfd
fix(python): resolve mixin calls with CPython C3 order (#3393)
* fix(python): resolve mixin calls with CPython C3 order

Breadth-first MRO bound a diamond mixin call to the wrong base, and dropping the site left the real method out of the graph. Use C3 and take the first compatible method in that order.

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

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

* fix(python): correct mixin receiver baseline counts

* fix(python): guard incomplete mixin inheritance

* fix(python): record unresolved MRO tail coverage (#3393)

Track a missing subtype target when the last indexed MRO owner has an unindexed parent, and cover the case with an integration test. Correct the C3 fixture description.

Note: local full npm test timed out amid parse-worker startup failures; focused tests and benchmark baseline passed.

---------

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-28 08:55:13 +00:00
EVA
f6e70016d6
fix(python): resolve mixin self calls to subtype implementations (#3390)
* fix(python): resolve missing mixin self members through subtypes

* fix: honor Python effective MRO and static mixin targets

* fix: bind Python subtype dispatch to receiver provenance

* fix(python): limit mixin fanout to instance receivers

* fix(python): capture call arity and invalidate stale parsed facts

* fix(python): count bound receivers by method context

Preserve static, free, nested and typed variadic parameters; test renamed target receivers without weakening incompatible-arity rejection. Regenerate capture goldens for receiver metadata and eight mixin fixtures. Record the deliberate missing_target coverage outcome: CI measured Python call drops 5->6 and total call drops 113->114; no shape or scaling threshold relaxed.

* test(python): require a call capture before checking unknown arity

* fix(python): bind subtype dispatch to receiver definition

* test(python): include conditional renamed-receiver target

* fix(python): prove positional mixin targets and report partial coverage

Preserve upstream notebook coordinate mapping and maintainer changes. Reject incompatible/implicit-class targets, retain proven targets across ambiguous alternatives, and report capped or unresolved coverage without confusing edge deduplication.

* fix(python): isolate subtype call proof and preserve lookup boundaries

---------

Co-authored-by: Eva <eva@100yen.org>
Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
2026-09-28 09:42:56 +05:30
Sravan Avvaru
86a39202cf
fix(#2965): system headers must not resolve to in-repo files (#3341)
* fix(#2965): system headers must not resolve to in-repo files

C and C++ use two syntactically distinct include forms:
  #include <x.h>   -- angle-bracket: search system include paths only
  #include "x.h"  -- quoted: search relative to the including file first

The old suffix-match fallback in resolveCImportTarget had no awareness
of this distinction, so a repo containing its own stdio.h would capture
every #include <stdio.h> and resolve it to the local file.

Fix:
- Add isSystem?: boolean to the wildcard variant of ParsedImportSyntax
- interpretCImport / interpretCppImport now set isSystem from the
  @import.system tree-sitter capture (present for angle-bracket form)
- resolveImportTarget in both cScopeResolver and cppScopeResolver
  short-circuits to null when context.parsedImport.isSystem is true,
  refusing to suffix-match system headers against workspace files
- Remove C and C++ from KNOWN_GAPS in the conformance test; add proper
  test cases with a parsedImport factory that distinguishes angle-bracket
  (isSystem:true) from quoted (isSystem:false) includes

Test: all 36 external-import-conformance cases pass, including the two
new c/cpp arms that were previously in KNOWN_GAPS.

* fix(#2965): update cpp-imports unit tests for isSystem field

Two test assertions were broken by the interpreter change:

1. Local include: the wildcard ParsedImport now always includes
   isSystem (false for quoted includes). Updated expected object
   to include isSystem:false.

2. System header: the old test asserted interpretCppImport returned
   null for system headers. The refactored design moves the null
   decision to the resolver layer (cppScopeResolver.resolveImportTarget)
   so the call graph and resolution stay separate concerns. The
   interpreter now returns { kind:'wildcard', isSystem:true } and
   the test name/assertion are updated to reflect this.

* fix(#2965): resolve C and C++ includes on search paths

Angle includes follow each translation unit's include roots, so a local
stdio.h no longer captures system headers or another file's -I list.

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

* Address PR review feedback (#3341)

- Accept in-repo include roots whose names start with `..` while still rejecting parent escapes.
- Correct the import-target bench comments so CONTEXT_LANGS and newPass match C/C++ header passes.

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

* fix(c,cpp): read include config per directory for monorepos (#2965)

Angle includes now resolve only against declared search roots, so config
read only at the repo root left monorepo sub-projects with nothing declared.

- compile_commands.json, compile_flags.txt, .ccls, .clangd and
  c_cpp_properties.json are read in every directory; a file takes the
  nearest one, and a nearer database's entry beats a shallower one (clangd).
- CMake include_directories / target_include_directories are read:
  directory-scoped and PRIVATE roots reach their subtree, PUBLIC and
  INTERFACE roots reach every file. ${CMAKE_CURRENT_SOURCE_DIR} and friends
  expand; unresolvable variables and generator expressions are dropped.
- Declared roots win: implicit include/Headers/inc roots apply only when no
  config speaks for the file, and never to a database-listed file.

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

* fix(c,cpp): give CMake PUBLIC includes only to linked targets (#3341)

target_link_libraries now decides who sees PUBLIC and INTERFACE roots, so an unrelated target no longer resolves another package's headers.

---------

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: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-27 13:40:54 +01:00
Gergő Magyar
51fb64c976
fix(swift): model Swift modules like the compiler (nested packages, Xcode targets, linear visibility) (#3387)
* fix(swift): discover nested Package.swift manifests for module grouping

A Swift monorepo laid out as Core/<pkg>/Package.swift has no root manifest
and no root Sources/, so every Swift file fell into one __default__ module.
The Swift resolver now walks the repo (bounded, skipping dot, ignored, and
Xcode bundle directories) and adds each nested package's targets, keyed by
repo-relative directory and ordered deepest-first so first-match grouping
picks the most specific target. Import resolution keeps the root view.

Refs #3355

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

* fix(swift): bound implicit IMPORTS edges by a total budget

Implicit same-module IMPORTS are n*(n-1) edges per module, and the graph
keeps every relationship in one Map capped by V8 at 2^24 entries. Emission
now fills a 4M-edge budget smallest module first and skips, with a warning,
any module that does not fit, so no module layout can crash analyze.

Refs #3355

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

* fix(swift): skip pairwise sibling passes for oversized modules

The target-siblings and sibling-type-bindings passes copy every file's
declarations into every other file of a module, so heap grows as n^2:
about 3.8 GB at 1,000 files with 15 defs each, about 15 GB at 2,000.
Modules over 1,000 files now skip both passes with a warning and resolve
through the global name fallback. GITNEXUS_SWIFT_MAX_MODULE_FILES changes
the ceiling.

Refs #3355

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

* test(swift): cover nested SwiftPM packages end to end

A fixture with three nested Package.swift manifests (two declaring a target
named Net) and no root manifest. Each target gets its own implicit IMPORTS,
none cross packages, and Config() resolves to the caller's own package.
The Swift capture golden and scope-capture fingerprint grow with the new
fixture corpus only.

Refs #3355

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

* fix(review): apply review findings

- Rebase nested target paths with the existing Zig path helpers, which also
  reject Windows drive paths and paths that resolve to the repo root (whose
  empty prefix would group every file into one target).
- Document GITNEXUS_SWIFT_MAX_MODULE_FILES in the README env table.
- State that skipped modules resolve through lower-confidence fallback edges,
  why nested-type fragments still run for oversized modules, and fix a stale
  loader name in the target-grouping header.

Refs #3355

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

* test(swift): cover every skipped Xcode bundle suffix in the package walk

Refs #3355

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

* fix(swift): model modules the way the compiler does

Replace the caps from the first round with representations that stay
linear, and derive module identity from the same sources the compiler uses.

- Same-module visibility is one File -> Module IMPORTS edge per file
  (reason `module-membership`) instead of an edge per ordered file pair.
  Incremental importer expansion treats files sharing a Module hub as
  importers of each other. The 4M edge budget is gone.
- Sibling declarations and type bindings live in one shared table per
  module in the namespace channel C# uses since #1871, instead of being
  copied into every file. The 1,000-file ceiling and
  GITNEXUS_SWIFT_MAX_MODULE_FILES are gone.
- Modules come from root and nested SwiftPM manifests (Sources, Source,
  src, srcs; plugins under Plugins; the newest Package@swift-X.Y.swift)
  and from Xcode native targets in project.pbxproj, including Xcode 16
  synchronized folders. Target paths are matched from the repo root, so
  a vendored copy of the same layout is no longer grouped into a root
  target (this reverses #2931's floating match).
- `import X` resolves to the modules named X instead of any folder named
  X. A name no module carries is external when every manifest and
  project was read completely.
- The global-name-fallback veto uses the same membership, so test
  targets, custom-path targets and Xcode targets have module identity.

Refs #3355

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

* fix(bench): move the Swift package bench to module grouping

The bench still imported the removed groupSwiftFilesBySpmTarget. Its
first-wins probe keeps its meaning under root-anchored grouping: the
clash file sits under Sources/Mod0, and the Sources/Mod1 further down its
path is a vendored copy.

Plugins are now non-importable modules under Plugins/, so parse_targets
counts importable source targets (still 3) and parse_binary_skipped also
checks that the plugin is recorded that way. Baseline values unchanged;
the notes say why the definitions moved.

Refs #3355

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

* fix(swift): match compiler module names, manifests, and target filters

- Module names follow the compiler's c99 mangling (`my-lib` imports as
  `my_lib`); Xcode targets use a literal PRODUCT_MODULE_NAME or
  PRODUCT_NAME when the project sets one.
- Package manifests are one-file modules, as SwiftPM compiles them. Files
  outside every target are one-file modules once every manifest and
  project was read; otherwise they keep the shared __default__ module.
- Xcode 16 synchronized-folder exceptions add a file to a target that
  does not list the folder, and remove it from one that does.
- SwiftPM `sources:` / `exclude:` narrow a target; a computed list marks
  the manifest unreadable.
- The default target folder is chosen once per package, as SwiftPM does,
  instead of per target.

Refs #3355

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

* Address PR review feedback (#3387)

- Inferred Swift folders keep SwiftPM's first predefined parent when a
  target name repeats (Sources before srcs).
- The pbxproj parser rejects a \U escape without four hex digits instead
  of decoding garbage, so the project reads as incomplete.
- Extension owners are stamped in every Xcode membership of a shared file.
- A plugin name never reaches a plugin: neither the fallback veto nor the
  folder-index fallback treats a known non-importable module as imported.
- A file an Xcode target compiles keeps that membership even when it also
  lies under a SwiftPM target directory.
- The workspace scan bounds the queue, not only the directories read.
- Integration tests require each module hub to exist before comparing.

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

---------

Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com>
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-27 06:24:35 +01:00
azizur100389
5fc518d2cb
fix(dart): resolve package imports by pubspec identity (#3369)
* fix(dart): resolve package imports by pubspec identity

* fix(dart): keep package-identity edges out of the cycle check

Pubspec identity edges invalidate importers when a manifest changes. They cannot form an init cycle, so the cycle query excludes them before the row cap. Discovery reads each manifest once, with a size bound, and resolution shares one package-URI parser with those edges.

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

* Address PR review feedback (#3369)

- Reject package URIs with an empty library path so they do not emit identity edges
- Skip the pubspec permission test where chmod cannot deny reads
- Document that the Dart heap probe is not a uniqueTarget spelling

Note: pre-existing failure in test/unit/incremental-index-extension-dml-gate.test.ts not addressed by this PR.
Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(dart): list package directories through a no-follow descriptor

A directory replaced by a symlink between the parent listing and the next visit must not be traversed. The walk opens it with O_DIRECTORY|O_NOFOLLOW and lists that inode.

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

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

* fix(mcp): keep the Dart identity reason out of MCP startup

The cycle query still excludes the same reason string. The constant now lives with the other non-initializing import reasons, so MCP startup does not load a language provider.

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

* Address PR review feedback (#3369)

Open discovered pubspecs and child directories through the parent directory inode on Linux, so replacing that directory with a symlink cannot redirect the walk.

Note: pre-existing failure in test/unit/incremental-index-extension-dml-gate.test.ts (worker pool startup timeout) not addressed by this PR.
Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(dart): reject Windows junctions during pubspec walk

* Address PR review feedback (#3369)

Refuse pubspec discovery that cannot set O_NOFOLLOW, and verify macOS child opens against the pinned directory chain instead of reopening a mutable path.

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

* fix(dart): bound live pubspec descriptors and close the macOS check-then-open

A deep directory chain held one descriptor per level until open failed with EMFILE, and macOS child opens statted the path before using it. Refuse the next directory at 64 live handles, and stat only the descriptor opened with O_NOFOLLOW.

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

* fix(dart): open pubspecs non-blocking so a FIFO cannot hang discovery

A listed pubspec can be replaced by a FIFO before open. O_RDONLY alone waits inside open for a writer, so the file-type check never runs. O_NONBLOCK returns immediately and the walk rejects the non-regular file.

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

* fix(dart): cap names read from each pubspec directory

readdir kept every entry before the visit budget could run, so one huge directory could allocate without bound. Read the listing one name at a time and fail closed past 100,000 entries.

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

* fix(dart): reject a pubspec that grows while its descriptor is read

The size cap was taken from the stat before the read, so a file that grew in that window could be parsed from a short prefix. Re-stat the same descriptor afterward and fail closed when the size no longer matches the bytes captured.

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

* fix(ci): rebaseline the Dart scope-capture fingerprint for package-import fixtures

The benchmark hashes every dart-* fixture. The new package-import corpus adds six Dart files and 33 capture groups. Parking that directory restores the previous fingerprint, so this is corpus growth, not a capture change.

Co-authored-by: Cursor <cursoragent@cursor.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-26 13:21:09 +01:00
Gergő Magyar
e06c2dd13e
fix(impact): fail closed on id-less targets and follow ??/||/?: callable values (#3354) (#3373)
* fix(impact): fail closed on id-less targets and follow ??/||/?: callable values (#3354)

#3354 reports `impact` returning a byte-identical 1037/CRITICAL/`exact`
result for three unrelated targets, with only `target.name` differing.
The reporter's `target` had no `id` and no `filePath`, which the traversal
path always emits, and a synthetic reproduction of their monorepo (pnpm,
Cloudflare worker-configuration.d.ts in five packages, Hono, a Durable
Object) resolves every target correctly on main. So the identical result
was not reproduced. The repro did surface two real gaps and one hardening
point:

- `_runImpactBFS` now throws when the target has no node id. Every
  caller already catches, so impact reports `impactedCount: null,
  risk: UNKNOWN` instead of a normal-looking blast radius that cannot be
  about this symbol.
- Callable-value flow followed only a single designator on the RHS, so
  `const sweep = env.__sweep ?? runSweep; await sweep(env)` produced no
  flow and `scheduled` was missing as a caller of `runSweep`, while the
  result still claimed `epistemic: exact`. Each branch of `??`, `||`,
  `or`, and `?:` now flows into the binding (language-neutral: operator
  and field names, no language checks). Parse cache bumped 104 -> 112
  (105-111 are claimed by open PR #3326).
- A whitespace-only `target_uid` (strict adapters materialize omitted
  optional strings) is treated as omitted and falls back to the name.

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

* fix(mcp): treat a non-string uid as omitted instead of throwing (#3354)

Review follow-up on #3373. `query.uid?.trim()` called `.trim()` on a
client-supplied value, and the MCP envelope is not type-validated, so
`context({uid: 42})` threw a TypeError that `context()` does not catch.
Before #3373 the same input ended as a structured not_found.

A non-string uid now counts as omitted, which matches how
normalizeToolParams already treats a non-string `target_uid`. The impact
and trace not-found messages print a trimmed uid only when it is a
non-blank string, so a strict adapter sending " " sees the name it
searched for instead of `' '`.

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

* fix(ingestion): expand ??/?:/ternary branches through a provider hook (#3354)

Review follow-up on #3373. The shared value-alternatives rule keys on
tree-sitter field names, and several grammars spell the same construct
differently, so the expansion never fired for them:
- Kotlin `elvis_expression` has no fields.
- Swift uses `value`/`if_nil` and `if_true`/`if_false`.
- Dart uses `first`/`second`, and its conditional has no `condition` field.
- Python `a if c else b` has no fields.
The `'?:'` operator entry was dead, since no bundled grammar emits it.

Add an optional `valueAlternatives` hook to CallableFlowCaptureOptions,
consulted before the shared rule, and implement it in the Kotlin, Swift,
Dart, Python and Ruby providers. Shared code still names no language.

Ruby's statement-bodied `if`/`unless`/`elsif` also carries
`condition`/`consequence`/`alternative`, so the shared ternary rule dug
an identifier out of an arbitrary statement (`g = h; 0` flowed `h`) and
produced a wrong CALLS edge. The Ruby hook now keeps a multi-statement
branch as one opaque source, as before #3373.

Tests: new provider fixtures for Python, Kotlin, Swift, Dart and Ruby
(including a Ruby negative), and TS chain, parenthesized, callable-left
and `&&` negative cases. Captures goldens gain one entry each for the
new fixtures. SCHEMA_BUMP stays 112 (same unreleased PR).

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

* fix(ingestion): keep long ||/??/?: chains linear in callable-flow capture (#3354)

Expanding value-selecting sources into per-branch flows made long chains
super-linear and, past a few thousand operands, a stack overflow. A
generated `w === "k0" || w === "k1" || ...` keyword table took 6.7 s at
1000 operands and 48 s at 2000, and threw RangeError at 8000.

Two causes, both fixed without changing any emitted capture:

- valueAlternatives recursed once per operator level and spread the
  partial results at every level. It is now an explicit stack that
  writes to one output array. The left-to-right order, the paren
  unwrapping, and the provider-hook contract (`[node]` means opaque) are
  unchanged.
- Every alternative ran its visibility walks from its own leaf, which
  can be as deep as the chain is long, all the way to the root. Also,
  tree-sitter's `parent` re-descends from the root, so each step costs
  the node's depth. The walks now jump between "anchor" nodes, the only
  nodes any check can match: region ids and formal owners. The nearest
  anchor is memoized per node across the file, and parents come from a
  map recorded by the one DFS the synthesizer already does.

After the fix: 0.34 s / 0.23 s / 1.6 s at 1000 / 2000 / 8000 operands.
That is within about 1.2x of main without the expansion; what remains
is tree-sitter query time. Capture fingerprints are byte-identical
before and after the fix for all 16 scope-capture bench languages and
for the Python harness.

A `typescript-deep-chain` case added to the scope-capture bench guards
the scaling: 1.11-1.22 now, 7.5-8.0 before. A unit test pins a
10000-operand chain: it must still yield the seed for a callable
operand, the copy for a formal at the deepest leaf, and the invoke.

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

* fix(kotlin): see through braced if-branches in callable alternatives (#3354)

tree-sitter-kotlin wraps a braced branch as
`control_structure_body > statements > <expr>`, so for any non-empty
block kotlinValueAlternatives saw exactly one named child (`statements`)
and pushed the wrapper itself as the branch value. operandSyntax emits
nothing for a `statements` node, so
`val run = if (c) { ::f } else { ::g }; run()` produced no flow edges,
and the "multi-statement block stays opaque" guard could never fire.

Descend one level through `statements` and require exactly one
non-comment expression there. Empty blocks (`{}` has no named children),
multi-statement blocks, and `if` without `else` still return the whole
`if` as one opaque source. The doc comment now describes that.

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

* fix(ruby): skip only the multi-statement branch in callable alternatives (#3354)

rubyValueAlternatives returned `[node]` (opaque) as soon as one branch
held more than one statement. Two things went wrong because of that:

- At the top level, `run = if c then g = h; 0 else method(:f) end`
  dropped the single-statement `else`, so `f` got no flow edge.
- In an elsif chain, the outer `if` pushed the `elsif` node as a branch.
  The shared loop called the hook on it again, got `[elsif]` back, and
  used the whole elsif subtree as one source. So in
  `if a then method(:run_a) elsif b then g = h; 0 else method(:run_b) end`
  the `run_b` edge was lost.

The hook now walks the elsif chain itself and skips each multi-statement
branch, while every single-statement branch still becomes an
alternative. This cannot add a wrong edge: each emitted alternative is a
value the conditional really evaluates to, and nothing is taken from the
skipped branch. Before, that branch did not contribute a resolvable
value either. A whole conditional used as a source becomes a
qualified-name seed, which resolves to nothing when it has more than one
identifier leaf. With a single identifier leaf, it could even seed the
condition variable. When no branch is a single statement, the
conditional still stays one opaque source, as before.

Ruby captures golden: ruby-callable-alternatives/app.rb goes from 33 to
58 capture groups. 23 of them come from the new fixture functions
(checked against the old source). The other 2 are the new branch
alternatives, `statement_if -> run_sweep` and `elsif_chain -> run_b`.

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

* fix(ingestion): flow the right operand of && / and into callable bindings (#3354)

`a && b` / `a and b` yields `a` when it is falsy and `b` otherwise. A
falsy value is never a callable, so the right operand is the only one
that can be invoked later. The capture left `&&` unexpanded, and the
compound source became a qualified seed that resolves to nothing:
`const run = x && f; run()` gained no edge to `f` while impact claimed
`exact`. Python `x and f or g` reached only `g`.

The shared expansion now maps `&&` / `and` to the right branch only.
It recurses, so `x and f or g` reaches both `f` and `g`. Where `&&`
yields a boolean (Java, C#, Go, Rust, C, C++, PHP, Zig), the
destination cannot be invoked, so the flow never meets a call. A
before/after CALLS diff over all 83 lang-resolution fixtures that
contain `&&` or `and` shows exactly one new edge,
logicalAnd -> runAndRight. PHP and Ruby bind low-precedence `and`
looser than `=`, so `$g = $x and $y` never reaches this rule.

The Python golden digest changes only for the extended
python-callable-alternatives fixture.

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

* fix(ingestion): keep operator branches of a value-selecting source opaque (#3354)

The fan-out sent every branch of `??` / `||` / `or` / `?:` to
emitAssignmentFact on its own, including branches that compute a value.
`Handlers.fallback === run || fb` then emitted the comparison as a seed
whose qualified text sliced to receiver `Handlers`, member `run`, and
resolveBoundMemberCandidates minted a CALLS edge to `Handlers.run`. The
same happened for `this.state !== run ?? this.fallback`,
`this.state + run || this.fallback`, and Python
`self.state != run or self.fallback`. Before the fan-out, the whole
compound source was one opaque seed.

A branch that is a binary operator expression now contributes nothing.
The check uses the field vocabulary valueBranches already reads (a
`left`/`right` pair, or an `operator`/`operators`/`op` token after the
expression start), not grammar type names. Member accesses that field
their `.`/`->` as `operator` (Ruby `call`, C/C++ `field_expression`) also
field a member name through the list memberParts uses, now shared as
memberNameNode, so they stay designators. Unary `&f`/`*fp` lead with
their operator and stay designators. Call results were already dropped
by emitAssignmentFact, and lambdas and callable references are unchanged.

CALLS-edge diff over 87 lang-resolution fixtures (505 -> 501 edges):
only the four false edges above were removed, and none were added.

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

* test(ingestion): pin the LEFT operand of ?? / or / elvis per provider (#3354)

The Python, Kotlin, Swift and Dart "override ?? fn" cases put an
unresolvable parameter on the left, so a fan-out that kept only the
last operand still passed them. Each fixture now adds a callableLeft
case with a real function on the left and a parameter on the right
(`run_left or fallback`, `::runLeft ?: fallback`, `runLeft ?? fallback`),
and each language asserts `callableLeft → runLeft`.

Mutation check: dropping the left branch from the shared `??`/`||`/`or`
rule and making the four provider hooks return only their last operand
fails all four new tests, while the existing right-operand tests still
pass.

Capture goldens were regenerated with UPDATE_GOLDEN=1:
- python app.py: 35 -> 85 groups. 35 -> 75 is stale drift from
  8ad0d8db7, which added the comparison cases without regenerating.
  75 -> 85 is the new case: 2 declarations, 2 scopes, the variable,
  the `run()` reference, and 4 callable-flow captures (seed -> run_left,
  copy <- fallback, formal, invoke).
- swift App.swift: 25 -> 36 groups for the same new case, plus
  Swift's type-binding for the fallback parameter.

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

* test(bench): re-baseline scope-capture fingerprints for #3354 callable alternatives

This PR makes a callable chosen by a value-selecting source flow every
branch it can yield (??, ||, or, && right operand, ternary, statement
if, elvis). It also keeps an operator branch opaque. Both change
@callable-flow captures, and the PR adds *-callable-alternatives
regression fixtures that these benches glob.

To verify, the BASE (merge-base 233ca2849) and HEAD emitters were run
over the same HEAD fixture corpus plus the synthetic source. Every
added or removed match is an @callable-flow.* match on one of those
sources. The pre-existing corpus is byte-identical for all 16
scope-capture languages and for Python. Emitter delta, then corpus
growth:

- ruby:       +5/-0   app.rb (+1 file, +58 groups)
- swift:      +6/-3   App.swift (+1 file, +36 groups)
- dart:       +6/-3   app.dart (+1 file, +33 groups)
- kotlin:     +8/-2   App.kt (+1 file, +71 groups)
- typescript: +18/-14 4 files (+288 groups)
- python:     +11/-7  app.py (+1 file, +85 groups)

The removed matches are whole-expression seeds and copies that carried
a qualified name of the compound source. They are replaced by one
seed or copy per operand. Synthetic scaling counts are unchanged and
every scaling ratio stays under budget. Per-language notes are in
baselines.json under _rebaselined_3354_callable_alternatives.

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

* refactor(mcp): share one blank-uid check across impact, trace and symbol lookup (#3354)

The "trimmed uid, or omitted when blank/non-string" rule was inlined four
times, three of them trimming twice. nonBlankUid() owns it now; behaviour is
unchanged. The whitespace and empty target_uid tests collapse into one it.each.

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

* docs(cfg): correct the Kotlin and Python grammar-field notes (#3354)

The Kotlin CFG visitor claimed no control-flow node has fields; a parse of
the vendored grammar shows if_expression fields condition/consequence/
alternative (when/for/while/do/try/elvis are fieldless). The Python harvest
note listed conditional_expression's children like field names; the node is
fieldless and they are positional. Both notes were misleading reviewers of
the #3354 value-alternatives hooks. Comment-only.

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

* test(ingestion): skip optional-grammar suites in callable-alternatives providers test (#3354)

Kotlin, Swift and Dart grammars are optional installs. Guard their describe
blocks with isLanguageAvailable, as swift.test.ts and dart.test.ts do, so an
install without one of them skips instead of failing.

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

* test(ingestion): probe the Dart parser before enabling its providers suite (#3354)

isLanguageAvailable only proves the module loaded; tree-sitter-dart can still
fail on setLanguage. Probe loadParser/loadLanguage and skip on failure, the
same guard dart.test.ts uses.

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

---------

Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com>
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-25 21:43:24 +01:00
Adam B.
7376347058
feat(ingestion): add tRPC procedure detection and MCP chain/route surfacing (#3339)
* feat(ingestion): tRPC pattern detection — procedures, curried calls, route extraction

Three changes to properly index tRPC router files:

1. HOC-in-pair patterns: detect procedures like
   'create: procedure.mutation(async ({ input }) => {...})'
   as named Function nodes (pair > call_expression > arguments > arrow_function)

2. Curried call detection: capture 'workflow(db)(input)' chained calls
   where call_expression.function is itself a call_expression

3. tRPC route extraction: new route-extractors/trpc.ts detects
   .query()/.mutation()/.subscription() procedures, maps to /trpc/* routes
   with prefix inference from router variable names

4. Function/Const dedup: structural check skips Const nodes when
   variable_declarator value is arrow_function/function_expression

5. Process-route linking: match by (filePath, methodName) instead of
   filePath only, preventing shared flows across procedures in same router

(cherry picked from commit eced15e60a39a181a48bff8c5bbe278d26aa53b8)

* fix(route-extractors): line-by-line scanner for chained tRPC procedures

Original regex only matched direct patterns (list: proc.query()) but not
chained patterns (create: proc.input(z.object({...})).mutation()). Only
41/256 routes were detected on Jurialis.

Rewrote extractTrpcRoutes() as a line-by-line scanner that:
- Detects procedure keys starting with *Procedure builders
- Tracks currentProcedure forward until terminal .query()/.mutation()
- Handles .input() chaining naturally
- Deduplicates via seen Set on procedurePath

Result: 245 routes from 30 routers (was 41), 256 total after re-index.
(cherry picked from commit 49edf953e93e6b67b77932e866fcbfb8d089b1f3)

* feat(mcp): expose tRPC chains via context/query tools

Eight fixes to make the tRPC route->procedure->workflow->sub-workflow chain
visible through the standard MCP tools (context, query) that AI agents use,
instead of requiring raw Cypher queries.

- A context: order incoming/outgoing CALLS test-last, raise LIMIT 30->100
- B query: batched ENTRY_POINT_OF lookup, surface route URL on processes
- C query: mark process_symbols entry point with is_entry_point=true
- D entry-point-scoring: skip UTILITY_PATTERNS (get*/set*) penalty for
  symbols in tRPC router files so framework boost (3.0x) is preserved
- E fts-schema: index Route nodes so /trpc/* URLs are keyword-searchable
- F tools: bump max_symbols default 10->25 to fit procedure->workflow chain
- G context: optional chain_depth (0-3) param walks CALLS edges in both
  directions and returns layered chain field
- H LadybugDB bug workaround: WHERE r.type IN [...] silently drops edges
  on relationship properties; replace with OR chains (7 occurrences in
  local-backend.ts, pdg-impact.ts, graph-queries.ts)

Validated on Jurialis: setProviderCap procedure now appears as caller of
setProviderCapWorkflow, /trpc/cabinet.setProviderCap Route is searchable,
chain_depth=3 returns the layered call graph.

(cherry picked from commit c24df0adce91e80f191a5c5d2e8b14e9da677366)

* feat(mcp): surface is_entry_point + routes in context, raise query limit

context() is the mandatory pre-edit tool (AGENTS.md). Until now an agent
had to issue a separate query() call just to learn whether its symbol is
a process entry point or which HTTP route it handles. These two fields
make context() self-sufficient for the bmad-dev flow.

- context: query STEP_IN_PROCESS now returns p.entryPointId; new
  ENTRY_POINT_OF lookup attributes routes only from processes where the
  symbol is the entry point (middle steps do not own the route) plus any
  direct Route->symbol handler edge (tRPC procedures outside any Process)
- context: new top-level is_entry_point (true only) and routes[] fields
  ({url, method?}), emitted only when non-empty to keep the diff additive
- query: raise default limit 5->10 so dense domains (tRPC action router
  with 20 chains, data-export with 9 flows) surface more of their flow
  set without an explicit param

(cherry picked from commit 004f5b0ba985fcef3dfac5e67a2f2f7262184215)

* docs(mcp): document chain_depth, routes, and entry-point flag; neutralize examples in comments

* fix(mcp): address code-review findings on tRPC entry points, queries, and dispatch

- entry-point scoring: optional-chain framework detection and normalize
  path separators before router-pattern matching (P1 crash on .js routers)
- tRPC extractor: emit controllerName as null (callers resolve via
  lookupClassByName; a router object stringified as name corrupted lookups)
- route dedup: key seenRoutes by method:url so GET/POST pairs survive
- legacy TS queries: drop curried-call patterns (arity-corrupting for
  overload resolution) and require non-array callee on pair member calls
- registry-primary TS query: mirror the non-array-callee predicate on the
  new pair member-expression patterns (fixes query compile error)
- group tool port: forward chain_depth into per-tool context args

* fix(mcp): nested tRPC router paths, controller-less route binding, query chain_depth

- trpc extractor: brace-depth nesting stack composes sibling router paths (bare router() import style included); merge-prefix dot normalization; strict publicProcedure allowlist gate; drop phantom router metadata

- call-processor: bind controller-less tRPC routes to same-file handlers via exact single-match symbol lookup; ambiguous or missing handlers skipped

- query/group query: optional chain_depth (0-3) enriches ranked processes with their context chain; fix _computeContextChain layer docstring

- tree-sitter TS/JS scope queries capture string-key function declarations; tRPC pattern scoring narrowed to server router files

- parse-cache schema bump to v103 for extractor and route-binding changes

- add trpc route extractor regression tests (9 cases incl. sibling nesting and merge prefix)

* Address PR review feedback (#3339)

Close remaining review threads: compact tRPC keys, comment-safe terminals,
quoted-key identifier HOC captures, group-query default alignment, and
LadybugDB label scalars on context chains.

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

* test: pin PARSE_CACHE schema bump to 103 (PR #3339 review fixes)

* fix(ingestion): bind same-name tRPC handlers and surface HANDLES_ROUTE

Same-name procedures resolve by startLine, the extractor keeps nested paths
through multiline schemas, and context/query UNION HANDLES_ROUTE for leaf
procedures that never become Process entries.

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

* Address PR review feedback (#3339)

Clamp group query bounds, drop HOC pair false positives, emit tRPC
terminal lines and hyphenated quoted keys, cap chain BFS concurrency,
and restrict the router utility exemption to accessor names.

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

* Address PR review feedback (#3339)

Score JS/JSX tRPC routers like TypeScript, ignore inner db.query and
unrelated .merge calls, mask regex braces, and assert clamp tests invoke
the query mock.

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

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

* fix(ingestion,mcp): emit-side HOC callback guards + MCP schema prettier (PR #3339 round 2)

* Address PR review feedback (#3339)

Treat `/` after return-style keywords as a regex, drop the synthetic
appRouter path prefix, and bind the create mutation via an inline callback.

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

* Address PR review feedback (#3339)

Qualify query() docs so is_entry_point is promised only when the entry
symbol is among the search hits.

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

* Address PR review feedback (#3339)

Emit tRPC terminals only at procedure paren depth, drop the filename
prefix on bare appRouter = router(), mask regex after if (), and cap
groupQuery member fan-out.

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

* Address PR review feedback (#3339)

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

* Address PR review feedback (#3339)

De-duplicate context-chain BFS nodes reached through multiple frontier
edges, and bind tRPC identifier callbacks (`.mutation(handler)`) to the
handler symbol instead of the procedure key.

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

* Address PR review feedback (#3339)

Apply LIMIT 50 after DISTINCT neighbors in the context-chain BFS, and
treat the official lowercase `procedure` builder as a tRPC procedure key.

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

* Address PR review feedback (#3339)

Keep the later duplicate tRPC object-literal key so route binding matches
the handler JavaScript actually ships.

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

* Address PR review feedback (#3339)

Compose same-file identifier-mounted tRPC subrouters so admin: adminRouter
emits the live admin.list path instead of an unprefixed /trpc/list.

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

* Address PR review feedback (#3339)

Memoize tRPC identifier-mount paths so a depth-N chain stays linear, and
gate it with the build-free measure.mjs / baselines.json harness.

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

* Address PR review feedback (#3339)

Compose identifier mounts without phantom /trpc URLs, bind wrapped and
multiline handlers, fail-close missing bench budgets, and reject query
page bounds before search.

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

* Address PR review feedback (#3339)

Reject advertised MCP query bounds in group mode and chain_depth
instead of clamping or accepting non-integers.

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

* Address PR review feedback (#3339)

Reject invalid groupContext chain_depth and ignore non-callable
same-file tRPC handler candidates.

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

* Address PR review feedback (#3339)

Memoize live tRPC mount paths so the depth-N chain bench stays linear,
and render invalid group/MCP bounds without throwing.

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

* Address PR review feedback (#3339)

Keep t.merge('prefix.', namedRouter) procedures live by recording a
zero-hop mount so the unmounted-router drop does not hide them.

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

* Address PR review feedback (#3339)

Recognize type-annotated `const adminRouter: AppRouter = t.router(`
bindings so identifier mounts still compose.

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

---------

Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-21 09:44:42 +01:00
Twisted_Arrow
dcb2eb5cb4
fix(swift): resolve imports from Package.swift targets, not path segments (#3105)
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
* fix(swift): match repeated SPM target prefixes

* test(swift): cover repeated SPM target prefixes

* test(swift): cover valid prefix before later partial match

* docs(swift): clarify target grouping parity scope

* docs(swift): clarify target grouping parity scope

* docs(swift): clarify target grouping parity scope

* fix(swift): resolve imports from Package.swift targets, not path segments

Stop fabricating IMPORTS from import Foundation onto a same-named folder.
Declare modules from Package.swift when the manifest is usable; keep
Sources/* for grouping and fail-open folder resolve minus SDK names.

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

* fix(swift): keep empty Package.swift declarations and nested .target() deps external

An inferred Sources/* folder is grouping-only. A dependency .target(name:) is not a module. Treat both as unresolved so import Foundation cannot bind to a decoy folder.

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

* fix(swift): keep implicit IMPORTS intra-group and gate linear Package.swift resolve

@_exported must not paint sibling files as implicit imports. A dedicated
bench pins declaration-only resolve and (t_4n/t_n)/4 linearity.

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

* Address PR review feedback (#3105)

Honor member-only @_exported imports, skip comments while scanning
Package.swift factories, fail-open mixed helper-built target lists, and
block CoreData/CoreGraphics decoy folders on the inferred path.

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

* Address PR review feedback (#3105)

Match path: "." as the package root, skip block-commented Package.swift
factories, read import kind from the clause only, and skip capture tests
when the optional Swift grammar is missing.

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

* fix(ci): scan Swift import-kind without nested regex backtracking

CodeQL js/redos flagged the comment-skipping IMPORT_KIND_RE; a linear walk keeps the same kind tokens.

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

* fix(ci): linear Package.swift factory scan and gate Swift context

parseSwiftPackageManifest re-walked every prefix for comments (O(n²) in factory count). Resume the scan and cover nested factories in one pass. Wire Swift into the import-target context arm now that resolveImportTarget is 5-arg.

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

* Address PR review feedback (#3105)

Tighten Package.swift and import-text scanners: skip comments/strings, reject escapes, treat ident + [ as incomplete, and drop the unused factory-comment wrapper.

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

* fix(ci): prettier the @_exported availability fixture

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

* Address PR review feedback (#3105)

Judge Package.swift completeness from Package(...)'s own targets: argument instead of raw-text regexes, nest block comments when reading an import kind, and strip leading ./ from declared target paths.

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

* Address PR review feedback (#3105)

Collect Package.swift factories only from Package(targets: [...]), fail-open on computed array elements, and treat // after a label colon as a comment.

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

* Address PR review feedback (#3105)

Ignore stray factories when Package() exists but omits targets:.

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

* Address PR review feedback (#3105)

Require the Package-scan seen box so the always-true undefined guard goes away.

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

---------

Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-19 15:50:31 +01:00
azizur100389
a578747455
fix(swift): resolve nested constructors in extensions (#3308)
* fix(swift): resolve nested constructors in extensions

* fix(swift): preserve qualified extension owners

* Address PR review feedback (#3308)

Recover qualified Swift extension owners through public / attribute prefixes, and stop last-dot-guessing when source text is present.

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

* fix(swift): keep attribute text from stealing extension owners

Bound header recovery so @available messages cannot rekey a fragment, and still inject nested types when the Class scope has no bindings.

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

* fix(swift): nest comments and keep the public extension fixture valid

Review follow-up: skip nested /* */ in the header scan, put the Inner.Entry decoy in a parsed file, and mark Outer/Container/Entry public so the live fixture is valid Swift.

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

* fix(swift): recover Unicode identifiers as extension owners

The header regex was ASCII-only, so extension Café.Container keyed as Caf and dropped nested-type siblings. Match ID_Start/ID_Continue segments instead.

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

* fix(swift): skip raw strings and decode UTF-8 scope columns

Header recovery treated #"..."# as an ordinary quote and sliced Tree-sitter byte columns as JS offsets, so a same-line Café prefix or a raw attribute message could steal or drop the extension owner.

Co-authored-by: Cursor <cursoragent@cursor.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-19 09:46:53 +01:00
Parafee41
795cf0e151
fix(swift): resolve inherited protocol extension calls (#3309)
* fix(swift): resolve inherited protocol extension calls

* fix(scope): gate inherited implicit receiver lookup

* fix(swift): resolve call result types by exact callee

* test(swift): align cache and local call expectations

* fix(scope): reconcile replay diagnostics

* fix(swift): preserve exact callable return types

* fix(swift): capture throwing async call results

* test(swift): refresh capture golden

* test(swift): refresh scope capture baseline

* fix(swift): require explicit callable returns

* fix(scope): preserve duplicate return metadata

* chore(scope): align index documentation

* Address PR review feedback (#3309)

Stamp only protocol/class-extension members (nested QN + SPM buckets),
arity-narrow implicit-this across MRO, and keep Swift type peeling out
of shared workspace-index via stripTypePreservingDecoration.

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

* Address PR review feedback (#3309)

Stamp extension members even when the extension declares a nested type, keep inherited class members ahead of protocol-extension defaults, and report replay-only interface-dispatch fan-out drops.

Note: pre-existing failure in gitnexus tsc against an older gitnexus-shared dist not addressed by this PR.
Co-authored-by: Cursor <cursoragent@cursor.com>

* Address PR review feedback (#3309)

Walk inherited implicit-this owners nearest-first so a nearer override wins, and tighten Swift owner-stamp tests.

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

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

---------

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-19 07:44:35 +01:00
Abhinav Pandey
0ede6ae501
fix(rust): respect Cargo target boundaries in name fallback (#3294)
* fix(rust): respect Cargo target boundaries in name fallback

* fix(rust): read Cargo sources through validated file descriptors

* fix(rust): require matching imports for crate-root guesses

* fix(rust): require Cargo root identity for cross-target imports

* fix(rust): enforce Cargo identity across fallback imports

* fix(rust): keep Cargo membership on typical derive/macros (#3294)

Abort only genuine unknown expansion, ignore Cargo artifact layouts instead of every `target` path segment, and require a covering import for unanswered crate-root name fallback so #3253 still holds on ordinary Rust sources.

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

* test(rust): gate Cargo target membership in CI (#3253)

Add a parse-dispatch-rounds-style bench so derive/macro abort, target-path globs, and superlinear membership walks fail in CI instead of staying graph-invisible.

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

* fix(rust): verify Cargo macro and re-export evidence

* style(bench): format Cargo membership benchmark

* fix(rust): require imports for cross-file root guesses

---------

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-16 11:36:07 +01:00
Yahoo
0edf9ce0ff
fix(ruby): guard gem requires with dependency metadata (#3096)
* docs(plans): add ruby gem require boundary plan

* fix(ruby): guard gem requires with dependency metadata

* fix(ruby): scope gem sources by manifest

* test(ruby): model resolved lockfile specs

* fix(ruby): stop local gem suffix fallthrough

* docs: remove Ruby resolution plan

* test(ruby): gate gem resolution correctness and scaling

* test(ruby): align gem benchmark baseline with ratio gates

* Address PR review feedback (#3096)

- Strip a trailing .rb so local and external gem prefix matching follows Ruby's optional-suffix require.

Co-authored-by: Cursor <cursoragent@cursor.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-10 16:04:11 +01:00
Kevin Rajan
3236e2fbcd
fix(cobol): prefer copybook dirs so COPY EXTERNAL does not hit vendor decoys (#3240)
* fix(cobol): prefer copybook dirs so COPY EXTERNAL does not hit vendor decoys

COPY of an out-of-repo member first-won any same-named .cpy, so
vendor/EXTERNAL.cpy became a live cobol-copy IMPORTS edge. Share one
resolver between census and the regex processor: prefer copybooks/cpy/copy
plus the importer dir when present, else fail-open. Drop COBOL KNOWN_GAPS.

Fixes #2967

* bench(cobol): update depth budget for copybook-dir preference (#2967)

COBOL resolver now prefers well-known copybook directories (copybooks/,
cpy/, copy/, plus importer dir) over vendor paths when resolving COPY
statements. This intentional behavior change moves the resolver from
depth-free (prior measured ~0.885) to depth-sensitive (measured 1.751
on CI run 34394116972), because the new preferredCopybookDirs check
walks path components.

- Raise depth_budget from 1.6 to 2.4 (~1.37x the measured ratio)
- Update _measured.depth_ratio from 0.885 to 1.751
- Add _cobol_copybook_dir_preference_2967 note documenting the change

The COBOL fingerprints already reflect the new target set behavior
(vendor/EXTERNAL.cpy correctly returns null when a copybook dir is
present) per commit fc9e8270.

Co-authored-by: Kevin Rajan <kvnloo@users.noreply.github.com>

* bench(cobol): update baselines for copybook-dir preference (#2967)

COBOL preferred-dir filtering now affects resolution outcomes. The
unique-arm layouts (mixed copybooks/src dirs) drop from 1153 to 442
resolved as files outside preferred directories are correctly filtered.
The collide arm (all files in svc${d}/copybooks) keeps 1153 resolved
because ALL files remain in the preferred class.

- Update small/deep resolved: 1153 → 442
- Update small/large/deep fingerprints for new target set
- Raise heap_bound_bytes.cobol: 3500000 → 6200000 (1.5x measured 4112464 B)
- Add measure.mjs exception: collide legitimately differs from small
- Update _heap_bound_note with new cobol measurement context
- Expand _cobol_copybook_dir_preference_2967 note to explain collide delta

The collide arm now measures collision behavior within the preferred
class rather than across mixed layouts — an intentional outcome of the
preferred-dir semantics rather than a corpus defect.

Refs #2967

Co-authored-by: Kevin Rajan <kvnloo@users.noreply.github.com>

* fix(cobol): P1-A stem key with uppercase extensions, P1-B polyglot preferred-class latch

P1-A: Use raw extension for path.basename so CUSTREC.CPY keys as CUSTREC
  - Before: path.basename('CUSTREC.CPY', '.cpy') -> 'CUSTREC.CPY' (no strip)
  - After: path.basename('CUSTREC.CPY', '.CPY') -> 'CUSTREC' (stripped)
  - Processor used raw extension; census now matches

P1-B: Latch preferred-class only from copybook-tier paths
  - Before: docs/copy/README.md triggered hasPreferredDir = true
  - After: check preferred-dir only after extension filter
  - Processor receives polyglot allPathSet; test added

Test coverage:
  - cobol-copy-external-imports.test.ts: processor pins for both fixes
  - cobol-import-target-parity.test.ts: resolver parity updated to new behavior
  - All existing COBOL tests pass

Co-authored-by: Kevin Rajan <kvnloo@users.noreply.github.com>

* test(cobol): fix Mixed.CPY integration test for P1-A behavior

The integration test in cobol-import-index-reuse.test.ts had outdated
expectations from before P1-A. With P1-A, path.basename uses the raw
extension, so Mixed.CPY is now keyed as MIXED (extension stripped).

Before (pre-P1-A):
- Mixed.CPY → keyed as MIXED.CPY (uppercase ext not stripped)
- COPY MIXED.CPY → found, COPY MIXED → null

After (P1-A):
- Mixed.CPY → keyed as MIXED (raw ext stripped)
- COPY MIXED → found, COPY MIXED.CPY → null

Updated test expectations to match P1-A behavior. All three validation
tests now pass:
- cobol-import-index-reuse.test.ts (integration, index reuse)
- cobol-copy-external-imports.test.ts (processor P1-A/P1-B pins)
- cobol-import-target-parity.test.ts (resolver parity)

Fixes PER-994 CI failure on abhigyanpatwari/GitNexus#3240.

Co-authored-by: Kevin Rajan <kvnloo@users.noreply.github.com>

---------

Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Kevin Rajan <kvnloo@users.noreply.github.com>
2026-09-10 15:42:59 +01:00
Abhinav Pandey
79f210c5b5
fix(go, workspace): resolve test siblings and tighten package discovery (#3191)
* fix(go): resolve test helpers through package sibling tables

* fix(workspace): discover source entries from scoped static configuration

* Address PR review feedback (#3191)

- Align sibling comments with the no-bare-name partition and drop the stale same-dir fallback claim.
- Pin `_test.go` dot-import wildcard augmentation so a revert to nonTestFiles cannot stay green.

Note: pre-existing failure in worker-pool startup crashes in the full vitest suite not addressed by this PR.
Co-authored-by: Cursor <cursoragent@cursor.com>

* fix: tighten workspace discovery and index Go sibling bindings

Skip leftover test/ workspace roots and extra Vite configs. Publish
same-package Go names from per-package indexes instead of pairing every file.

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

---------

Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-10 12:22:12 +01:00
Abhinav Pandey
2220f4d851
fix(resolution): label fallback guesses and preserve export visibility (#3190)
* fix(resolution): distinguish name guesses and preserve export visibility

* test(go): keep method enrichment fixture in one package

* fix(resolution): address split review edge cases and evidence reporting

* fix(exports): recognize imported and expression-local receivers

* fix(resolution): align export and target evidence with language scope

* fix(ingestion): preserve lexical import provenance through resolution

* test(ci): rebalance Windows shards from measured slow suites

* Address PR review feedback (#3190)

- Label constructor unique-name guesses as global-name-fallback and run language vetoes
- Tighten Go qualified, Rust crate::, and Ruby class-reopen fallback guards
- Ignore for-loop shadowed CommonJS receivers and exclude guesses from the resolved-call census
- Refresh FinalizeOutput and hook docs for lexical binding scopes

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

* Tighten review-feedback leftovers on fallback visibility.

Qualified Go calls still respect export and test-package rules, nested Rust src/ stays a module segment, and top-level conditional this.x is treated as CommonJS.

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

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

* Address PR review feedback (#3190)

- Distinguish Swift package prefixes when comparing target modules
- Document lexical import binding and handledSites refusal marking
- Drop the stale ci-scope-parity workflow claim and prototype-safe export verdicts

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

* Address PR review feedback (#3190)

Supply caller source on Ruby visibility cases so they exercise the named allow branches instead of the missing-text bypass.

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

* fix(resolution): label unique constructor types as name guesses

A workspace-unique class hit in findClassBindingInScope was treated as
an in-scope bind, so Go/JS constructor-form sites skipped the guess
label and the Go unexported veto.

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

* fix(resolution): keep qualified constructors precise after unique-name split

Bare constructor unique-name hits stay guesses so Go can veto an
unexported type. A written qualifier is now carried as rawQualifiedName
so `new pkg.Foo()` and `models.Box[T]{}` can still recover the unique
class without that veto.

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

* test(bench): rebaseline Go/Java scope-capture fingerprints for constructor qualifiers

Generic Go composite literals and qualified Java `new pkg.Foo()` now
carry @reference.qualified-name on existing constructor matches.

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

* fix(resolution): treat import-reached unique constructors as precise

C++ #include and Rust re-exports do not mint a lexical class binding.
A unique type in an imported file (or imported directory) is therefore
a real bind, not a name guess, so those CALLS edges stay import-resolved.

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

* fix(resolution): require named or resolved imports for constructor precision

Bare Go Box[T]{} is not package-qualified, and a sibling-file import of a different name is not constructor visibility.

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-09-10 11:38:19 +01:00
Navid EMAD
506432017f
fix(zig): model callable-value references, and stop reporting their absence as exact (#3219)
Some checks are pending
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
Trivy Image Scan / Trivy (gitnexus-cli) (push) Waiting to run
Trivy Image Scan / Trivy (gitnexus-web) (push) Waiting to run
CodeQL / Analyze (javascript-typescript) (push) Waiting to run
Scorecard / Scorecard analysis (push) Waiting to run
* fix(impact): stop reporting 'exact' over unmodelled callable-value references

A function named in VALUE position — `bridge.accessor(Element.getNamespaceUri,
null, .{})`, `{ onClick: handler }`, a comparator handed to a sort — is
registered somewhere rather than called. The registration is modelled (a
`value-ref` site becomes a USES edge; Kythe `ref` vs `ref/call`, Joern
METHOD_REF), but the invocation THROUGH the stored value is not: it happens
later via a struct field, a registry lookup or comptime reflection.

impact()/context() nonetheless reported such a target as `epistemic: 'exact'`.
For lightpanda-io/browser that meant the DOM `Element.namespaceURI` accessor
came back with two internal callers, LOW risk and a claim of completeness —
worse than no answer, because 'exact' tells the reader not to look further.
tools.ts defines 'lower-bound' as "the walk provably missed callers", which is
precisely this case.

computeEpistemicBoundary now probes for inbound USES edges stamped with the
value-ref reason and hedges when it finds any, contributing a boundary note and
a new `causes.callableValueReferences` (unit: distinct referrer symbols).

Read from the graph, not from index metadata: unlike a dropped receiver — which
leaves no edge to find and therefore needs a persisted summary — a value
reference IS in the graph. So the signal needs no re-index and no analyzer
change, and it works on indexes written before this commit.

The cause gets its own slot rather than joining `dispatchBoundary`: a value
referrer is neither an implementation nor an interface-level consumer, and the
two differ in what the reader should do about them — a dispatch boundary is
irreducible, a callable value usually becomes traceable once the provider
models the store/load that carries it.

Language-neutral: every provider that emits a `value-ref` capture participates.
The writer and the reader now share VALUE_REF_EDGE_REASON, because drift
between them would fail silently — the probe would match nothing and every
answer would go back to claiming certainty.

* fix(zig): emit `value-ref` for a callable named in value position

`mapReferenceKindToEdgeType` has handled the registration-vs-invocation case
since #2437, and TypeScript, JavaScript and C++ all emit `value-ref`. Zig
emitted zero — so a Zig function handed somewhere as a VALUE was absent from
the graph entirely.

That is not a corner: Zig's JS bridge is built out of this one shape,

    pub const namespaceURI = bridge.accessor(Element.getNamespaceUri, null, .{});

and `bridge.{accessor,function,indexed,…}` appears 2,047 times across 257 files
in lightpanda-io/browser — the project's whole JS<->Zig surface, none of it
reaching the graph. `impact` on `Element.getNamespaceUri`, which IS the DOM
`Element.namespaceURI` accessor, answered with its two in-file callers.

Three query rules, tagging `@reference.value-ref` on: a bare identifier in
argument position (`bridge.accessor(_tagName, …)`), a qualified one
(`bridge.accessor(Element.getNamespaceUri, …)`), and a const binding initialiser
(`pub const defaultHandler = onReset;`). Everything downstream already existed —
no new edge type, no schema change, no capture-machinery change, no baseline
edited.

One grammar detail is load-bearing: in tree-sitter-zig, call arguments are
DIRECT children of `call_expression` — there is no `arguments` node, only
builtins have one — so the callee has to be consumed explicitly by `function:`.
Without that binding the same rule also matches the callee of `foo(bar)` and
mints a USES edge shadowing the call's own CALLS edge. There is a test for it.

The rules are deliberately broad — `js.Bridge(Element)` and `register(count)`
match too — because the callable gate in the property-dispatch pass
(Function/Method/Constructor only) is the filter, the same design that keeps
TypeScript's `{ port: DEFAULT_PORT }` from registering anything. Measured on the
real corpus: all 3,169 emitted edges land on a Method (3,146) or Function (23).

Deliberately NOT modelled: the terminal invoke. The value reaches
`Accessor.init` -> a struct field -> `Factory.zig` reflection (`inline for`,
`@typeInfo`) -> `Caller.zig`'s `@call(.auto, func, args)` over `func: anytype`.
Resolving that needs comptime evaluation. The point is to stop dropping the
reference; the shortfall is now reported as `epistemic: "lower-bound"` by the
preceding commit instead of being papered over.

Measured on lightpanda-io/browser (698 .zig files), wiped index both times:
30,222 nodes / 71,070 edges -> 30,222 nodes / 74,229 edges. The delta is
entirely `USES` (3,169 after vs 10 before, and every one is a value-ref); CALLS,
ACCESSES, IMPORTS, HAS_METHOD, DEFINES and the rest are unchanged — purely
additive, no node invented.

The fixture joins the existing `zig-idioms` corpus rather than adding a new
one, because `bench/receiver-resolution` uses `test/fixtures/lang-resolution` as
its `--check` corpus. That gate, `scope-capture` (15 languages),
`zig-cross-file-resolution` and every other bench `--check` pass unchanged.

* fix(review): resolve qualified value references through their owner, and stop
hedging on registrations the analyzer already followed

Five findings from the review bot on #3219; four valid, all addressed.

1. QUALIFIED VALUE REFERENCES RESOLVED BY TAIL NAME (the serious one).
   `bridge.accessor(Element.getNamespaceUri, …)` was resolved with
   `findCallableBindingInScope(site.inScope, site.name, …)`, which never sees
   the receiver and gives LOCAL bindings precedence. Reproduced:

       const Element = @This();
       pub fn getThing(...)                 // main.getThing
       pub const JsApi = struct {
           fn getThing(...)                 // JsApi.getThing
           pub const thing = bridge.accessor(Element.getThing, null, .{});
       };

   emitted `USES JsApi → JsApi.getThing` — a WRONG edge, which is worse than
   the missing edge this PR set out to fix.

   `resolveValueRefTarget` now resolves a site carrying an explicit receiver
   through `findClassBindingInScope` → `findOwnedMember` (the machinery
   `receiver-bound-calls` already uses), gated on `CALL_TARGET_TYPES` because
   `findOwnedMember` also answers with fields. A bare site keeps the lexical
   walk, which is what an unqualified name means. When the owner cannot be
   resolved the site is DECLINED rather than falling back: declining costs a
   reference, falling back mints a confident edge to the wrong function, and
   the missing reference is now reported as `lower-bound` anyway.

   Cost on lightpanda-io/browser: value-ref edges 3,169 → 2,799 (−12%). Those
   370 were tail-name coincidences, not registrations — all 94 `Element.zig`
   `JsApi` entries survive, the cross-container case resolves and is correctly
   attributed (`IntersectionObserverEntry.JsApi` → `IntersectionObserverEntry.
   getTarget#0`, not the enclosing observer), and both acceptance probes are
   unchanged: `getNamespaceUri` 7/3/LOW/lower-bound, `getTagNameLower`
   31/10/HIGH/exact.

2. A FAILED PROBE READ AS "NO BOUNDARY". `.catch(() => [])` turned an
   unanswerable query into count 0 and no note, so `exact` could be published
   on the strength of a question that was never asked. It now returns `null`
   and emits a boundary note. This file's own `loadMeta` comment states the
   rule: a probe failing must never read as certainty.

3. NOT EVERY VALUE REFERENCE IS AN UNMODELLED INVOCATION. Where
   `emitPropertyDispatchCalls` sweep 2 synthesized the dispatch (reason
   `property-dispatch`), the walk did NOT provably miss the caller, so hedging
   was noise over an answer that was computed — and a signal that fires on
   every JS/TS hook table stops carrying information. A second probe excludes
   those targets. Zig never sets a property key, so the motivating case is
   untouched. The exclusion is symbol-level, not per-edge, because the graph
   does not record which registration produced which synthesized call; that
   residual is documented at the call site.

4. `LIMIT 50` SILENTLY UNDERSTATED THE COUNT. `rows.length` over a capped row
   set published a ceiling as the documented symbol count. Replaced with
   `COUNT(DISTINCT other.id)` — bounded work without a bounded answer, the
   shape `countByType` in the same file already uses.

5. THE TEST DID NOT PIN THE CALLER COUNT its own comment promised. Added
   `expect(result.impactedCount).toBe(2)`.

New regression tests: qualified references bind the written owner and not the
nearer lexical match; an unresolvable receiver emits nothing rather than a
wrong edge; a dispatch-modelled registration stays `exact`; a probe that cannot
run hedges instead of claiming certainty.

Known limitation, pinned by a test rather than left implicit: when a `@This()`
alias's NAME differs from its container's (`const Element = @This();` inside
`main.zig`), the receiver does not resolve and the reference is declined. The
provider has `rewriteZigThisAlias` for this, but it is applied to type nodes
and extending it to reference receivers would change existing CALL-site
behaviour. Lightpanda's `const Foo = @This();` in `Foo.zig` convention makes
the names coincide, which is why the corpus is unaffected.

* fix(review): bump the parse-cache schema and resolve module-owned value references

Review round 2 on #3219. Four inline items, all reproduced against the
worktree before deciding.

P1 — the new captures could stay INERT on a warm parse cache. Adding
`@reference.value-ref` rules to `ZIG_SCOPE_QUERY` changes
`ParsedFile.referenceSites`, which is a PARSE-TIME fact, but `SCHEMA_BUMP`
stayed at 93. A repo indexed before this branch and re-analyzed after it
replays the old, empty site list for every unchanged `.zig` file — `--force`
included, since shards are content-addressed — so no USES edge is emitted, the
boundary probe measures a real zero, and `impact` on a registered accessor goes
back to `epistemic: "exact"`. #3399 un-fixed on the incremental path most users
are on, with every cold-run test still green. DECISIONS D1-1's "zero changes to
… the schema" conflated the graph schema with the cache schema.

Bumped 93 -> 98, not 94: #3190 claims 94 and #3179 claims 94 through 97 in one
PR. `incremental-parse-cache.test.ts` re-pinned, with 93-97 added to the taken
list. Re-check against origin/main and open PRs immediately before merging.

P2 — a qualified value reference through a MODULE was declined with no hedge.
R1-2 resolves a written receiver with `findClassBindingInScope`, which requires
`isClassLike`; a namespace-only `@import` handle is not class-like, so

    const dom_utils = @import("dom_utils.zig");   // no `@This()` in that file
    pub const comparator = bridge.accessor(dom_utils.compare, null, .{});

resolved to nothing. That is not the conservative half of R1-2's trade-off: a
declined site emits NO edge, so there is nothing for the probe to read and
`impact` on `compare` reports `exact`. Silence, not a hedge — and the pass
comment claiming otherwise was wrong on this path.

`resolveValueRefTarget` now tries the second kind of owner a qualified name can
have. `findNamespaceValueRefTarget` reads the file's `namespace` import edges
for the handle and the target module's own `origin: 'local'` module-scope
bindings for the member — the same channel `receiver-bound-calls` Case 1
already trusts for `dom_utils.compare()`, with Case 1's three guards for Case
1's reasons: `isNamespaceNameShadowed`, local-origin bindings only, and two
distinct defs under one name resolve nothing. `CALL_TARGET_TYPES` gates module
owners exactly as it gates container owners. Language-neutral: it reads generic
namespace import edges, names no language.

Still declined, deliberately: a receiver this index knows under no name at all
(the `@This()`-alias case, owners outside the workspace). There the alternative
is a confident edge to a lexically-nearer function the source did not name.

P2 — `impact-callable-value-references.test.ts` was in neither vitest list.
It opens a real engine via `withTestLbugDB(poolAdapter: true)`, so TESTING.md
puts it in the `lbug-db` include list and the `default` exclude list; it was in
neither, so `default` also collected it into the parallel pool. Added next to
its `impact-epistemic-lower-bound` sibling in both arrays.

P3 — DECISIONS.md was stale against HEAD and embedded host paths. D2-3 still
described the `LIMIT 50` that R1-5 removed and D1-3 still described the
receiver-blind resolution that R1-2 replaced; both now carry explicit
"superseded by" pointers. The `~/code/...` and mise-node paths are replaced
with placeholders.

Two smaller corrections the new cause made necessary:

- `formatImpactResult`'s `lower-bound` header hard-coded "callers binding via
  DI / dynamic dispatch", which now contradicts the value-reference bullet
  printed directly under it. The bullets carry the cause; the header only
  states that the count is a floor.
- `tools.ts` said a `causes.callableValueReferences` of 0 means "nothing was
  missed". The dispatch exclusion is symbol-level, not edge-level, so a target
  with both a followed registration and an unfollowed escape also reads 0. The
  docs now say a 0 means "no unfollowed registration was proven".

Fixture: `src/webapi/dom_utils.zig` (namespace-only) plus three cases in
`Element.zig` — the module-qualified registration, a non-callable module member,
and a `u8` parameter shadowing the handle. The shadow case was verified to FAIL
with the guard disabled, so it is not passing for an unrelated reason.

Gates: `tsc --noEmit` clean, `npm run build` clean, prettier clean.
`resolvers/` 3,630 passed / 3 skipped; `unit/scope-resolution` 2,010 passed;
`impact-callable-value-references` 7 passed under `lbug-db`;
`incremental-parse-cache` 40 passed; eval formatters 104 passed. Bench --check
all PASS with no baseline edited: receiver-resolution, zig-cross-file-resolution,
scope-emission, callable-value-flow, scope-capture (15 languages), python-scope.

* fix(review): guard the class receiver against a shadowing binding, and pin the dispatchability partition

Review round 3 (`gitnexus-check` bot on `cf53bbaa`). Three findings, each
reproduced against the code before deciding.

R3-1 (Error, valid, REPRODUCED) — a CLASS receiver could resolve through a
shadowing value binding. `findClassBindingInScope` is a class-only walk: it
filters the scope chain by `isClassLike`, so it steps over a nearer binding that
is a value and keeps climbing — and past the chain entirely, into a
qualified-name fallback that answers with the unique workspace definition of the
name. A `u8` parameter named `Ticker`, in a file that neither declares nor
imports the `Ticker` container another file defines, therefore emitted
`register(Ticker.fire)` as a confident USES edge to that container's method.
That is exactly the wrong-edge failure R1-2 exists to prevent, arriving through
the class channel instead of the lexical one.

Fixed with `isOwnerNameShadowedBySomethingElse` — a sibling of
`isNamespaceNameShadowed` with one extra clause. The plain namespace guard could
NOT be reused: a container is often its own local declaration
(`fn make() { const Local = struct {…}; register(Local.go); }`), and reading that
binding as its own shadow suppresses precisely the resolutions this path exists
to make — the #2723 mistake, one channel over. So a scope that binds the name
answers immediately, and the answer is "not shadowed" only when one of that
scope's own bindings IS the def just resolved.

Both halves are pinned and both were verified to fail when the guard is
weakened: the parameter case fails with no guard at all, the local-container
case fails with the plain `isNamespaceNameShadowed`.

R3-2 (Warning; mechanism correct, unreachable today; fragility fixed instead) —
the dispatch exclusion could suppress an unfollowed registration. The bot is
right about the code: sweep 2 synthesizes CALLS only for a registration whose
site carried a `propertyKey`, while the exclusion zeroes the note on ANY inbound
`property-dispatch` CALLS edge. It is not reachable in the current rule set, and
the reason is measured rather than assumed: `@reference.value-ref` is emitted by
exactly three languages — JavaScript (2 rules), TypeScript (2), Zig (3) — every
JS/TS rule also captures `@reference.property-key` (both are object-literal
shapes) and no Zig rule does. A dispatchable registration is therefore always a
JS/TS one, an undispatchable one always a Zig one, and they cannot meet on one
symbol.

Rejected: splitting the edge `reason` into dispatchable / undispatchable. It is
the precise fix, but it is a graph-content change that churns whichever side
keeps the old literal — the Zig, TypeScript and probe suites all pin
'scope-resolution: value-ref' by hand as a drift canary — and it buys nothing
against a case no rule can produce.

What was actually wrong is that the exclusion's soundness rested on a
coincidence recorded nowhere, in files nobody reading `local-backend.ts` would
open. Fixed at both ends: the exclusion site now states the invariant, the three
facts it rests on and the two options for when it breaks; and
`value-ref-dispatchability.test.ts` fails the day it does — a JS/TS rule for a
bare callback argument, a Zig rule that grows a key, or a fourth language
emitting `value-ref` at all. Verified to fire (adding a property key to a Zig
value-ref rule fails the Zig case), and its rule splitter has its own guard test
so the suite cannot pass vacuously. `ZIG_SCOPE_QUERY` is exported for that test
only.

R3-3 (Nit, valid) — a test comment claimed the wrong epistemic result. The
`declines a qualified reference whose receiver cannot be resolved` case said the
shortfall shows up as `lower-bound`. It does not: with no edge there is no
evidence and the target stays `exact`. The pass docstring was corrected in round
2 and this comment was missed. It now says the decline costs the reference AND
the hedge, and why that is still the right trade.

Gates: tsc --noEmit clean, npm run build clean, prettier clean.
`test/integration/resolvers` 3,632 passed / 3 skipped (70 files);
`test/unit/scope-resolution` 2,015 passed (120 files);
`impact-callable-value-references` 7 passed under `lbug-db`. Bench --check:
receiver-resolution, zig-cross-file-resolution, scope-capture (15 languages),
scope-emission PASS with no baseline edited; callable-value-flow failed once on
its TIMING budget (2.006 > 1.9) with a byte-identical fingerprint, then passed
twice at 1.788 / 1.813 — machine load, not a regression.

* fix(review): let a file's own `@import` outrank the workspace-wide class fallback

Found by running the local preflight harness before pushing rather than after.
One of its five findings is a real hole in R2-2; the rest are documentation that
overclaimed.

R4-1 (valid, REPRODUCED) — the container channel preempted the file's own
`@import`. R2-2 added the module channel as a FALLBACK after
`findClassBindingInScope`, and that order is wrong: `findClassBindingInScope`
does not stop at the scope chain. When its `isClassLike` walk misses — and a
namespace handle binds a Module, so it always misses — it falls back to
`scopes.qualifiedNames`, a workspace-wide index, and answers with the unique def
of that name anywhere in the repo. A container named `dom_utils` in a file
`Element.zig` never imports therefore captured
`bridge.accessor(dom_utils.compare, …)`, binding
`Method:src/webapi/decoy.zig:dom_utils.compare#2` while the module channel that
would have answered correctly was never reached. R3-1's shadow guard cannot
catch it: the import binds at MODULE scope, which that guard treats as the floor.

Fixed by trying the module channel FIRST. An `@import` written in this file is
the strongest available statement about what the name means here and outranks a
global uniqueness guess; when the handle is not an import of this file the
channel answers nothing and the container path runs exactly as before. Pinned by
`decoy.zig` and a strengthened assertion on the existing module-owner test,
which fails on the old order.

R4-2 — the cause documentation named shapes nothing captures. `tools.ts`
illustrated `callableValueReferences` with "a callback argument", "a stored
function pointer" and `qsort(xs, n, sz, compareItems)`. Only Zig captures a call
argument or a const initialiser; JS/TS capture only object-literal property
values, and C has no value-ref rule, so the `qsort` example is counted in no
language. Both cause blocks now name the captured shapes and say that a bare
JS/TS callback argument is not among them, so a 0 does not rule it out.

R4-3 — the same block exempted itself from the re-index caveat this PR proves it
needs. "Read from the graph, so it needs no index-time metadata" is true of the
probe and false of the edges: an index built before a language emitted these
captures has none and reports 0 — the warm-cache failure R2-1 bumped
SCHEMA_BUMP for. It now says to re-analyze before reading a 0 as measured.

R4-4 — the dispatchability canary was narrower than its own promise. Its header
claimed it fails on "a fourth language emitting value-ref at all"; it reads
`languages/<dir>/query.ts`, so Vue — which owns no query and borrows
`emitTsScopeCaptures` / `emitJsScopeCaptures` — emits value-refs while the
assertion lists three languages, and a capture synthesized in code is invisible
to it. The case now asserts on query OWNERS, which is what it checks and is
sound because a delegating language inherits the rules it borrows; the header
states the synthesized-capture gap instead of letting a green tick imply it away.

R4-5 — the BARE docstring described a lexical walk that is not one.
`findCallableBindingInScope` applies the callable predicate WHILE walking, so a
nearer parameter or local is stepped over: the defect R3-1 fixed on the container
channel, unguarded here, pre-existing since #2437 and reachable in JS/TS. Out of
scope for #3399, so behaviour is unchanged and the sentence now says what the
walk does rather than implying a guarantee it does not give.

Gates: tsc --noEmit clean, npm run build clean, prettier clean.
`test/integration/resolvers` 3,632 passed / 3 skipped (70 files);
`test/unit/scope-resolution` 2,015 passed (120 files);
`impact-callable-value-references` 7 passed under `lbug-db`. Bench --check:
receiver-resolution, zig-cross-file-resolution, scope-capture (15 languages),
scope-emission, callable-value-flow all PASS with no baseline edited.

* fix(review): honour the hub opt-in for value references, so `hub.fn` means one thing

Review round 5 (`gitnexus-check` bot on `1c7c05ff`). One finding, valid and
reproduced before fixing.

`findNamespaceValueRefTarget` accepted only `ref.origin === 'local'`. R2-2
recorded that as deliberate — "the `namespaceExportsIncludeImportedNames` hub
opt-in is a provider decision this language-neutral pass does not make" — and
that reasoning was wrong twice over. Zig sets the flag
(`languages/zig/scope-resolver.ts:41`, measured on ghostty and tigerbeetle before
it landed), and the pass does have the provider in scope: `runScopeResolution`
takes one and already forwards several of its hooks to other passes.

The result was the exact asymmetry R2-2 argued against in its own first
paragraph. A Zig hub declares nothing — every name it publishes it imported — so
requiring a local declaration declines every member reached through one:

    // hub.zig
    pub const scale = @import("dom_utils.zig").scale;

    // Element.zig
    const hub = @import("hub.zig");
    pub fn callsThroughTheHub(v: u8) u8 { return hub.scale(v); }   // resolved
    pub const scaled = bridge.accessor(hub.scale, null, .{});      // declined

One name meaning two different things depending on whether a `(` follows it.

Fixed by forwarding `provider.namespaceExportsIncludeImportedNames` into the pass
and consulting the published channel when it is set — the same question
`receiver-bound-calls` Case 1 asks, through the same `lookupBindingsAt` read
`findExportedDefIncludingImportedNames` performs for the CALL form. The pass
still names no language; it asks the provider, which is the sanctioned hook.

Precedence is unchanged where it mattered: a locally declared member still wins
over a republished one, two distinct defs under one name still resolve nothing,
and `CALL_TARGET_TYPES` still gates the answer — pinned by `hub.DEFAULT_NS`, a
re-exported CONSTANT, which stays unregistered. Languages that do not opt in are
unaffected: the parameter defaults to `false`, and passing `false` was verified
to fail the new hub test, so it is load-bearing. The fixture republishes a member
(`scale`) that nothing else uses, so the hub assertion cannot be satisfied by an
edge another case emitted.

Gates: tsc --noEmit clean, npm run build clean, prettier clean.
`test/integration/resolvers` 3,634 passed / 3 skipped (70 files);
`test/unit/scope-resolution` 2,015 passed (120 files);
`impact-callable-value-references` 7 passed under `lbug-db`. Bench --check:
receiver-resolution, zig-cross-file-resolution, scope-capture (15 languages),
scope-emission, callable-value-flow all PASS with no baseline edited.

Also recorded in DECISIONS.md: `/autofix` returned "No successful autofix run"
because the `PR Autofix` run on `1c7c05ff` failed at `actions/upload-artifact`
with a 403 from GitHub's artifact storage, not because of anything in this
branch. This push triggers a fresh run.

* fix(review): inspect the module scope in the owner-shadow guard, and check the TSX suffix

The `gitnexus-check` pass on `bd6e577e` carried three findings of its own, and I
answered the later `1c7c05ff` pass without noticing them. Recording that as a
process failure too: bot reviews are per-head, and a later pass does not
necessarily repeat an earlier one's findings.

R6-1 (Error, valid, REPRODUCED) — the shadow guard stopped one rung short.
`isOwnerNameShadowedBySomethingElse` returned `false` on reaching the module
scope, justified as "a container declared there IS the binding, and the caller
already resolved it". That holds when the owner came from the scope chain and
fails when it came from the workspace-wide qualified-name fallback:

    // Gauge.zig — never imported by Element.zig
    const Gauge = @This();
    pub fn read(self: *Gauge) u8 { … }

    // Element.zig
    const Gauge = @import("dom_utils.zig").DEFAULT_NS;          // NOT a container
    pub const level = bridge.accessor(Gauge.read, null, .{});   // → Gauge.zig's read

`findClassBindingInScope` steps over the module-scope binding because it is not
class-like, answers from `scopes.qualifiedNames`, and the guard waved it through.

Worth recording: the first fixture attempt did NOT reproduce. A local
`const Gauge: u8 = 3;` also claims the workspace qualified name `Gauge`, leaving
two candidates, and the fallback refuses to guess between two — so the shape
defeated itself. Binding the name by IMPORT claims no qualified name, the
fallback stays unique, and it fires. A negative result on the first shape was not
evidence the finding was wrong.

Fixed by inspecting the module scope as the last rung instead of skipping it.
The identity exemption is what makes that safe where `isNamespaceNameShadowed`
cannot do it (#2723: a namespace import writes its own name into the module scope
and would read as its own shadow) — the binding that IS the owner exempts itself,
and only a binding to something else answers `true`. `lookupBindingsAt` is
consulted at that scope and only there, because an imported alias lives in the
finalized channel rather than in `scope.bindings`.

R6-2 — the hub re-export finding, already fixed in `5d8fe9d8`; the same defect
restated on the later head.

R6-3 (valid, fixed) — the dispatchability canary omitted the TSX suffix.
`getTsScopeQuery` analyzes a `.tsx` file with `TYPESCRIPT_SCOPE_QUERY +
TSX_JSX_QUERY_SUFFIX`, and the test read only the base, so a `value-ref` rule
added to the suffix would be emitted in TSX analysis with the canary green. The
suffix is now exported and concatenated into the check; verified load-bearing by
adding an unkeyed `jsx_expression` value-ref rule to it, which fails the
TypeScript case.

Gates: tsc --noEmit clean, npm run build clean, prettier clean.
`test/integration/resolvers` 3,635 passed / 3 skipped (70 files);
`test/unit/scope-resolution` 2,015 passed (120 files);
`impact-callable-value-references` 7 passed under `lbug-db`. Bench --check:
receiver-resolution, zig-cross-file-resolution, scope-capture (15 languages),
scope-emission, callable-value-flow all PASS with no baseline edited.

* perf(bench): gate callable-value reference resolution on linear scaling

`resolveValueRefTarget` (#3399) replaced one lexical walk with four
channels, and the last of them — a qualified receiver resolved through
`findClassBindingInScope` — falls back to `scopes.qualifiedNames`, a
WORKSPACE-WIDE index consulted once per site. Keyed that is O(1); scanned
it is O(files) per site, and a registration table that costs O(sites)
today costs O(sites x files) tomorrow. Nothing in the suite can see that:
the fixtures are single-file, and the corpus it actually matters on is
lightpanda-io/browser, where `bridge.{accessor,function,…}` appears 2,047
times across 257 files.

`bench/value-ref-resolution/measure.mjs` builds two synthetic Zig corpora
of identical shape 4x apart in file count, and times ONLY the per-site
resolution loop — extraction, `reconcileOwnership` and finalize are setup.
`linear_factor` is `(t_large/t_small)/(N_large/N_small)`: measured
1.00-1.09 across runs, with `us_per_site` flat at 1.4-1.5 between the arms.

Correctness comes first, because a timing gate alone is satisfied by a fast
wrong answer: exact site/resolved/declined counts per arm plus an
order-independent sha256 over every (site -> resolved target) pair. The
corpus exercises all four channels — CONTAINER, NAMESPACE, HUB and BARE —
and carries two DECLINE controls per module (a non-callable namespace
member, a non-callable bare argument), so a widened callable gate moves
`declined` instead of hiding inside the timing.

Verified load-bearing rather than assumed: making the qualified lookup scan
a workspace-sized collection leaves the fingerprint IDENTICAL and takes
`linear_factor` to 3.752 against a slack of 1.375 — the regression class
this exists for is exactly the one no correctness gate can see.

Zig is the corpus because it is the only language whose provider sets
`namespaceExportsIncludeImportedNames`, so it is the only one that can
exercise the hub channel at all; the pass itself names no language.

`resolveValueRefTarget` is exported for the bench. Timing
`emitPropertyDispatchCalls` instead would fold the signal into edge
emission, and re-implementing the channel order in the bench would pin the
bench's idea of the function rather than the function.

* feat(zig): index every build package in the repo, not only the root one

Zig already had workspace setup — `loadZigBuildConfig` has parsed
`build.zig.zon` `.path` deps, the root `build.zig`'s named modules and each
build module's own `addImport` table since #1432, threaded through
`ScopeResolver.loadResolutionConfig` exactly as tsconfig is for TypeScript.
What it did not have is the part `tsconfigFor` supplies: per-package scope.

`loadZigBuildConfig` reads `<repoRoot>/build.zig{,.zon}` and nothing else, so
a repo laying its packages out as `packages/<name>/build.zig` — no root build
files at all — got `null`, and EVERY bare `@import("<module>")` in it went
unresolved. Cross-file resolution silently degraded to relative imports.
Measured on a two-package probe before this change: `config = null`,
`@import("core")` from `packages/app/src/main.zig` → `null`.

`loadZigWorkspaceIndex` discovers packages the way `findTsconfigFiles`
discovers configs — one bounded breadth-first walk that skips the hardcoded
ignore set — and `zigPackageFor` is the `tsconfigFor` analogue: the nearest
enclosing package governs a file, deepest-first, with no fall-through to an
outer package. Fall-through is what makes a vendored dependency's
`@import("config")` resolve to the outer repo's `config` module, the same
failure `loadTsconfigIndex` documents for a package declaring no `baseUrl`.

`resolveZigImportInternal` is UNCHANGED — impact analysis puts it at HIGH risk
with 6 dependents, and it does not need to move: it is handed one package's
config, and which config it receives is the only difference. Its 48 existing
tests pass untouched. A nested package's paths are rebased to repo-relative at
load time (`.path = "../core"` → `packages/core`, which the root-relative
reading rejects outright as an escape); the ROOT package keeps its raw
spelling, which is what `parseZigBuildZon` promises and its tests pin, so a
single-package repo is byte-identical.

`loadImportConfigs` — which runs unconditionally for every repo, Zig or not —
keeps calling the root-only loader, so no non-Zig repo pays for the walk. That
is the split TypeScript already has between the cheap `loadTsconfigPaths` and
the repo-walking `loadTsconfigIndex`, which `loadResolutionConfig` reaches only
during a language pass.

The `zig-monorepo` fixture carries the discriminating case rather than only the
happy path: `tool` binds the alias `core` to its OWN `src/core.zig`, so a
repo-wide flattened module map — the shape a workspace index invites — would
point `measure` at `packages/core`, a confident edge into a package `tool` does
not depend on. That is worse than the unresolved import this fixes, and only
per-package scoping keeps them apart.

Tests: 7 unit cases pinning the index (including `loadZigBuildConfig` answering
`null` on the same fixture, side by side) and 3 integration cases pinning the
EDGES — cross-package CALLS from the module root AND from a non-root file, and
`tool` not crossing over. Verified load-bearing: restricting the index to the
root package fails all three.

Second consumer caught by the integration test and fixed with it:
`populateZigWorkspaceStaticGating` reads the same `resolutionConfig`, per file
because two files of that pass can belong to different packages.

Gates: build clean, `tsc --noEmit` clean, prettier clean, eslint 0 errors.
`test/integration/resolvers` 3,768 passed / 4 skipped, `test/unit/scope-resolution`
2,015 passed. Bench `--check` with NO baseline edited: `receiver-resolution`
(whose corpus is `test/fixtures/lang-resolution`, where the fixture lands),
`zig-cross-file-resolution`, `value-ref-resolution`, `scope-capture` (15
languages), `import-target`, `callable-value-flow`, `scope-emission`.

* perf(zig): dequeue the package walk by head index, not `shift()`

`findZigPackageDirs` pushes children while it drains the queue, which keeps
the array in a mode where `Array.prototype.shift()` memmoves the whole
remainder rather than taking V8's left-trimming fast path — so the walk is
quadratic in the frontier, bounded only by `ZIG_SCAN_MAX_DIRS` (20,000).

Measured at that bound rather than estimated from the move count: 53 ms at
fan-out 4 and 81 ms at fan-out 20, against 0.8 ms with a head index — 66-106x,
and paid before a single config is read.

FIFO order is unchanged, and the package ordering never depended on the walk
anyway: `loadZigWorkspaceIndex` sorts by (dir length desc, localeCompare).
Memory is unchanged too — the entries were already retained by the pushes;
`shift()` only dropped the head.

`findTsconfigFiles` and `loadNodeWorkspacePackages` carry the same walk with
the same bound and are equally affected. Deliberately NOT fixed here: they are
pre-existing, outside this PR's diff, and reach TypeScript/Node import
resolution, which nothing in this change set covers. Filed separately.

Reported by gitnexus-check on aea0ab06.

* chore: drop DECISIONS.md from the branch

Review feedback: the working log does not belong in the repository. The
reasoning it carried that is still load-bearing lives in the code comments
and in the PR description.

* perf(bench): gate value-ref resolution on scaling alone, not wall clock

A millisecond ceiling measures the runner. This repo has been bitten by that
twice already — bench/callable-value-flow's widening_overhead failed at 2.07
and 1.975 against a 1.9 budget on a shared runner while the code was correct,
both times on a sub-11ms measurement — so the arm is dropped rather than
loosened. `ms_budget` is gone from both arms and from `--check`; `min_ms`
and `us_per_site` are still reported, and nothing compares them to anything.

The remaining timing gate is the ratio, reshaped after
bench/parse-dispatch-rounds/baselines.json: `_what` / `_triage` notes, a
`_measured` block recording samples for context, and min-of-15 reps instead
of 7 (bench/import-target measured N=5 tripping its own budget about one run
in twenty, N=15 holding).

Budget 1.6 — 1.40x the measured maximum over 12 runs (0.907 .. 1.146), the
~1.5x headroom its siblings use on ratios.

Re-verified rather than re-quoted, and the previous note was wrong: replacing
QualifiedNameIndex.get with a full scan moves linear_factor from ~1.0 to 2.07,
not the ~3.7 recorded. Only 1 of the 17 value-ref sites per module reaches
that workspace-wide fallback. 1.6 sits clear of both ends. The note also
records the trap the first attempt fell into: patching gitnexus-shared/src
changes nothing, because the bench resolves the built package.

* fix(review): settle namespace precedence on the name, before the type gate

findNamespaceValueRefTarget's local lookup applied CALL_TARGET_TYPES while
selecting, so a target module declaring a NON-callable under the name answered
nothing and fell through to the published channel — binding a re-exported
callable under a name the module's own declaration owns. findExportedDef does
not do that: it returns any local def and lets its caller's type gate reject
it, so findExportedDefIncludingImportedNames never reaches the imported names
for a name the file declares. `x.f` and `x.f()` must not disagree about which
module owns f.

Not reachable through valid Zig today (a container cannot declare a name
twice, and Zig is the only provider setting namespaceExportsIncludeImportedNames),
which is why the regression test builds the indexes directly instead of adding
a fixture — there is no valid source to write. Its second case fails with the
guard removed.

Three smaller corrections from the same review:

- ZigBuildZonConfig.pathDeps promised the raw `.path` string; a nested package
  stores the normalized repo-relative value. The interface now documents both
  spellings and why either is safe to hand to normalizeZigDepPath. Comment
  only — no behaviour change.
- The 'single-package repo' compatibility test ran against zig-idioms, which
  declares libs/geo as a path dep and that directory has its own build.zig, so
  the walk finds two packages and the name was a claim rather than a check. It
  now runs against libs/geo itself and asserts the package list is exactly
  ['']; a second test pins the multi-package case, including that a file inside
  libs/geo is governed by that package and not by the root.
- The lower-bound header asserted 'some callers are not traced'.
  callableValueReferenceBoundaries hedges when its probe could not RUN and says
  whether the symbol is registered is unknown, so the header claimed an
  omission nothing established. It now says the count may be incomplete and
  names no cause; the per-cause bullets underneath carry that.

* feat(zig): bind a `@This()` alias to the container it names

`@This()` IS the enclosing container, and `const Self = @This();` is how most
Zig files say so. The container is minted under the FILE STEM and the alias
bound nothing class-like: a file-level alias mints no Const at all
(isZigFileThisAlias suppresses it so it cannot shadow the type for `w: *Widget`),
a container-level one mints a Variable every isClassLike walk steps over. So
`Self.member` resolved to nothing — not a wrong edge, no edge, and a caller
list missing it is the false confidence #3399 is about. This was the PR's
declared known limitation.

bindZigThisAliases binds the alias name to its container definition in
indexes.bindingAugmentations — the sanctioned post-finalize channel (I8),
consulted only AFTER a scope's own bindings, so it can never outrank a real
local declaration and can only answer where nothing answered before. Nothing
is replaced or removed. No query, capture or SCHEMA_BUMP change.

It runs inside populateZigRangeBindings, sharing that pass's parsed tree: a
pass of its own would re-parse every Zig file on a cold tree cache. Only
aliases declared directly in a container body or at file level are bound — the
same set collectZigThisAliases recognizes — because a function-local alias
belongs in that function's scope.

Measured cold-index before/after, same command, same build:

  tigerbeetle (246 .zig)  CALLS 17,066 -> 17,140 (+74), USES 437 -> 443,
                          MEMBER_OF 5,131 -> 5,143; every other edge type
                          unchanged, all 24 node-label counts identical
  mach (132 .zig)         CALLS 7,600 -> 7,615 (+15), MEMBER_OF +1, USES flat

The +74 matches a source census of 72 `Alias.member(` call sites in
tigerbeetle files whose alias differs from the stem. Spot-checked end to end:
src/aof.zig:636 writes `try AOF.init(io, output_path)` inside AOFType, and the
edge AOFType.merge -> AOFType.init#2 is present after and absent before. Node
totals move only through the derived layers (Community 524->527, Process
774->763) — no source symbol added or removed.

Scale of what was dropped: 73 of ghostty's 185 `@This()` files, 93 of
tigerbeetle's 94 and 8 of mach's 42 spell the alias differently from the stem,
carrying 302 `Alias.member` references between them, 96 of those calls.

Fixture zig-idioms/src/webapi/Widget.zig exercises both paths — a file-level
`Self` and a container-level `Me` in Metrics — through a call and a
registration. Five cases; three fail with the binding disabled, and the two
that do not are the controls: Element.zig's stem-spelled alias must keep
resolving byte-identically, and the alias name must not become resolvable from
another file.

* docs(review): stop claiming what the alias pass does not do

Two comments asserted things the adjacent code does not support.

The alias pass's call site said it ran before the payload walk "so a subject
spelled through the alias resolves here too". It does not: the payload walk
types subjects through findReceiverTypeBinding, which reads typeBindings plus
the namespace/workspace type channels and never bindingAugmentations, where
bindZigThisAliases writes. Measured on `for (Self.items) |it|` — `it` is bound
neither before nor after. The pass is in that loop for the tree and nothing
else, which is now what the comment says.

Nor is that a gap to close by also writing a typeBinding: no container name has
one, the file stem included, so a payload subject written `Type.member`
resolves for no spelling at all. Giving the alias an entry would make it behave
unlike the container it names. Recorded at the call site so the next reader
does not re-derive it.

The module-shadow test said Element.zig declares `const Gauge: u8 = 3;`. It
binds `Gauge` by IMPORT, and the distinction is the point of the fixture: a
local declaration would also claim the workspace qualified name `Gauge`,
leaving two candidates, and the fallback refuses to guess between two — so the
case the test exists for would never be reached. The comment now says which
binding it is and why the other shape would be self-defeating.

Comment-only: node and edge counts are byte-identical across a full re-index
(52,083 / 165,261 both sides).

* docs(zig): stop describing loadZigBuildConfig as root-only in the present tense

It takes a `packageDir` since this branch and reads
`path.join(repoRoot, packageDir, name)`; `loadZigWorkspaceIndex` is what
supplies it one, per package. Two comments still described the historical
root-only invocation as the function's current behaviour:

- the monorepo integration-test header, flagged by review;
- loadZigWorkspaceIndex's own docstring — the same sentence, in the function
  that calls it WITH a packageDir a few lines below, so fixing only the test
  copy would have left the worse of the two.

Both now attribute root-only reading to the CALL (no `packageDir`), which is
what the argument actually rests on: a monorepo has no root build files, so
that call answers null and every bare @import goes unresolved. The trailing
note about `loadImportConfigs` is reworded the same way — it calls the loader
for the root package alone; the loader is not root-bound.

Comment-only: node and edge counts byte-identical across a full re-index
(52,083 / 165,261 both sides), and detect-changes reports the two hunks
overlap no indexed symbol.

* fix(zig): a written namespace handle owns its own decline

`findNamespaceValueRefTarget` returning `undefined` conflated two different
answers: "no namespace import named this receiver" and "the module this file
named does not expose that member as a callable". Only the first should fall
through to the container channel. The second did too, and
`findClassBindingInScope`'s miss path answers from the WORKSPACE-wide
qualified-name index — so a same-named container in a file this one never
imported supplied the member the written module does not have.

The owner-shadow guard does not stop it, which is the part that is not obvious:
a plain `const utils = @import("utils.zig");` records a namespace IMPORT EDGE,
not a module-scope binding, so the guard finds nothing bound under the name and
reads the container as unshadowed. It catches `const Gauge = @import(x).MEMBER`
(a real binding) and misses the handle form.

Reproduced before fixing, not argued: `decoy.zig`'s `dom_utils` struct gains
`onlyOnDecoy`, a callable `dom_utils.zig` does not have, and `Element.zig`
registers `dom_utils.onlyOnDecoy`. That minted `JsApi -> onlyOnDecoy` — a
confident USES edge into a file `Element.zig` never imports, the wrong-edge
failure this PR exists to avoid, arriving through the container channel after
the namespace channel said no.

The channel now returns 'owned' for every outcome reached once the receiver is
established as this file's unshadowed namespace handle, and the caller declines
on it. A locally shadowed handle still falls through, because there the name
does not mean the import at that site and the container channel's guard is the
right decider.

Bench fingerprint and counts unchanged; no baseline edited.

* fix(zig): reject an absolute nested-package `.path` before rebasing it

The nested-package branch prefixes the package directory and THEN normalizes,
so an absolute `.path` stops looking absolute on the way: `packages/app/` plus
`/src` is `packages/app//src`, which is relative by inspection.
`normalizeZigDepPath` drops the empty segment and the dep lands on
`packages/app/src` — a directory that really exists — so a dependency pointing
outside the repository is fabricated into an in-repo resolution. The root
package was never affected: its prefix is empty, so the value reached the check
as written.

`isAbsoluteZigDepPath` is now asked of the value AS WRITTEN, before any
prefixing, and `normalizeZigDepPath` asks the same helper so the two cannot
drift. `..` is deliberately not handled there: `../core` escapes the package
but not the repo, and rebasing it is what the branch exists to do —
`normalizeZigDepPath` still rejects what escapes the ROOT afterwards.

Note for the reviewer: `path.posix.join(pkg, depPath)` does NOT fix this.
`join('packages/app/', '/dep')` is `packages/app/dep` — it strips the leading
slash too, producing the same fabricated path without rejecting anything.
Measured before writing the fix.

Regression test pins it through the real loader: `packages/app` declares
`.escapes = .{ .path = "/src" }`, and the test fails without the guard.

`impact normalizeZigDepPath` is HIGH (14 impacted, 4 direct, exact). The edit
is behaviour-preserving for that function — the same two conditions moved into
a named helper it calls — and its existing absolute-path suite, POSIX, Windows
drive and UNC spellings included, passes unchanged.

* docs(mcp): stop defining lower-bound as proof that callers were missed

The CLI header was corrected in round 8; the MCP tool contract still made the
assertion the CLI stopped making. `context` said lower-bound "means callers
exist that this view provably does not list" and `impact` said "the walk
provably missed callers" — but `callableValueReferenceBoundaries` also
publishes lower-bound when its probe could not RUN, and says in its own note
that whether the symbol is registered is unknown. A client following the
contract would read an unanswered question as evidence of an omission.

Both now define it as a FLOOR with two possible causes — the walk provably
missed callers, or a probe that would have established completeness could not
run — and point at `boundaries` for which. That keeps the common case exactly
as strong as it was; it only stops the contract asserting the one case it
cannot support. The `causes.callableValueReferences` bullet already documented
the probe-failure branch, so the headline was contradicting the body.

`local-backend.ts` quotes that definition to justify hedging; the quote is
updated to name which half it relies on.

---------

Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
2026-09-09 20:12:17 +00:00
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
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
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
Gergő Magyar
43a842724d
fix: bind Razor ViewComponent names to in-repo classes (#3104)
* fix: bind Razor ViewComponent names to in-repo classes

Index Component.InvokeAsync("Name") and in-repo ViewComponent("Name")
as CALLS to workspace ViewComponent classes so impact sees real callers
instead of an empty graph. SDK types stay unresolved.

Fixes #2991

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

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

* fix: scan Razor and C# ViewComponent names without regex holes

Use string-aware lexers so combined Name= aliases, code-block calls,
this/base helpers, and escaped @@ markup match ASP.NET instead of
emitting false or missing CALLS.

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

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

* perf: skip Razor scans without ViewComponent tokens

Preserve the lexer correctness fixes while avoiding per-character work for
the common view that cannot contain a supported invocation.

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

* test: gate Razor ViewComponent extractor scaling in CI

Wire mixed-corpus tripwire + GITNEXUS_BENCH loader/scaling checks into the dedicated ci-tests benchmarks job so the #2991 lexer cannot regress without a wall-clock gate.

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

* fix: read Razor views through one file handle

CodeQL js/file-system-race: the size gate stat'd the path and the read
re-resolved it, so a template swapped in between could be read past the
size ceiling. Both now go through the same handle.

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 22:41:13 +00:00
azizur100389
4f16bd8023
fix(impact): report scope extraction omissions (#3071)
* fix(impact): surface scope extraction omissions

* fix(impact): preserve complete index fixtures

* fix(impact): preserve scope completeness evidence

* test(analyze): model successful scope extraction in harnesses

---------

Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
2026-08-29 08:38:20 +01:00
azizur100389
ac68f5254c
fix(ingestion): preserve object handler identity (#3046)
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
* fix(ingestion): preserve object handler identity

* fix(impact): cap object callable expansion
2026-08-26 15:44:33 +01:00
azizur100389
e87b1c3ffd
fix(php): gate imports by Composer autoload map (#2987)
* fix(php): gate imports by Composer autoload map

* fix(php): handle Composer catch-all mappings

* test(php): clarify Composer fallback coverage

* bench(php): fold Composer into canonical arm

---------

Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
2026-08-25 07:56:41 +01:00
azizur100389
b77d6f662b
fix(kotlin): resolve imports from declared packages (#2990)
Some checks failed
CodeQL / Analyze (javascript-typescript) (push) Waiting to run
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 / Classify release event (push) Waiting to run
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
Skill copy sync / shipped skills drift guard (push) Has been cancelled
2026-08-18 20:47:30 -07:00
azizur100389
87dc6c4d00
fix(go): gate imports by module path (#2984) 2026-08-18 14:31:40 +01:00
azizur100389
dac33d8056
fix(java): resolve imports from declared packages (#2955)
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
Resolve Java imports against parsed package declarations, expand package wildcards deterministically, and keep external imports unresolved when no in-repo package declares them.

Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com>
Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
2026-08-15 11:53:28 +01:00
Gergő Magyar
28187bb3a7
fix(typescript): resolve imports against declared config, not path suffixes (#2953) (#2956)
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
* fix(typescript): resolve imports against declared config, not path suffixes (#2953)

TypeScript/JavaScript/Vue import resolution ended in `suffixResolve`, which
answers "does any file in this repo have a path ending in this specifier?" and
answers it by dropping leading segments until something matches. That is not
module resolution, and it failed in both directions at once:

  - `@acme/telemetry/nest`, a registry dependency with no in-repo file, landed
    on the repo's only path ending in `nest/index.ts` — a false IMPORTS edge at
    confidence 1.0, indistinguishable downstream from a real one. The reporter
    measured 44 of 74 `apps/ -> packages/` edges landing on two such files.
  - `@repo/utils`, a first-party workspace package, resolved to nothing: its
    name lives in `packages/utils/package.json` and appears in no file path, so
    a path matcher cannot find it. Zero CALLS from 75 import statements.

Both come from the same missing input — nothing read the config that says what
exists — so both are fixed by reading it.

Replaces the suffix matcher on this path with the algorithm tsc and Node
actually run, in their order: relative/absolute, `#imports`, tsconfig `paths`
(longest literal prefix wins, every target tried), tsconfig `baseUrl`, then the
workspace package's own `exports`/`main`. A specifier none of those declare is
external, and resolves to nothing. There is deliberately no fallback.

New:
  - `typescript/tsconfig.ts` — every tsconfig/jsconfig in the repo with
    `extends` chains resolved, nearest-config-wins per file. The old loader read
    three filenames at the repo root, required `paths` to exist, and kept only
    `targets[0]` — none of which describes a monorepo, where `apps/web/
    tsconfig.json` is what governs `apps/web/src/main.ts`.
  - `typescript/module-resolution.ts` — the algorithm.
  - `typescript/file-candidates.ts` — 11 TS-family extensions, replacing a
    shared 39-entry list spanning every indexed language, so a TypeScript
    import can no longer resolve to a `.py` file.
  - `import-resolvers/node-workspace-packages.ts` — in-repo manifests, with
    `exports` subpath maps, patterns, condition nesting, and the restriction
    that a package declaring `exports` exposes only what it lists.

The per-pass `SuffixIndex` is gone from these three adapters: real resolution
derives nothing from the file list — every candidate comes from a declared
source and is checked with one `Set.has` — so there is nothing left to cache.
Their `*-import-index-reuse` guards and the JS index-vs-scan differential are
deleted with the mechanism they measured; the cross-language contract test
moves the three languages to its existing `KNOWN_UNINDEXED` channel, and pins
the exemption as a list so a fourth arrival is deliberate.

Python, Ruby, Java, Go and the rest still route through `suffixResolve` and are
untouched here.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015Px638Zyqa9CJMUU7DsJoB

* test(scope-resolution): assert every resolver refuses external imports (#2953)

One property, for all 16 registered resolvers: a specifier naming something
outside the repository must not resolve to a file inside it.

That is the property #2953 was filed against, and its violation is not a
missing edge but a fabricated one — an IMPORTS edge at full confidence between
two files with no relationship, which `impact` then reports as blast radius.
The mechanism is shared (`suffixResolve`), so the guard is too.

Every case pairs an external specifier with a DECOY: an unrelated in-repo file
whose path ends the way the specifier does. Without one a resolver that merely
found nothing would pass while holding no property at all, so each case also
asserts the decoy is reachable by the spelling that SHOULD find it — a typo in
a fixture cannot manufacture a pass.

Two fixtures had to be corrected before the results meant anything, and both
would have recorded a false gap:

  - C# reads its #1881 gate from scanned namespace evidence and fails OPEN
    without any, so passing `undefined` measured nothing. Armed, C# holds.
  - C++ was posting a pass on an extension mismatch (`vector` could never match
    `src/vector.hpp` whatever the resolver did). Given the header spelling, it
    does not hold.

Result: six hold it — TypeScript, JavaScript and Vue because they resolve
against declared config only (#2953); Python (#898) and C# (#1881) because they
gate the fallback on in-repo evidence; Rust because `::` never decomposes into
a path suffix, which the decoy-reachability arm confirms is a real pass rather
than a vacuous one.

Ten do not, and are recorded in KNOWN_GAPS with what each currently answers:
Java, Kotlin, Go, Ruby, PHP, Dart, Swift, C, C++, COBOL. The map is a work
list, not an allowance — the entries are ASSERTED, so a language that starts
holding the property fails here and its line gets deleted deliberately rather
than rotting into a lie.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015Px638Zyqa9CJMUU7DsJoB

* fix(typescript): admit only declared workspace packages, and fix four resolver defects (#2953)

Review of #2956 found one boundary bug and four correctness defects. The
boundary one is the same defect class this PR exists to fix, arriving from a
different direction.

## The workspace boundary (review)

`loadNodeWorkspacePackages` registered every `package.json` the repo-wide scan
found, and never read `pnpm-workspace.yaml` or a root `workspaces`
declaration. Finding a manifest is not the same as the workspace admitting one:
an app importing registry package `foo` would bind to an excluded fixture or
example that happens to declare `name: "foo"` — the false-positive half of
#2953, from a new source of evidence. This repository is the example, since
`test/fixtures/**` declares `@repo/utils` among others.

The admitted set now comes from the declaration — `workspaces` (array and yarn
object form), `pnpm-workspace.yaml`, `lerna.json`, with `!` exclusions and
`*`/`**` — plus the root package itself. A repo that declares no workspace has
exactly one package: the root. A negative fixture pins it, with a named package
outside the declared globs that must not resolve.

## Four defects

  - tsconfig `paths` targets were resolved against the config's own directory
    when it declared `paths` but inherited `baseUrl`. tsc resolves them against
    the EFFECTIVE base, so an extending config loaded the right alias pattern
    and pointed every target at the wrong directory.
  - two configs in one directory were ranked by directory-listing order, so
    `tsconfig.base.json` could govern instead of `tsconfig.json` and a config's
    own `paths` went invisible. Found by the test written for the fix above.
  - an unexported package subpath also tried `<dir>/src/<subpath>`. Nothing
    declares that mapping; it is the same kind of guess this PR removes, and
    the import it "resolved" is broken in the real project too.
  - `imports` pattern keys (`"#internal/*"`) were looked up exactly, so a valid
    `#internal/foo` never matched. `exports` and `imports` now share one
    matcher, which is where they should never have diverged.
  - a relative specifier climbing past the repo root was silently clamped, so
    `../../../secret` from `src/main.ts` became `secret` and could resolve a
    root file it never named.

## Test rigor

The conformance suite asserted less than it claimed. The decoy-reachability arm
only checked non-empty, so five cases paired `reachesDecoy` with a different
file than `decoy` and passed while establishing nothing; the KNOWN_GAPS arm
likewise accepted any in-repo answer instead of the recorded one. Both now
assert the exact file. The reachability arm runs only for languages that HOLD
the property — for a gap language the recorded-answer assertion IS that proof,
and for Swift and COBOL no other spelling exists, since `Foundation` and
`EXTERNAL` name the in-repo directory and copybook as well as the external
module, which is precisely why those resolvers cannot tell them apart.

## Benchmarks

Both `--check` guards were red, and both were reporting something true.

`import-target`: the ts-family arms resolved 0 of 3200 imports. Their corpus is
bare specifiers with no config, which the deleted `suffixResolve` answered
without one — so the arms measured an empty branch while printing a clean
scaling ratio. Each now carries the config its corpus is spelled for, and the
`deep` arm's uniform prefix reaches it. THE FINGERPRINTS THEN MATCHED THE
RECORDED BASELINES EXACTLY: same corpus, same targets, once the config it
always implied is passed explicitly. Retained per-pass index went from
26 745 296 B (js, ts) and 28 884 016 B (vue) at 32 000 files to 0-16 B, because
these resolvers no longer build one; they move to the `HEAP_BOUNDED` tier rust
already occupies for the same reason. Depth ratio moved 2.0 -> ~2.2 and the
budget goes to 2.6: candidates now carry the 16-segment baseUrl prefix, so each
`Set.has` hashes a longer string — linear in path LENGTH, independent of file
COUNT.

`scope-capture`: TypeScript capture fingerprint drift, caused by this PR's 12
new `.ts` fixtures entering the corpus. Attribution is exact rather than
inferred — moving that one fixture directory aside returns the fingerprint to
`f719163e…` byte-for-byte with `fixture_count` back at 155 and all 15 languages
passing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015Px638Zyqa9CJMUU7DsJoB

* fix(typescript): honour exports fallback arrays, paths precedence and package extends (#2953)

Second review round. Four findings, judged against what this tool is: a static
analyser building a code graph, not a compiler. The bar is resolving what the
project DECLARES, on a checkout that may never have been built or installed,
and never inventing an edge.

  - `exports` and `imports` ARRAYS were skipped. An array is Node's ordered
    fallback list, and `{"./feature": ["./dist/feature.js", "./src/feature.ts"]}`
    is exactly what a workspace package publishes to mean "built output, or
    source". Skipping it dropped the declaration entirely and left the package
    looking as though it exported no subpaths. The source arm is the one that
    matters here, because `dist/` is build output and is not indexed — and for
    a static analyser the build need not have run at all.
  - an exact `paths` pattern did not reliably outrank a wildcard. `a` and `a*`
    both match `a` with the same literal prefix length, so sorting on length
    alone left tsc's exact-wins rule to declaration order.
  - package-form `extends` (`"@acme/tsconfig"`) was refused outright. Not
    indexing `node_modules` is different from not READING it, and a shared
    internal base is where a monorepo puts the `paths` its packages import
    through. It is now read from disk, walking `node_modules` up from the
    extending config the way Node does, and absent on an un-installed checkout
    it degrades to whatever that config declared itself.

    The test pins what tsc actually does with such a base rather than what one
    might hope: `extends` never rebases `baseUrl`, so a package base's paths
    point at the package's own directory. That is why a published base rarely
    contributes aliases a repo's files resolve through, and why the
    `@tsconfig/*` family — which sets `target` and `lib`, never `paths` — is a
    no-op here either way.
  - CodeQL flagged `String.replace('*', …)` in two places as replacing only the
    first occurrence. Node subpath patterns and tsconfig `paths` both allow AT
    MOST one `*`, so that IS the specified behaviour — but the spelling states
    it by accident and reads as the replace-all footgun. `substituteStar`
    slices at the known index and says the rule.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015Px638Zyqa9CJMUU7DsJoB

* fix(typescript): treat `exports` as the whole interface, and keep empty tsconfig scopes (#2953)

Third review round. Two findings, both valid, both cases of this resolver being
laxer than the thing it models — which is the direction that fabricates edges.

  - `exports`, when a manifest declares it, is the package's ENTIRE public
    interface: Node ignores `main` outright and refuses any subpath the map
    does not list. This resolver already honoured that restriction for
    SUBPATHS and not for the package ROOT, which is the same rule. A manifest
    exporting only `"./feature"` therefore still answered a bare `@repo/pkg`
    with `main` or `src/index` — an edge for an import that does not resolve in
    the real project. Legacy and conventional root candidates are now offered
    only when there is no `exports` field at all.

  - a tsconfig declaring neither `baseUrl` nor `paths` was dropped rather than
    kept as an empty scope, so `tsconfigFor` fell through to an enclosing
    config. A package whose own tsconfig declares no `baseUrl` — meaning its
    non-relative specifiers are package lookups — silently inherited the repo
    root's aliases instead. An empty scope is the accurate answer for such a
    file, and only a scope can express it.

Both are pinned at the level they broke: the manifest arms assert what
`readManifest` produces, not a hand-built package, since the resolver honouring
empty entries and the loader producing them are different claims.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015Px638Zyqa9CJMUU7DsJoB

---------

Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-14 10:13:04 +01:00
Gergő Magyar
77360e1043
fix(scope-resolution): make interface dispatch generic-instantiation aware (#2912) (#2939)
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
* fix(scope-resolution): make interface dispatch generic-instantiation aware (#2912)

Interface-dispatch fan-out walked the subtype closure with generic arguments
erased, so `IValidator<string>` and `IValidator<int>` — one declaration, one
subtype list — were indistinguishable and a call through the first reached
`IntValidator.Check(int)`, a target no runtime dispatch can produce.

The arguments were already in the capture, unread: every language anchors
`@reference.inherits` on the whole base node while `@reference.name` keeps the
erased base. `ReferenceSite.typeArguments` is therefore derived generically in
`scope-extractor.ts` from the anchor's own spelling — no per-language query
changed — covering C#, Java, TypeScript, Kotlin, Go (`Base[int]` embedding),
Python (`Base[User]`) and Swift; Rust and Dart anchor on the bare name and get
nothing, which reads as "unknown". `preEmitInheritanceEdges` is the only code
that pairs a heritage site with a resolved (subtype, supertype), so it records
the instantiation there and hands it to the dispatch pass.

The closure is then walked carrying a substitution, as a type checker would:
`Wrapper<T> : IValidator<T>` binds T to the receiver's argument and stays
reachable from every instantiation, while its own subtypes are matched against
that binding. An incompatible hop is skipped without being marked seen, so a
type reachable by a second, compatible path still gets its edge, and without
descending, since its subtypes inherit the mismatch.

Pruning happens only on positive evidence that two instantiations differ.
Unknown arguments on either side, an arity that does not line up, an unresolved
qualified spelling of the same simple name, or an argument that might be a type
variable the language never captured all keep the target. Telling an uncaptured
type VARIABLE from a concrete type is the crux: `typeParameters` is absent both
for a non-generic declaration and for every declaration in a language whose
query omits `@declaration.type-parameters`, so the pass reads the evidence in
front of it — one run resolves one language, so a single generic declaration
anywhere in it proves the captures record parameters. A language recording
neither arguments nor parameters keeps exactly its pre-#2912 fan-out.

Type arguments are compared as resolved declarations rather than spellings, so
`Models.User` and an imported `User` are one type; the new optional
`ScopeResolver.normalizeTypeArgument` hook canonicalizes a language's predefined
aliases, implemented for C# (`string` ≡ `String`) where mixing the spellings
would otherwise delete a real implementor.

Fan-out cap, skipped-target reporting, overload selection and non-generic
closure behaviour are unchanged. SCHEMA_BUMP 60 -> 64: the heritage arguments
are a parse-time capture, so a warm cache would replay pre-fix sites and leave
the filter silently inert on unchanged files (61/62/63 are claimed by open PRs).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01StNKYi7Qxv5DnSURZuFBef

* fix(scope-resolution): close the two generic-dispatch gaps (#2912)

The first commit left two shapes on the pre-#2912 fan-out. Both are now
covered, and the second one turned out to need a route the pipeline did not
have at all.

**Folded receivers (Cases 0 and 3b).** `this._validator.Check(x)` is typed by
the compound fold, and the fold answers with a CLASS — which is exactly what
loses the instantiation, since `IValidator<string>` and `IValidator<int>` fold
to one declaration. The fold now reports the SPELLING it typed each receiver
position from, through a pure side channel (`recordReceiverType`) added to the
one helper every declared-type route already shares plus the two return-type
routes; resolution is unchanged whether or not a caller passes it. The reader
keeps the last report and uses it only when it names the class the fold
returned, so an intermediate position cannot lend its arguments to another
class. This covers the dependency-injection shape the issue is really about —
a field-held generic interface — and multi-hop chains, where it is the last
hop's spelling that types the receiver.

**Rust and Dart heritage.** Neither recorded arguments, for two different
reasons, so both routes exist now:

  - Rust's `@reference.inherits` anchor is the trait identifier INSIDE a
    `generic_type`. Widening the anchor would move the site's range, and that
    range is part of every inheritance edge's id, so the arguments arrive
    through a new `@reference.type-arguments` sub-tag instead.
  - Dart's `implements` / `with` never become reference sites at all: they
    travel as heritage MARKERS and their edges are emitted by the language
    hook. The arguments ride the marker payload as an optional fourth field
    (dropped, not encoded, when the spelling contains the marker delimiter),
    and `ScopeResolver.emitHeritageEdges` now receives the same sink
    `preEmitInheritanceEdges` writes to, so whichever pass emits an edge
    records that edge's instantiation. Dart also gained the
    `@declaration.type-parameters` capture, without which its own type
    VARIABLES are indistinguishable from concrete arguments and
    `class Box<T> implements Validator<T>` would be pruned from every
    instantiation.

Note this makes Rust and Dart record their instantiations; it does not make
them fan out. Interface dispatch still fires only for a receiver whose folded
type is an `Interface` symbol, so a Rust `Trait` or a Dart abstract `Class`
receiver has no secondary targets to filter. Widening that gate emits new
edges for several languages and belongs to its own issue.

**Two matcher rules the wider coverage exposed.** A WILDCARD names a set of
types rather than one — `Repo<? extends User>` holds a `Repo<User>`, and
Kotlin's `Repo<*>` / `Repo<out User>` say the same — so a position with one on
either side is unknown; nullable spellings trip the same test, which costs a
little precision in the safe direction. And insignificant whitespace inside a
nested spelling (`Map<string, User>` vs `Map<string,User>`) is no longer a
difference.

One expectation changed in the #2833 field-receiver matrix: a
`Repo<Repo<User>>` receiver no longer reaches `UserRepo implements Repo<User>`.
That edge is precisely the false positive this issue is about, and the primary
edge to the interface's own declaration — which is what the matrix row exists
to prove — is untouched.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01StNKYi7Qxv5DnSURZuFBef

* refactor(scope-resolution): apply the quality pass to the #2912 change

Three cleanups, no behaviour change.

**One balanced-list scanner, not two.** `erasedTypeApplication` and
`typeApplicationArguments` each carried a copy of the same fiddly scan — one
bracket list, balanced, closing on the last character, non-empty — differing
only in what they did with the result. Both now call `balancedTailList`; the
rule that rejects `User[][]` and `Repo<User>?` lives in one place instead of
being free to drift between two.

**The receiver's arguments are parsed after the gates, not before them.**
`emitInterfaceDispatchFor` takes the receiver's declared SPELLING and parses it
itself, once the owner is known to be an Interface with subtypes. Every one of
the five cases calls it unconditionally and the overwhelming majority of
receivers are concrete classes that return at the first line, so the parse was
running per resolved receiver site to be discarded immediately. Case 4 and Case
6 now hand over the string they already hold, and the folded-receiver helper
returns the recorded spelling rather than parsing it.

**One question gates the whole instantiation apparatus.** Inside the closure
walk, the graph-id lookups now hang off "is the supertype's instantiation
known?" — false for every non-generic receiver and for every language that
captures no heritage arguments, which is what makes those walks cost exactly
what they cost before #2912.

Also lifted the argument-route choice in `pass5CollectReferences` out of a
nested ternary into a named `heritageTypeArguments`, where the reason the
explicit sub-tag wins over the anchor text can be stated once.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01StNKYi7Qxv5DnSURZuFBef

* test(scope-resolution): cover generic interface dispatch in Kotlin and Go (#2912)

Extends the #2912 dispatch coverage past C#/Java/TypeScript. No production
code changes — the derivation is language-agnostic by construction
(`heritageTypeArguments` reads the heritage anchor's own spelling), so the
question was only which languages actually reach the filter.

Kotlin rides the shared heritage pre-pass; Go reaches the same filter from
the other side, matching implementors structurally while the receiver's
`Validator[string]` spelling carries the instantiation. Both are confirmed
to prune the mismatched implementor.

Each language gets a NON-GENERIC control asserting the fan-out still reaches
every implementor. Without it the `not.toContain` assertion passes just as
well when a language emits no dispatch edge at all — which is what Dart,
Python and Rust were measured doing for this receiver shape, generic or not.
They are deliberately not asserted on here: a "filtered correctly" test over
a path that never fans out measures nothing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015Px638Zyqa9CJMUU7DsJoB

* test(bench): re-baseline the Rust and Dart capture fingerprints for #2912

The Rust trait-impl and Dart heritage capture changes this branch makes are
additive TEXT on existing matches — each carries the instantiation the clause
was written with — so they drift the scope-capture digest without adding or
removing a match. The baselines were never re-measured when those captures
landed, which left `measure.mjs --check` red on this branch independently of
the merge.

Re-measured rather than hand-edited. Rust's capture_groups_fp (3556) and
fixture_count (202) are unchanged across the move, which is the evidence that
this is digest drift and not a capture-set regression. The other 13 languages
are byte-identical; 15/15 pass.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015Px638Zyqa9CJMUU7DsJoB

* refactor(scope-resolution): quality pass over the #2912 change

Cleanup only — no behavior change. Findings from a four-angle review (reuse,
simplification, efficiency, altitude), applied where they were verified.

Reuse / duplication:
* `stripTrailingCallSuffix` was a second copy of `matchingOpenParen`'s backward
  balanced-paren scan. Both now live in `template-arguments.ts` beside
  `balancedTailList`, for the reason that helper was shared in the first place:
  two copies of a scan this fiddly are free to disagree.
* The two call-return arms of the compound fold repeated the same four-part
  expression character for character; they share `classOfReturnType` now, the
  return-type twin of `classOfDeclaredType`, which keeps the "look up by
  rawName, report the erased application" pairing in one place.
* `pipeline/run.ts` implemented first-writer-wins twice — once in the pre-pass
  and once in the provider sink. One store, one sink, one rule; the pass keeps
  its `Set<string>` return and the callable-flow-only arm stops building an
  empty map to satisfy a widened return shape.

Simplification:
* `subtypeParametersComplete` dropped a disjunct that could never decide: every
  `subDef` reaching it comes out of the same loop that sets
  `languageCapturesTypeParameters`, from exactly those defs.
* The heritage-argument lookup asked "is the supertype's instantiation known?"
  three times; `superGraphId` now gates the block once.
* `TypeArgumentResolver` and `HeritageInstantiationResult` un-exported — no
  consumer outside their module.

Efficiency (all on the per-site dispatch walk):
* `resolveSupertypeArgument` captures only the site, so it is built once per
  site instead of once per subtype visited; the subtype's scope id is looked up
  once per subtype instead of once per argument position.
* `erasedTypeApplication` no longer runs on every fold hop through a call — the
  spelling is built only once the lookup has found a class, since it is
  discarded otherwise.
* `normalize`+`compact` computed once per side rather than twice.
* Regex literals and the identity `normalize` fallback hoisted to module scope.
* C# `System.` prefix stripped with `startsWith`/`slice` instead of a regex.

Verified: tsc clean, build clean, 1994 scope-resolution unit tests, 171
generic-dispatch + generic-field-receiver integration tests, 15/15 capture
bench fingerprints unchanged.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015Px638Zyqa9CJMUU7DsJoB

* style: apply Prettier to the two files the quality pass reformatted

Whitespace only — `quality / format` (npx prettier --check .) was red.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015Px638Zyqa9CJMUU7DsJoB

* fix(scope-resolution): close the generic-dispatch review findings (#2912)

Addresses the gitnexus-check review on #2939.

A repeated type variable was rebound rather than unified: `class C<T> :
Pair<T, T>` accepted a `Pair<string, int>` receiver, with `T = int` silently
replacing `T = string` and the bogus substitution carried to the next hop.
It now unifies, and prunes only on the same positive evidence the concrete
path demands — an undecidable repeat keeps the target with no binding.

A type PARAMETER of the declaration enclosing either side is now recognised
and never compared. `subtypeParametersComplete` is evidence about the
SUBTYPE's parameter list and says nothing about a `T` written at the call
site, so `void Run<T>(IValidator<T> v) { v.Check(x); }` pruned every
implementor: unbounded, `T` grounds to nothing; bounded, it grounds to its
BOUND. Both read as a difference of type. That is the missing-edge failure
this filter is built to avoid, and it is the common dependency-injection
shape in C#, Java and Kotlin.

Making that recognition reliable is why generic METHODS now capture
`@declaration.type-parameters` in C#, Java and Kotlin — TypeScript already
did, which is why its generic functions never had the defect. The capture
feeds the existing `bindsTypeParameter` guard, so a method-level `T` also
stops resolving to a same-named class in every other lookup.

C# alias normalization additionally strips the `global::` qualifier, which
`import-decomposer` already unwraps elsewhere: `global::System.String` read
as unequal to `string` and pruned a live implementor.

The C# captures golden fixture is regenerated for the new capture; the
extractor reads `@declaration.type-parameters` generically, so no reader
changed. SCHEMA_BUMP 64 already covers these capture changes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015Px638Zyqa9CJMUU7DsJoB

* fix(scope-resolution): close the two remaining gitnexus-check findings (#2912)

`balancedTailList` counted ONE bracket family, so a crossed pair slipped
through: scanning `Foo<Bar]>` it never sees the `]`, reaches the final `>` at
depth zero, and reports `Bar]` as a balanced argument list — which
`typeApplicationArguments` then splits and `erasedTypeApplication` rebuilds a
spelling from. It now tracks a stack of expected closers, so every closer must
match the opener it actually closes and a crossed pair declines to `undefined`,
the "unknown" both callers already fail open on. Well-formed mixed nesting
(`List<Dict[a, b]>`) is unaffected.

C# `normalizeTypeArgument` stripped `System.` from every qualified spelling, so
`System.Custom` answered `Custom` and compared equal to an unrelated `Custom`
elsewhere in the workspace. The strip is now earned: a keyword answers from the
alias table first, and the qualifier is dropped only when what remains IS a
predefined type. `System.Custom` is returned as written and goes to the identity
comparison instead — the step that can actually tell two declarations apart.
`global::System.String` still meets `string`.

Both are pinned by unit tests, including the well-formed mixed nesting and the
`global::`-qualified ordinary type that must keep its qualifier.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015Px638Zyqa9CJMUU7DsJoB

* docs(csharp): record why a shadowed `String` keeps its implementor (#2912)

Answers a review finding rather than changing behavior.

A workspace may declare its own type named `String`, shadowing the BCL simple
name, and the alias table then reads `IValidator<String>` as the `string`
instantiation and keeps that implementor. That is the SAFE direction, not an
oversight: pruning instead would rest on the belief that two spellings differ,
which is the missing-edge failure `generic-instantiation.ts` exists to avoid.

Resolving rather than normalizing cannot settle it either — the identity
comparison needs a `definitionId` from both sides, and a built-in name carries
none, so "built-in versus workspace-declared implies different" would be a new
prune with no positive evidence behind it. The cost is one surplus edge for that
pair, which is exactly the pre-#2912 fan-out and no worse.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015Px638Zyqa9CJMUU7DsJoB

* refactor(scope-resolution): pair the receiver spelling with the class structurally (#2912)

The fan-out needs the spelling a receiver position was typed from, because the
class the fold returns has lost the generic arguments. That was carried by a
PASS-LEVEL mutable holder, written by every declared-type lookup anywhere in the
fold and read back through a def-id coincidence check, with the holder cleared
by hand before each call site. Three things were load-bearing and none were
enforced:

* the reset had to be remembered at every call site. It was not: the Case 3b
  retry (`rawName` then `rawName + '()'`) reset once, BEFORE the first attempt,
  so a spelling reported by the attempt that failed could be attributed to the
  one that succeeded.
* the holder outlived every resolution, so a site that resolved through a route
  reporting nothing could read the previous site's spelling if the def ids
  happened to line up.
* the pairing itself was inferred from "whichever lookup reported last", not
  from the fold's own bookkeeping — losing branches (an MRO walk that moved on,
  a step later folded past) report too.

`foldReceiverChain` already had the answer and threw it away: its final
`FoldState` holds `def` and `declaredType` produced by the SAME step. It now
reports that pairing last, so the structural route is the one that stands.

`resolveCompoundReceiverTyped` returns `{def, declaredSpelling}` and owns a sink
created and read within the single call, which is what removes the reset
discipline — a local cannot be forgotten, and each of the two retry attempts
carries its own. The def-id guard stays as the check that a report names the
class actually returned.

Behavior is unchanged: 1975 scope-resolution unit tests, 177 generic-dispatch
and generic-field-receiver integration tests.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015Px638Zyqa9CJMUU7DsJoB

---------

Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-13 14:50:53 +01:00
azizur100389
3d4a95360d
fix(java): materialize record component accessors (#2936)
* fix(java): materialize record component accessors

* fix(java): ignore receiver params in record accessor arity

* fix(java): address record-accessor review findings (#2917)

Five findings from the tri-review of #2936.

P1 — a synthesized callable evicted a source-written one from the method map.
`getMethodInfo` keyed its per-class map by `name:line`, but a callable that is
SYNTHESIZED at a position that is not its own declaration shares its owner's
line: a record's implicit accessor is minted at the component, and a C# 12
primary constructor at the owner's `parameter_list`. Both are appended last by
their extractor, so on a single line the synthesized entry overwrote the
explicit method's MethodInfo and both definitions collapsed onto one id —
`record P(int x, int y) { int x(int s) {...} }` lost `P.x#1` and rebound the
arity-1 call to the zero-argument accessor. Adds a required `MethodInfo.column`
and keys the map by `name:line:column` through a single `methodInfoKey` helper.
Required, not optional: an absent column would key an entry no lookup could
reach — a silent, whole-language loss of enrichment instead of a compile error.
All three lookup sites move together; the file's own lockstep docblock warns
that a half-applied change loses caller edges silently rather than dangling.
This also fixes the same collision in C#, which never touched record code.

Degenerate component names no longer mint a node. tree-sitter's zero-width
MISSING recovery token satisfies `name: (identifier)`, so `record M(int x, y) {}`
minted an empty-named Method whose returnType was the neighbouring `y`; and the
grammar admits `underscore_pattern` in the same field, which the query rejected
but the scope path accepted, so `record R(int _) {}` left a scope declaration
with no node behind it. One `isRecordComponentName` predicate now gates all
three emitters — query suppression, scope synthesis, and the method extractor —
so they cannot drift apart again.

Component annotations reach the implicit accessor (JLS 8.10.3 / 9.7.4) by
reusing the shared `extractAnnotations` helper. Deliberately over-approximate
and commented as such: `@Target` lives in another file and parsing is per-file.

`explicitZeroArgAccessorNames` is memoised per record node. It was rebuilt on
every component capture — O(components x body members) for one record, measured
at ~4x per 2x input — while the scope path already hoisted the identical call.

Docs: the `java-local-types` baseline now stores the `capture_groups_fp` its own
note cites, the SCHEMA_BUMP ledger no longer claims a v65 that nothing holds,
and `shouldSkipDefinitionCapture` documents that `defaultLabel` may be ignored.

Scope-capture fingerprints are unchanged (`measure.mjs --check` PASS, 15
languages): the bench corpus contains no degenerate components, so the new
predicate is inert on it. SCHEMA_BUMP stays 67 — this branch's existing claim
already covers the changed worker output; re-check it against origin/main before
merging.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015Px638Zyqa9CJMUU7DsJoB

* docs(ingestion): reunite the overload-suffix JSDoc with typeTagForId

The block describing the `~type1,type2` same-arity discriminator was stranded
above `buildCollisionGroups` when that function was inserted between it and the
`typeTagForId` it documents (#658). Adding `methodInfoKey` in this branch parked
it directly above yet another unrelated function, which gitnexus-check flagged.

Moves the comment down to the function it describes. No behaviour change.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015Px638Zyqa9CJMUU7DsJoB

---------

Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-13 08:43:11 +00:00
azizur100389
cdc98a9cf8
fix(java): capture enum interface heritage (#2935)
* fix(java): capture enum interface heritage

* fix(java): harden enum heritage dispatch

* test(java): refresh synthetic capture baselines

---------

Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
2026-08-13 07:04:00 +01:00
Gergő Magyar
d540b00184
fix(check): stop reporting erased and deferred imports as initialization cycles (#2934)
Some checks failed
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-web) (push) Waiting to run
Trivy Image Scan / Trivy (gitnexus-cli) (push) Waiting to run
Skill copy sync / shipped skills drift guard (push) Has been cancelled
2026-08-12 17:09:32 +00:00
Gergő Magyar
054641cafa
fix(scope-resolution): resolve a package whose directory name repeats higher in the path (#2881) (#2929)
* fix(kotlin): resolve a root-level package whose name repeats higher in the path

`getKotlinFileIndex` built its `dirChildren` buckets under two guards
inherited from the pre-index per-import scan rather than from anything
Kotlin requires: a `startsWith` test that skipped the bucket when the
path began with the package name, and an `indexOf` equality that
demanded the parent be the FIRST occurrence of `/<name>/` in the path.

`s` is taken as `dir.slice(i + 1)` at each `/`, so `dir` ends with `/s`
by construction and the file always IS a direct child of a directory
named `s`. The guards therefore dropped legitimate buckets:

  data/src/main/kotlin/com/example/data/Repo.kt   (leading, startsWith)
  top/data/mid/data/Repo.kt                       (mid-path, indexOf)

`import data.helper` resolved to null against both. Only the fan-out
tier was affected — `data.Repo` answers from `suffixByStem`, which
carries no such guard — which is why the shape looked narrow enough for
#2872 to preserve rather than change inside a performance PR.

Both guards are removed. The rule stays "the parent directory is named
`s`" — a name that appears in the path without being the parent
(`top/data/mid/Repo.kt` for `data.something`) is still not a child, and
a new case pins that.

Widening is filtered downstream for the fan-out tier, which hands the
finalize pass a candidate list (#1759), but NOT for the tier-1 fallback,
which commits to `children[0]` unfiltered — and that is where most of
the change lands: 149 of the 235 moved corpus records are a different
first child against 32 wider arrays. Both are deliberate. A narrower
bucket for the first-child tier alone would keep its answers identical
and would also leave `import data.*` — a wildcard, which strips to
`data` and lands on exactly that tier — resolving to null on the very
shape this fixes.

Both Kotlin benches are re-baselined deliberately, with the drift
measured rather than accepted:

  - bench/kotlin-import-target: 235 of 19968 distinct records moved.
    54 null -> resolved (the fix, and exactly the +54 in non_null),
    181 answers that changed within a now-larger bucket. Zero buckets
    lost a member, zero results were dropped, and every reselected
    answer's parent directory is the queried package segment. The
    corpus is untouched, so `cases` is unchanged and the fingerprint
    covers the same surface as the value it replaces.

  - bench/import-target: the collide arm needed a corpus edit beside
    the new numbers. Its `d % 7` slice imported `com.example.vendor{d}`,
    a package that exists nowhere, purely to mirror the unique arm's
    nested-slice MISS; with that slice now resolving, leaving it would
    have left collide at 1100 against small's 1153 and broken the
    same-workload invariant the arm is built on. That assertion is what
    caught it.

The gate controls were re-run against the new baseline, including one
the fix makes newly plausible: a HALF fix that drops only `startsWith`
and keeps the `indexOf` check still fails the fingerprint, so a partial
fix cannot land quietly.

Two gates moved with the code rather than being left behind:

  - kotlin `heap_reading_bytes` and `heap_ceiling_bytes` are re-recorded
    together as `_heap_reading_note` requires (48073096 -> 48200224,
    +0.264%, ceiling still 1.5x). The note says why that is small: the
    heap corpus is built with HEAP_PAD 8, so no path can begin with a
    suffix of its own directory and the leading-segment half of the old
    rule is invisible to that arm.

  - `depth_budget` 2.4 -> 2.2. Deleting two string comparisons per
    directory component is per-depth work, so the depth band fell from
    1.44-1.51 to 1.27-1.40; left at 2.4 the gate's headroom would have
    drifted from ~1.6x to ~1.8x without anyone deciding to loosen it.

`package-dir-index.ts` documents the same first-occurrence rule as
universal, and it is not any more: Go, Java and C# still carry it and
still have the shape. Fixing them means re-baselining three languages
and editing the verbatim pre-change scans that
import-target-index-parity.test.ts keeps as the specification, so it is
a separate change — the comment now says so instead of describing a rule
one of its readers no longer follows.

Fixes #2881.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W8QYxYvd5ntikRUvpLZD5e

* fix(scope-resolution): drop the first-occurrence directory rule for Java, Go and C# too

#2881 was reported against Kotlin, but the rule it removed was never
Kotlin's. It is what the pre-index per-import scan happened to compute —
`indexOf` for the package directory, then "nothing after the match holds
a slash" — and every resolver built to reproduce that scan inherited it.
Three still had it, and all three reproduced the reported defect:

  java   data/src/main/java/com/example/data/Repo.java  `import data.*`  -> null
  java   top/data/mid/data/Repo.java                    `import data.*`  -> null
  csharp Models/src/App/Models/User.cs                  `using Models;`  -> null
  csharp a/Models/b/Models/User.cs                      `using Models;`  -> null
  go     a/internal/auth/b/internal/auth/svc.go   import "internal/auth" -> null

Controls (`top/data/Repo.java`, `a/b/internal/auth/svc.go`) resolve, so
these are the rule firing rather than an unrelated miss.

Four sites, all reduced to "the file's parent directory ends with the
queried path":

  - `package-dir-index.ts` `matchingDirs` (Go, Java, C# without csproj):
    the `indexOf` equality becomes `endsWith`, which also subsumes the
    length guard it needed — a shorter haystack is false instead of
    comparing -1 to -1.
  - `csharp.ts` `matchingDirPositions` (csproj step 3): same, and still
    deliberately UNANCHORED, so `src/SubModels` keeps answering `Models`.
  - `csharp.ts` csproj step 2: `indexOf` -> `lastIndexOf`, EXCEPT for an
    empty `dirPrefix`, which must keep `indexOf`. Its needle is a bare
    '/', and step 3 answers that query from `singleSegmentDirs` ("exactly
    one directory deep"), which only the first occurrence expresses; with
    `lastIndexOf` there, step 2 accepts every `.cs` in any directory and
    diverges from step 3. The csproj parity test catches it.
  - `go.ts` `resolveGoPackage`: `indexOf` -> `lastIndexOf`. No production
    caller, but the parity harness copies it verbatim as its spec.

The two C# csproj sites must move together. Fixing only step 3 makes
`Lib.Models` return step 3's superset instead of step 2's segment-aligned
answer.

Risk is not symmetric across the three. Go's consumer is a fan-out list
and the finalize pass materializes one IMPORTS edge per element, so
widening only ADDS edges. Java and C#-without-csproj commit to a single
file through `firstFileDirectlyInPkgDir` with no downstream filter, so a
widened bucket can also change which file an already-resolving import
binds to — java's collide fingerprints moved while its resolved count
did not, which is exactly that. C#'s leg is additionally gated by
`csharpSuffixFallbackAllowed` (#1881) before resolution runs.

Gates:

  - Twenty fingerprints re-baselined across go, csharp and java (five
    arms plus the top-level alias each). resolved 979 -> 1153 small,
    4064 -> 4681 large for go and csharp; 1100 -> 1153 / 4456 -> 4681 for
    java. No `distinct_outcomes` moved.

  - csharp and java hit the same collide-arm trap Kotlin did: both sent
    their `d % 7` slice to a namespace that exists nowhere purely to
    mirror the unique arm's nested-slice MISS, so once that became a hit
    the arms resolved fewer imports than `small` and the same-workload
    assertion failed. Both now use their arm's ordinary spelling.

  - GO WAS NOT GATED AT ALL and the corpus had to change to make it so.
    Its nested slice repeated only the last segment (`src/pkg{d}/internal/
    pkg{d}`) while a Go query addresses the whole package path, so the
    directory never ended with the query and the rule was never reached —
    every go arm sat unchanged through the resolver fix. `uniqueDir` and
    `collideDir` now repeat the shape at the granularity Go queries.
    `languages.go.heap.path_segments` 13 -> 14 follows from that.

  - `csharp_csproj`'s heap reading moved -0.79% (stable across runs) and
    is re-recorded with its ceiling: the step-2 filter decides which lazy
    `getFilesInDir` maps the probe forces. Everything else stayed within
    +/-0.03%, which is this box's jitter — `_heap_reading_note`'s claim
    that the readings reproduce to the byte across processes did not hold
    here, and the note now says so.

The three parity harnesses keep VERBATIM copies of the pre-change scans
as their specification, so each copy was updated with the resolver and
the cases that pinned the rule now pin its removal. Two of them left the
`mustBeNull` set in the shared harness — they resolve now, which holds
them to the stronger "pin a winner" bar the rest of that arm uses.

Refs #2881.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W8QYxYvd5ntikRUvpLZD5e

* perf(kotlin): intern dirChildren keys per directory and compact the buckets

Two optimizations to `getKotlinFileIndex`, both output-identical, kept
because they were measured and a third was dropped because it was not.

1. PER-DIRECTORY KEY MEMO. The component walk over `dir` cut one `slice`
   per component per FILE, and every slice after the first file of a
   directory is a freshly allocated string that hashes to a key the map
   already holds and is then dropped. The key list is a pure function of
   `dir`, so it is interned once per DIRECTORY. Measured -18.4% to -21.7%
   of the build at 32 000 files; zero retained cost, the memo dies with
   the frame.

2. BUCKET COMPACTION at the freeze loop. `addChild` mints `[raw]` and
   pushes, and V8 grows a backing store by `old + old/2 + 16`, so the
   SECOND child takes a 1-slot store to 17 and every bucket then retains
   its overshoot. 61 144 buckets at 32 000 files, 52.9% of their slots
   empty, 88 B each. `slice()` on freeze: -5 397 768 B, -11.20%, and the
   predicted 5 382 507 B lands within 0.03% of it. Same fix and the same
   accounting as the python `byBasename` note this repo already carries.

   `length === 1` is skipped deliberately. A bucket that never grew is
   already exact, so slicing it allocates a second array to save nothing
   — on a corpus of single-file packages the unguarded form costs 31% of
   the build for zero bytes.

DROPPED: merging the `dirChildren` walk into the `suffixByStem` walk.
It measures -0.10% at 32k, +0.23% at 100k and +0.40% at one file per
directory, all inside a base-vs-identical-copy noise floor of -2.3% to
+3.1%, and it does not compose usefully with the memo — the second scan
it deletes is exactly the scan the memo makes rare. Only its provably
free half is kept: `stem.lastIndexOf('/')` in place of `norm.lastIndexOf`,
one backwards scan instead of two, exact because an extension carries no
'/'.

Neither optimization is visible to the correctness fingerprint, which is
the point and also the risk: it observes the index only through the four
resolver tiers, so a key-order move no corpus query reaches would survive
it. Correctness therefore rests on a structural comparison of all three
maps — key insertion order, values, bucket contents in order, frozen-ness
— over 1234 corpora in both iteration orders, 14 808 comparisons, zero
failures. The fingerprint, `cases` and `non_null` are unchanged and MUST
NOT be re-baselined by this commit.

Gates that did move, both because a reading and its budget move with the
code rather than when CI goes red:

  - `heap_reading_bytes.kotlin` 48 200 224 -> 42 802 456 with its ceiling
    at 1.5x. A memory WIN passes every arm, so nothing forced this.
  - `depth_budget` 2.2 -> 2.0. The memo turns a per-file component walk
    into a per-directory one, which is precisely the per-depth work this
    arm exists to see: the band went 1.27-1.40 -> 1.20-1.26, and 2.2 held
    over it would have drifted from ~1.6x headroom to ~1.9x.

The gate controls were re-run against the optimized builder, including
one this change makes newly plausible: keying the memo on the directory's
LAST SEGMENT instead of its full path drifts the fingerprint
(36a4e9dad313, non_null 13310 -> 13305). That is the memo's whole
safety argument stated as a test — its key decides which key set a
directory contributes — and it is the one way this optimization could
move an answer. The bucket-cap control was re-run too, since compaction
now rewrites the same buckets.

Also recorded, from measuring a reuse this repo had been invited to make:
replacing `dirChildren` with the shared `package-dir-index` is
output-identical (0 divergences over 107 948 answers) and passes every
arm of the kotlin bench at 1.37x-1.50x — while costing 409x per fan-out
and 8114x on `import data.*` at 200 matching directories on a corpus this
bench does not carry. `_blind_spot` in the kotlin baselines now says so,
with the memory the trade would have bought (26.2%, 12.18 MiB) and the
corpus arm that would have to exist first.

Refs #2881.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W8QYxYvd5ntikRUvpLZD5e

* fix(scope-resolution): close the gaps a four-lens review found in the #2881 change

Correctness review found no defect in the shipped resolvers — the `endsWith`
rewrites, the C# empty-prefix guard, the memo's purity, the `stem` vs `norm`
derivation, the Map re-`set` during iteration and Go's `substring` arithmetic
were each attacked with running code and each held. Everything below is a gap
in what the change ASSERTS, measures or claims.

UNGATED BEHAVIOUR, now covered:

  - `import-resolvers/go.ts` had no test at all. Its rule changed, the shared
    bench drives the indexed leg rather than this one, and a revert was caught
    by nothing. `go-package-resolve.test.ts` pins the membership rule and, more
    usefully, pins that Go's two independent legs agree on it — they disagreed
    before #2881 and a divergence here means the LanguageProvider hook and the
    ScopeResolver hook hold different views of a package.
  - The memo and the compaction are output-identical, so no fingerprint sees
    them and reverting either leaves both benches green. `kotlin-index-internals
    .test.ts` asserts them directly: the memo hit path against the miss path,
    two directories sharing a component-suffix keeping separate buckets (the one
    way a coarser memo key could move an answer), and that the bucket handed out
    is the cached, frozen, compacted array on both the sliced and the skipped
    path. The comment claiming this was "asserted structurally" previously
    pointed at nothing in the repo.

GATES:

  - Six ratio budgets in `bench/import-target` were slack: the measurements they
    bound got faster and the numbers were left alone. kotlin depth 3.4 -> 2.8,
    go 1.6 -> 1.4, csharp 2.2 -> 2.0, java 2.2 -> 2.1, kotlin collide_scaling
    1.8 -> 1.65, go 5.5 -> 5.1, each holding the headroom the old value
    expressed. The absolute ms ceilings are deliberately untouched: they carry
    runner-contention headroom, and a ratio is runner-speed-invariant where a
    millisecond is not. This is the failure the branch already fixed one
    directory over and missed here.
  - The `csharp_csproj` heap re-baseline is REVERTED. Base and branch both
    measure ~73.10e6 three runs each; the recorded 73703384 was simply not
    reproducible, and re-recording it would have dropped that language's derived
    floor 0.8% for no reason belonging to this change.
  - kotlin's collide arm was blind to the rule it was re-baselined for — a full
    revert of the Kotlin guards left both its fingerprints unmoved, because
    `com/example/models` is not a suffix of `…/models/inner/models`. Deepened to
    repeat the whole queried path; those two fingerprints are the only ones that
    moved for it. The same deepening on the java and kotlin UNIQUE arms was
    measured and REVERTED: ten more fingerprints, java's heap reading up 43%,
    and no coverage gained, because progressive stripping lands those queries on
    the same file either way.

SIMPLIFICATION:

  - `go.ts` now states the predicate as ends-with like its three siblings,
    instead of keeping the `indexOf` shape with `lastIndexOf` swapped in.
  - C# csproj step 2's direct-child filter is dead for a non-empty prefix —
    `getFilesInDir`'s keys ARE segment-aligned directory suffixes, so it cannot
    reject, and measurement agrees over 12 008 pairs. Only the empty-prefix case
    does work, and only that case remains.
  - `addChild` had one call site left; inlined. The memo's double read of its
    own lookup is gone. The V8 byte accounting duplicated verbatim between the
    resolver comment and the baselines note now lives only in the note.
  - Four copies of the same ternary in the csproj parity harness collapse onto
    one hoisted `dirTrail`; two locals in the java harness were named for the
    branch that was deleted.

CLAIMS THAT WERE WRONG:

  - `package-dir-index.ts` said "the four resolvers agree again". It is six, and
    the sixth is the evidence: `import-resolvers/jvm.ts` has answered the same
    question with `lastIndexOf` since #488, so before #2881 Java's and Kotlin's
    LanguageProvider hook and their ScopeResolver hook disagreed about which
    files a package holds.
  - The `uniqueDir` docblock claimed the last segment IS the query granularity
    for csharp/java/kotlin. They query the whole dotted path first and reach the
    tail only through stripping — which is why the partial-revert control fires
    on the go arm alone, now stated instead of implied.
  - Three parity harnesses described themselves as verbatim copies of the
    pre-change implementations; they were edited by this branch, so they are
    re-derivations of the current spec, a weaker claim their headers now make.
  - The shared harness header still listed the removed rule as current, the
    `DIRS` docblock still justified shapes by a divergence that no longer
    exists, and `measure.mjs`'s tier-two docblock plus `_heap_bound_note` still
    counted nine bounded languages when `HEAP_BOUNDED` derives to three — this
    branch had dutifully updated a kotlin bound in a list no gate reads.
  - `_blind_spot` told the next reader to build a repeated-leaf arm that already
    exists in the sibling bench, with a budget that already fails the swap.

Both baselines are also re-serialized to preserve each note's original escaping,
undoing ~20 KB of no-op churn an earlier revision introduced by round-tripping
the JSON.

Refs #2881.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W8QYxYvd5ntikRUvpLZD5e

* perf(scope-resolution): drop the string each membership test built per candidate

The three `endsWith` membership tests each minted a decorated copy of the
directory once per candidate, per import. The decoration cancels:

  ('/' + D + '/').endsWith('/' + P + '/')  <=>  D === P || D.endsWith('/' + P)
  (D + '/').endsWith(P + '/')              <=>  D.endsWith(P)

Verified exhaustively rather than argued — every pair of strings up to length 5
over `{a, b, /}` including the empty string, 132496 pairs, 0 divergences, with
the match count reported beside it because two predicates that agree on `false`
everywhere also show 0 divergences. `matchingDirs` 32.58 -> 8.22 ns/candidate
(3.96x), `matchingDirPositions` 64.9 -> 18.4 ns. C#'s deliberate unanchoredness
survives verbatim: `src/SubModels` still answers `Models`.

`resolveGoPackage` was the opposite of a win — the rewrite in this branch left
the `'/' + path` cons the old `includes` guard used to short-circuit, and the
first `endsWith` forces V8 to flatten it once per file. Working on the raw path
with an explicit start index is 4.8x faster than that and 1.78x faster than the
code before this branch. It also now reuses `resolveGoPackageDir` instead of
re-deriving six of its lines.

Three claims these files make are corrected while they are open:

- `package-dir-index.ts` argued the rule was accidental because a sixth
  implementation never had it, "wired as `importResolver` by
  `languages/{java,kotlin}.ts`" and therefore live. It is wired and not read:
  `provider.importResolver` is consumed only at `import-target-adapter.ts:74-75`,
  and that module's exports have no importer outside their own unit test, while
  its docblock claims it is threaded through `finalizeScopeModel`. The argument
  survives on the pre-index-scan derivation; `jvm.ts` is evidence about how the
  predicate was written, not about live behaviour. Whether those resolvers
  should be deleted or wired is left as an open question.
- `csharp.ts` derived the empty `dirPrefix` case from "any path whose first
  slash is its last", which is wrong in both directions: `src/X.cs` satisfies it
  and emits no empty key, `a//X.cs` violates it and does. The conclusion stands
  and the filter stays — it is what rejects `a//X.cs`.
- Step 2 returns on its first push, so widening it also suppresses step 3's
  unanchored leg. The narrower answer is the more precise one, but it was an
  unstated output change.

`SuffixIndex.getFilesInDir` now states the segment-alignment its callers rely on,
bounded as a guarantee about what may be RETURNED — php's root-anchored index
answers only the equality arm.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Kim7UK3kPhF1ZRZDuuvnus

* fix(scope-resolution): say what the widened bucket actually does downstream

The comment justifying the widening claimed a bucket that is too wide is
"filtered downstream" by the finalize pass. It is not, for the edge that
matters. `finalize-algorithm.ts` mints one draft per candidate, each keeping its
own `targetFile`, and the File->File emitter in
`graph-bridge/imports-to-edges.ts` tests only `targetFile === null` and
`targetFile === sourceFile` before adding an `IMPORTS` relationship at
confidence 1.0 — it never reads `linkStatus`. The `localDefs` filter from #1759
constrains `targetDefId` and the `BindingRef`; every extra bucket member is an
unconditional file-level edge regardless. Measured on an Android-shaped layout,
one `import data.load` goes from 5 to 6 edges, all six unresolved.

No filtering is added here. Whether an unresolved candidate should produce that
edge at all is a design question about the graph bridge, not about this bucket.

The published drift census — 149 first-child reselections, 32 wider arrays, 54
null -> resolved — has no bucket for a fourth class this change introduces.
Tier 3 precedes tier 4, so a bucket the guards used to leave empty returned null
and let the progressive strip run; a populated bucket stops tier 4 entirely,
turning a bound answer into a candidate list that need not carry the symbol.
Re-running the census with a shape classifier finds that class ZERO times over
the corpus, and the zero is the finding: the shape reproduces by hand, and this
bench's own generator at 4000 repositories hits it 4-12 times per seed. The
fingerprint cannot gate what the corpus cannot express — the same blindness the
go arm carried until #2881 widened it.

Two further claims are brought back in line with what shipped. The memo's
docblock said `kotlin-index-internals.test.ts` asserts the key set, key
insertion order and bucket order "over the built maps"; that file says it works
through the resolver's observable surface and omits key order deliberately. The
mutation matrix bounds it honestly: a mis-keyed memo is caught, a deleted one is
not, and the compaction's only instrument is the bench heap ceiling.
`findKotlinDirectoryChild` no longer claims to return "the same file the scan
used to return" — that is precisely what moved.

Structural, no behaviour: `let keys` sits with its consumer instead of 33 lines
above it, the archaeology moves to the docblock, `tight` -> `compacted`,
`dirEnd` -> `lastSlash` (the name three sibling builders use), and the one-use
`MutableDirChildren` alias goes with the `addChild` it existed for.

`finalize-algorithm.ts` annotates `targetFiles` as `readonly string[]` so
`Array.isArray`'s `any[]` predicate can no longer widen a frozen cached bucket
into something `.sort()` compiles against. The runtime freeze stays; it is the
backstop for every other call site.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Kim7UK3kPhF1ZRZDuuvnus

* test(scope-resolution): gate the edges #2881 moved but nothing watched

Every widened-shape test in the branch used a one-file corpus, so not one of the
149 first-child reselections was pinned — the tier that commits to `children[0]`
unfiltered had no test that could see which file it commits to. Kotlin and Java
now pin that choice absolutely, in both insertion orders, for the member path
(tier 3, both members) and the wildcard path (tier 1, one file) separately,
saying plainly that both candidates are valid members and the only tie-break is
file-set iteration order.

The tier-3-preempts-tier-4 class gets its first gate, with a control that makes
it a transition rather than a fact. The bench corpus holds zero instances, so
this case is the only thing standing between that behaviour and a silent
revert.

C# gains three absolute arms, because its differential harness cannot see any of
them — the legacy copy was edited in lockstep with production, which the file's
own header admits. One pins the empty-`dirPrefix` filter the branch calls
load-bearing and which nothing defended: deleting the guard leaves the whole
suite green but changes the answer, so the arm was verified to fail with the
guard removed and pass with it restored. Java gains the negative control Kotlin
already had.

`kotlin-index-internals.test.ts` stops implying coverage it does not have. The
mutation matrix is recorded in its header: deleting the memo passes every arm
(it is output-identical by construction), deleting the compaction's `slice()`
passes every arm (a JS array's capacity has no reflective surface), while
mis-keying the memo fails three and compacting-but-never-storing fails two. Four
arms were added that do fail under those mutations. V8's growth steps were
re-measured — 1, 19, 46, 86 with growth at lengths 2, 20, 47, 87 — so the old
1/17/41 model, which under-counted the slack at 40 files by 6x, is gone.

`go-package-resolve.test.ts` drops four `as never` casts that were hiding
nothing (`GoModuleConfig` is structurally satisfied), and pins vendor/, testdata/
and nested-go.mod directories, which merge into the importing package — a
pre-existing unmodelled gap, verified present before #2881 and documented as
such rather than blamed on it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Kim7UK3kPhF1ZRZDuuvnus

* test(bench): gate the bucket compaction, and publish the whole drift taxonomy

The compaction shipped with no gate anywhere. Deleting `bucket.slice()` while
keeping the freeze moves no fingerprint, no count and no test — only retained
heap, 42805256 -> 48184784 B (+12.57%), byte-identical across three runs. Note
the direction: compaction reclaims, so losing it makes the reading GROW, which
no floor can see. `heap_ceiling_bytes.kotlin` tightens 64203684 -> 46000000
(1.5x -> 1.0747x of the reading), leaving the regression 4.8% clear above the
ceiling and the reading 7.5% below it. The band is derived from first principles
in `_heap_compaction_gate` (~61000 buckets x 11 spare slots at Node 22's 1->19
step) so it can be re-checked rather than trusted, and the note carries the
triage rule: heapUsed accounting drift moves every arm, so kotlin alone over its
ceiling is a lost compaction.

`_gate_controls` claimed the two optimizations rest on a structural comparison
over 1234 corpora in both iteration orders. No such probe exists in the tree. It
now names the test that does exist and lists what it actually pins, and says
key insertion order is unasserted by design.

`_provenance` gains the full shape classification behind the 235 moved records:
149 string -> string, 38 null -> string, 16 null -> array, 32 array grew, and
zero of every other transition — including `string -> array`, the
resolved-becomes-unresolved class the old taxonomy had no bucket for. The
harness was validated byte-exactly first: driven over this corpus the base
resolver reproduces ebf1790bf1 / 13256 and head reproduces d91110bee3 / 13310.

`measure.mjs` loses a paragraph asserting the C# unique slice repeats the whole
queried path, directly above the paragraph explaining it is leaf-only
deliberately and the code that makes it so. Acting on the deleted half resolves
the csproj arm to zero. While measuring: the csharp collide arm is NOT blind —
its fingerprint already moves across #2881 — but both csharp_csproj arms are,
because `getFilesInDir` keys on segment-aligned suffixes and neither nested slice
is one. Closing that needs a corpus redesign and four re-baselines; recorded, not
attempted.

One number changes in either baselines file, and it tightens.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Kim7UK3kPhF1ZRZDuuvnus

---------

Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-12 10:26:39 +01:00