Skip to content

fix(ingestion): two-channel binding lifecycle (closes #1066) + scope-resolution I8 hardening#1082

Merged
magyargergo merged 11 commits into
mainfrom
fix/csharp-bindings-augmentation-channel
Apr 26, 2026
Merged

fix(ingestion): two-channel binding lifecycle (closes #1066) + scope-resolution I8 hardening#1082
magyargergo merged 11 commits into
mainfrom
fix/csharp-bindings-augmentation-channel

Conversation

@magyargergo

Copy link
Copy Markdown
Collaborator

Summary

Fixes issue #1066 and refactors the C# cross-file binding fanout so we no longer mutate finalized scope bindings — the original cloneBindingBucket workaround violated ScopeResolver Invariant I8 in spirit and was the wrong layer to patch.

This PR consolidates everything into one coherent change:

  1. Direct fix for Analysis failed: Phase 'scopeResolution' failed: Cannot add property 2, object is not extensible #1066 (commit a18f6e1):

    • Adaptive tree-sitter buffer (getTreeSitterBufferSize) on cache-miss parse() calls in C#, Python, and TypeScript captures — eliminates the Invalid argument crash on large files (>32KB).
    • Workaround cloneBindingBucket helper to copy frozen BindingRef[] arrays before pushing — fixes the Cannot add property N, object is not extensible crash.
    • Comprehensive regression tests (unit + integration + 77KB padded fixture).
  2. Architectural refactor (commits 0a9679b1c23a4620): the workaround in step 1 contradicted I8. The clean fix is a two-channel binding lifecycle:

    • indexes.bindings — finalize-output channel, deep-frozen post-finalize, never mutated by hooks.
    • indexes.bindingAugmentations — new append-only post-finalize channel for hooks like populateNamespaceSiblings.
    • All read sites consult both channels via a new lookupBindingsAt helper.
    • C# populateNamespaceSiblings now writes to bindingAugmentations, not bindings. cloneBindingBucket is deleted.
    • Dev-mode validator validateBindingsImmutability (gated on NODE_ENV !== 'production' && VALIDATE_SEMANTIC_MODEL !== '0') catches drift if a future hook regresses.
    • Invariant I8 in contract/scope-resolver.ts rewritten to capture the new contract; lifecycle comment in gitnexus-shared/types.ts extended.

Commit-by-commit

# Commit Purpose
1 a18f6e1 fix(csharp): adaptive tree-sitter buffer + frozen-bucket clone for cross-namespace siblings (#1066) Direct #1066 fix (workaround) + regression suite.
2 0a9679b1 refactor(scope-resolution): add bindingAugmentations channel to indexes Step 1 — interface + finalize-orchestrator return empty Map.
3 b88570fd feat(scope-resolution): add lookupBindingsAt dual-source helper Step 2 — helper + namesAtScope companion + 8 unit tests.
4 40e2864c refactor(scope-resolution): route binding lookups through lookupBindingsAt Step 3 — findClassBindingInScope / findCallableBindingInScope / findExportedDefByName / propagateImportedReturnTypes go through the merger.
5 516e03f1 refactor(csharp): write namespace siblings to bindingAugmentations channel Step 4 — switch C# write target; delete cloneBindingBucket.
6 c23a4620 feat(scope-resolution): tighten I8 + add validateBindingsImmutability dev guard Step 5 — contract docs + dev-mode validator + 6 validator tests.

Validation

  • npx tsc --noEmit clean.
  • npx prettier --check clean (only .cursor/hooks.json warns — untracked workspace file).
  • 7235 / 7327 vitest tests pass. 6 failures are pre-existing and unrelated:
    • 4 Swift METHOD_IMPLEMENTS regressions (verified on baseline pre-PR).
    • 1 Swift method-extraction unit test (Swift parser-side issue).
    • 1 Swift constructor-inference unit test (Swift type-env issue).
    • 1 LadybugDB vector-extension test failed to acquire its file lock on Windows temp dir (documented gotcha in AGENTS.md).
  • All 4 issue Analysis failed: Phase 'scopeResolution' failed: Cannot add property 2, object is not extensible #1066 regression integration tests green (csharp-large-cache-miss-resolution).
  • All 8 walkers-augmentations.test.ts cases green (precedence, dedup-by-def.nodeId, empty-arrays-merge, fast-path miss).
  • All 6 validate-bindings-immutability.test.ts cases green.

Bidirectional regression verification (Step 7 of plan)

To prove each architectural piece carries weight, the architecture was reverted in two surgical ways and the regressions were observed before re-applying.

  • Reverted Step 4 alone (write target back to indexes.bindings):

    TypeError: Cannot add property 1, object is not extensible
      at populateCsharpNamespaceSiblings (.../namespace-siblings.ts:346:19)
    

    ✅ Same crash as Analysis failed: Phase 'scopeResolution' failed: Cannot add property 2, object is not extensible #1066. Re-applied — green.

  • Reverted Step 3 alone (read sites bypass lookupBindingsAt):

    AssertionError: expected undefined to be defined  // CreateUser -> User constructor
    AssertionError: expected [ 'import-resolved', 'global' ] to include 'scope-resolution: call'  // CreateUser -> Save
    

    ✅ Augmentations invisible to readers; cross-file C# resolution regresses. Re-applied — green.

Test plan

  • npx tsc --noEmit
  • npx prettier --check (touched dirs)
  • Full vitest run (npm test)
  • Targeted regression: test/integration/resolvers/csharp.test.ts -t "issue #1066"
  • Step-7 bidirectional reversal sanity (documented above)
  • Step-by-step commit verification (each commit is independently green; pre-commit hook ran tsc on every commit)

Notes for reviewers

  • This is intentionally a single PR with the immediate fix (commit 1) followed by 5 architectural commits. The first commit alone resolves Analysis failed: Phase 'scopeResolution' failed: Cannot add property 2, object is not extensible #1066 in production behavior; commits 2–6 replace the workaround with a clean architecture without changing behavior. Any commit can be cherry-picked / inspected in isolation.
  • validateBindingsImmutability is a dev-mode-only guard. In production it is a no-op (return 0 on the first line).
  • lookupBindingsAt returns the existing array by reference when only one channel populates it (zero allocation for the common case where augmentations are empty).

Closes #1066.

…oss-namespace siblings (#1066)

Two coupled regressions surfaced when analyzing real-world C# repos with
large source files (issue #1066):

1. Tree-sitter `parser.parse()` is hard-coded to a 32 KB buffer by
   default. Any file exceeding that threshold throws `Invalid argument`
   on the worker re-parse path of `populateCsharpNamespaceSiblings`
   (and the analogous Python / TypeScript captures fallbacks).
2. After the buffer fix unblocks the AST walk, the hook tries to
   `push()` onto the inner `BindingRef[]` array fetched from
   `indexes.bindings` — but `materializeBindings` froze that array via
   `Object.freeze(refs.slice())`. Result: `Cannot add property N,
   object is not extensible`.

Fixes:

- `csharp/captures.ts`, `python/captures.ts`, `typescript/captures.ts`:
  pass `bufferSize: getTreeSitterBufferSize(sourceText.length)` to
  `parser.parse()` on the cache-miss path so multi-MB files parse.
- `csharp/namespace-siblings.ts`: introduce `cloneBindingBucket` to
  copy the frozen array before mutating, then `set()` the new array
  back. This is a working but architecturally compromised workaround
  (#1050 follow-up will replace it with an explicit augmentation
  channel — see docs/plans/2026-04-26-001 plan).

Tests:

- New `csharp-large-cache-miss-resolution` fixture (Models/Services/
  Other layout, ~77 KB padded UserService.cs) drives the buffer-size
  failure end-to-end through worker mode.
- `csharp.test.ts`: 4 new regression assertions covering both the
  parse-time buffer-size failure and the freeze workaround.
- Per-language captures unit tests gain "large cache-miss file uses
  adaptive buffer" coverage (TS, Python, C#).
- `csharp-hooks.test.ts`: in-memory freeze regression test that
  reproduces the `Cannot add property` crash without invoking the C#
  parser at all.

Made-with: Cursor
Step 1 of the binding-augmentation-channel refactor (issue #1066
follow-up). Pure shape change — no consumers yet.

Adds a new `readonly bindingAugmentations` field to
`ScopeResolutionIndexes` initialized as an empty `Map` by
`finalizeScopeModel`. The new channel is the dedicated post-finalize
write target for hooks like `populateCsharpNamespaceSiblings`, so
`indexes.bindings` can stay frozen and finalize-owned.

Behavior unchanged: nothing reads or writes the new field yet. tsc and
the full unit suite remain green.

Plan: docs/plans/2026-04-26-001-binding-augmentation-channel.md (local
only — `docs/plans/` is gitignored).

Made-with: Cursor
Step 2 of the binding-augmentation-channel refactor. Introduces a
single primitive every walker uses to read both the finalize-owned
`indexes.bindings` channel and the post-finalize
`indexes.bindingAugmentations` channel.

Contract:
- Finalized refs come first (preserves existing precedence).
- Augmented refs append, deduped by `def.nodeId`.
- Empty input on both channels returns a shared frozen empty array.
- Single-channel hits return the bucket by reference (no allocation).

No consumers are wired yet — Step 3 routes the existing walker
primitives through this helper. Augmentations remain empty for every
language; behavior of the full suite is unchanged.

8 unit tests pin precedence, dedup, identity for single-channel hits,
and the shared-empty-frozen-array sentinel.

Made-with: Cursor
…ngsAt

Step 3 of the binding-augmentation-channel refactor. Every direct
`indexes.bindings.get(...)` consumer in the post-finalize phase is
now routed through `lookupBindingsAt` (per-name) or `namesAtScope`
+ `lookupBindingsAt` (bulk iteration).

Routed sites:
- `findClassBindingInScope` (walkers.ts) — class-receiver lookups.
- `findCallableBindingInScope` (walkers.ts) — free-call lookups.
- `findExportedDefByName` (walkers.ts) — module-scope-fallback
  callable lookups.
- `propagateImportedReturnTypes` (passes/imported-return-types.ts)
  — bulk iteration over an importer's binding entries; switched to
  `namesAtScope` + per-name `lookupBindingsAt` so post-finalize
  augmentations are visible to import-derived typeBinding mirrors.

Behavior unchanged: augmentations are empty across the suite (Step 4
populates them for C# `populateNamespaceSiblings`). 587
scope-resolution unit tests + 50 integration resolver suites green
(4 pre-existing Swift method-implements failures unrelated to this
work).

Adds `namesAtScope` companion helper for the bulk-iteration callers.

Made-with: Cursor
…annel

Step 4 of the binding-augmentation-channel refactor. The C#
`populateNamespaceSiblings` hook is the only consumer that needed
to inject cross-file bindings post-finalize, and prior to this
change it cloned the (frozen) finalized `BindingRef[]` arrays
through a `cloneBindingBucket` helper, then `set()`-back the new
array — a workaround for the `Object.freeze` applied by
`finalize-algorithm.ts` (issue #1066 root cause).

Architecturally that violated `ScopeResolver` Invariant I8 (which
permits post-finalize modifications but not in-place mutation of
finalized buckets). It also forced read-side consumers to be aware
of the workaround.

This change:
* Switches the three C# write sites to append into
  `indexes.bindingAugmentations` via `getAugmentationBucket`. The
  augmentation channel was added in Step 1 and is mutable by
  contract: inner `BindingRef[]` arrays here are NEVER frozen.
* Deletes `cloneBindingBucket` and `getMutableScopeBindings`
  (workaround helpers no longer needed).
* `lookupBindingsAt` (Step 2) merges the two channels transparently
  for every walker (Step 3), so behavior is unchanged for callers.
* Updates the unit test to assert against both channels: finalized
  bucket stays frozen and untouched, cross-file siblings show up in
  augmentations only. Renamed the test accordingly.

Validation:
* `npx tsc --noEmit` clean.
* csharp hooks unit + walkers-augmentations unit + csharp integration
  resolver suite all green (236/236).
* Wider `test/unit/scope-resolution test/integration/resolvers`
  suite: 2507 pass, only 4 pre-existing Swift METHOD_IMPLEMENTS
  failures remain (unrelated to this work, present on baseline).

Refs: issue #1066, ADR-pending binding-augmentation-channel.
Made-with: Cursor
… dev guard

Step 5 of the binding-augmentation-channel refactor. Captures the
new two-channel binding lifecycle in the contract docs and adds a
dev-mode runtime validator so a future hook cannot silently drift
back into mutating `indexes.bindings`.

Contract changes:
* `contract/scope-resolver.ts` — rewrote Invariant I8 to describe
  the two channels (`indexes.bindings` is finalize-output and
  immutable post-finalize; `indexes.bindingAugmentations` is the
  append-only post-finalize channel populated by hooks like
  `populateNamespaceSiblings`). Documented `lookupBindingsAt` as
  the read-side merger and pointed at the new validator as the
  enforcement mechanism.
* `gitnexus-shared/src/scope-resolution/types.ts` — extended the
  module-header lifecycle contract to call out
  `bindingAugmentations` alongside `ReferenceIndex` as the two
  structures populated after the freeze.

Validator:
* New `pipeline/validate-bindings-immutability.ts` mirrors the
  shape of `validateOwnershipParity` (#909): runs only when
  `NODE_ENV !== 'production' && VALIDATE_SEMANTIC_MODEL !== '0'`,
  emits via `onWarn`, never throws. Asserts (a) every inner
  `BindingRef[]` in `indexes.bindings` is `Object.isFrozen`, and
  (b) every inner array in `indexes.bindingAugmentations` is NOT
  frozen.
* Wired into `pipeline/run.ts` after both
  `populateNamespaceSiblings` and `propagateImportedReturnTypes`,
  before `resolveReferenceSites`. One sweep covers the full
  post-finalize surface.

Tests:
* `validate-bindings-immutability.test.ts` — 6 cases pinning happy
  path, both drift directions, multi-violation accumulation, and
  both production no-op gates.

All scope-resolution + csharp resolver tests green (242/242 in the
focused run; matches the wider Step 4 baseline).

Made-with: Cursor
@vercel

vercel Bot commented Apr 26, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
gitnexus Ready Ready Preview, Comment Apr 26, 2026 10:41am

Request Review

@github-actions

github-actions Bot commented Apr 26, 2026

Copy link
Copy Markdown
Contributor

CI Report

All checks passed

Pipeline Status

Stage Status Details
✅ Typecheck success tsc --noEmit
✅ Tests success unit tests, 3 platforms
✅ E2E success gitnexus-web changes only

Test Results

Tests Passed Failed Skipped Duration
7566 7469 0 97 281s

✅ All 7469 tests passed

97 test(s) skipped — expand for details
  • Swift MethodExtractor > isTypeDeclaration > recognizes class_declaration
  • Swift MethodExtractor > isTypeDeclaration > recognizes protocol_declaration
  • Swift MethodExtractor > isTypeDeclaration > rejects import_declaration
  • Swift MethodExtractor > visibility > extracts public method
  • Swift MethodExtractor > visibility > extracts private method
  • Swift MethodExtractor > visibility > defaults to internal when no modifier
  • Swift MethodExtractor > protocol methods > marks protocol method as abstract
  • Swift MethodExtractor > static and class methods > detects static func as isStatic
  • Swift MethodExtractor > static and class methods > detects class func as isStatic
  • Swift MethodExtractor > parameters > extracts parameters with types and default values
  • Swift MethodExtractor > return type > extracts return type from -> annotation
  • Swift MethodExtractor > annotations > extracts @objc attribute
  • Swift MethodExtractor > isFinal > detects final func
  • Swift MethodExtractor > isFinal > is false when not final
  • Swift MethodExtractor > isAsync > detects async func
  • Swift MethodExtractor > isOverride > detects override method
  • buildTypeEnv > constructor inference (Tier 1 fallback) > lookupClassByName regression coverage > Swift lookupClassByName regression coverage > Swift cross-file constructor inference uses lookupClassByName
  • buildTypeEnv > constructor inference (Tier 1 fallback) > lookupClassByName regression coverage > Swift lookupClassByName regression coverage > Swift explicit init inference uses lookupClassByName
  • buildTypeEnv > constructor inference (Tier 1 fallback) > lookupClassByName regression coverage > Swift lookupClassByName regression coverage > Swift cross-file constructor inference does not bind plain functions
  • buildTypeEnv > known limitations (documented skip tests) > Ruby block parameter: users.each { |user| } — closure param inference, different feature
  • Swift constructor-inferred type resolution > detects User and Repo classes, both with save methods
  • Swift constructor-inferred type resolution > resolves user.save() to Models/User.swift via constructor-inferred type
  • Swift constructor-inferred type resolution > resolves repo.save() to Models/Repo.swift via constructor-inferred type
  • Swift constructor-inferred type resolution > emits exactly 2 save() CALLS edges (one per receiver type)
  • Swift self resolution > detects User and Repo classes, each with a save function
  • Swift self resolution > resolves self.save() inside User.process to User.save, not Repo.save
  • Swift parent resolution > detects BaseModel and User classes plus Serializable protocol
  • Swift parent resolution > emits EXTENDS edge: User → BaseModel
  • Swift parent resolution > emits IMPLEMENTS edge: User → Serializable (protocol conformance)
  • Swift cross-file User.init() inference > resolves user.save() via User.init(name:) inference
  • Swift cross-file User.init() inference > resolves user.greet() via User.init(name:) inference
  • Swift return type inference > detects User class and getUser function
  • Swift return type inference > detects save function on User (Swift class methods are Function nodes)
  • Swift return type inference > resolves user.save() to User#save via return type of getUser() -> User
  • Swift return-type inference via function return type > resolves user.save() to User#save via return type of getUser()
  • Swift return-type inference via function return type > user.save() does NOT resolve to Repo#save
  • Swift return-type inference via function return type > resolves repo.save() to Repo#save via return type of getRepo()
  • Swift implicit imports (cross-file visibility) > detects UserService class in Models.swift
  • Swift implicit imports (cross-file visibility) > resolves UserService() constructor call across files (no explicit import)
  • Swift implicit imports (cross-file visibility) > resolves service.fetchUser() member call across files
  • Swift implicit imports (cross-file visibility) > creates IMPORTS edges between files in the same module
  • Swift extension deduplication > detects Product class
  • Swift extension deduplication > resolves Product() constructor despite extension creating duplicate class node
  • Swift extension deduplication > resolves product.save() to Product.swift (primary definition)
  • Swift constructor call fallback (no new keyword) > resolves OCRService() as constructor call across files
  • Swift constructor call fallback (no new keyword) > resolves ocr.recognize() member call via constructor-inferred type
  • Swift export visibility (internal vs private) > resolves PublicService() constructor across files
  • Swift export visibility (internal vs private) > resolves internalHelper() across files (internal = module-scoped)
  • Swift if let / guard let binding resolution > detects User and Repo classes
  • Swift if let / guard let binding resolution > resolves user.save() inside if-let to User#save
  • Swift if let / guard let binding resolution > resolves repo.save() inside guard-let to Repo#save
  • Swift if let / guard let binding resolution > user.save() in if-let does NOT resolve to Repo#save
  • Swift await / try expression unwrapping > resolves user.save() via await fetchUser() return type
  • Swift await / try expression unwrapping > resolves repo.save() via try parseRepo() return type
  • Swift await / try expression unwrapping > detects fetchUser and parseRepo as functions
  • Swift for-in loop element type inference > detects User and Repo classes
  • Swift for-in loop element type inference > creates implicit import edges between files
  • Swift field-type resolution > detects classes and their properties
  • Swift field-type resolution > emits HAS_PROPERTY edges from class to field
  • Swift field-type resolution > resolves field-chain call user.address.save() → Address#save
  • Swift field-type resolution > emits ACCESSES edges for field reads in chains
  • Swift field-type resolution > populates field metadata (visibility, declaredType) on Property nodes
  • Swift call-result binding > resolves call-result-bound method call user.save() → User#save
  • Swift call-result binding > getUser() is present as a defined function
  • Swift call-result binding > emits processUser -> getUser CALLS edge for let-assigned free function call
  • Swift method enrichment > detects Animal protocol and Dog class
  • Swift method enrichment > emits IMPLEMENTS edge Dog -> Animal
  • Swift method enrichment > emits HAS_METHOD edges for Dog methods
  • Swift method enrichment > marks protocol Animal.speak as isAbstract
  • Swift method enrichment > marks Dog.speak as NOT isAbstract
  • Swift method enrichment > marks breathe as isFinal
  • Swift method enrichment > marks classify as isStatic
  • Swift method enrichment > captures @objc annotation on breathe
  • Swift method enrichment > populates parameterTypes for classify(_ name: String)
  • Swift method enrichment > records parameterCount for classify
  • Swift method enrichment > records returnType for speak
  • Swift method enrichment > resolves dog.speak() CALLS edge
  • Swift method enrichment > resolves Dog.classify("dog") CALLS edge
  • Swift abstract dispatch > detects Repository protocol and SqlRepository class
  • Swift abstract dispatch > emits IMPLEMENTS edge SqlRepository -> Repository
  • Swift abstract dispatch > emits HAS_METHOD edges for Repository.find and Repository.save
  • Swift abstract dispatch > emits HAS_METHOD edges for SqlRepository.find and SqlRepository.save
  • Swift abstract dispatch > marks base Repository.find as isAbstract
  • Swift abstract dispatch > marks base Repository.save as isAbstract
  • Swift abstract dispatch > marks concrete SqlRepository.find as NOT isAbstract
  • Swift abstract dispatch > resolves repo.find(id: 42) CALLS edge
  • Swift abstract dispatch > resolves repo.save(entity: user) CALLS edge
  • Swift abstract dispatch > populates parameterTypes for Repository.find
  • Swift abstract dispatch > populates parameterTypes for Repository.save
  • Swift abstract dispatch > records returnType for SqlRepository.find
  • Swift abstract dispatch > emits METHOD_IMPLEMENTS edges from SqlRepository methods → Repository protocol methods
  • Swift overloaded method disambiguation > detects 2 distinct find Method nodes on SqlRepository
  • Swift overloaded method disambiguation > emits METHOD_IMPLEMENTS edges for both find overloads
  • Swift overloaded method disambiguation > emits METHOD_IMPLEMENTS edge for save
  • Swift overloaded method disambiguation > emits exactly 3 METHOD_IMPLEMENTS edges total
  • Swift Child extends Parent — inherited method resolution (SM-9) > detects Parent and Child classes
  • Swift Child extends Parent — inherited method resolution (SM-9) > resolves c.parentMethod() to Parent.parentMethod via first-wins MRO walk

Code Coverage

Tests

Metric Coverage Covered Base Delta Status
Statements 75.59% 21951/29037 75.53% 📈 +0.1 🟢 ███████████████░░░░░
Branches 64.53% 14060/21787 64.45% 📈 +0.1 🟢 ████████████░░░░░░░░
Functions 80.8% 2126/2631 80.76% 📈 +0.0 🟢 ████████████████░░░░
Lines 78.4% 19807/25264 78.35% 📈 +0.1 🟢 ███████████████░░░░░

📋 View full run · Generated by CI

Tree-sitter buffer sizing is byte-based, so computing adaptive buffers from JavaScript string length under-sized UTF-8-heavy files. Make getTreeSitterBufferSize accept source text directly and compute Buffer.byteLength internally, then update all parse call sites and max-buffer skip checks to use byte length.

Add multibyte cache-miss and cap regressions for C#, Python, TypeScript, and the C# namespace-sibling fallback parse path.

Made-with: Cursor
@magyargergo magyargergo changed the title fix(csharp): two-channel binding lifecycle (closes #1066) + scope-resolution I8 hardening fix(ingestion): two-channel binding lifecycle (closes #1066) + scope-resolution I8 hardening Apr 26, 2026
Add focused unit coverage for augmented-only binding reads across the routed walker helpers and imported-return-type propagation path. Clarify I8 wording around lexical Scope.bindings versus post-finalize index channels, and document the intentional local-only behavior of findExportedDef.

Also switch the immutability validator tests to Vitest env stubs, document one intentional validator blind spot, and split C# namespace-sibling tests so UTF-8 parsing and augmentation-channel behavior are asserted independently.

Made-with: Cursor
Replace high-cardinality large-file capture fixtures with large padding plus a trailing declaration. This still proves adaptive tree-sitter buffers parse beyond large ASCII and UTF-8-heavy input, without making query matching process thousands of declarations and risking timeouts.

Made-with: Cursor
… regressions

Add worker-mode resolver integration coverage mirroring the C# #1066 scenario for Python and TypeScript. Each test builds a temp fixture with large ASCII and UTF-8-heavy source padding, then asserts trailing declarations and call edges still resolve after scope-resolution cache-miss reparsing.

Made-with: Cursor
@github-actions

github-actions Bot commented Apr 26, 2026

Copy link
Copy Markdown
Contributor

Claude finished @magyargergo's task in 6m 8s —— View job


PR #1082 — Adversarial Production Readiness Review


Review bar for this PR

  • Issue Analysis failed: Phase 'scopeResolution' failed: Cannot add property 2, object is not extensible #1066 fixed through the real C# pipeline path, not only isolated helper behavior.
  • indexes.bindings inner arrays remain frozen post-finalize; no post-finalize hook mutates them.
  • Every reader that must see augmentations uses lookupBindingsAt / namesAtScope; stale direct reads from indexes.bindings where augmentations are required are absent.
  • I8 contract is consistent across gitnexus-shared types, scope-resolver.ts docs, and runtime.
  • validateBindingsImmutability is a true no-op in production, gates correctly in dev, and runs after all post-finalize hooks.
  • getTreeSitterBufferSize uses UTF-8 byte length at every call site; large-file skip threshold is consistent.
  • C#, Python, and TypeScript capture behavior is unchanged except for the intended buffer-size fix.
  • Tests prove both crash modes and prove architectural necessity via bidirectional reversal.

Understanding of the change

Issue #1066 produced two crashes in C# scope resolution:

  1. Invalid argument (tree-sitter)emitCsharpScopeCaptures and extractFileStructure invoked parser.parse() on cache miss using the old TREE_SITTER_BUFFER_SIZE constant (512 KB), which is a fixed minimum regardless of file size. For files >32 KB the old constant worked by coincidence; files >256 KB caused buffer underflow.

  2. Cannot add property N, object is not extensiblepopulateCsharpNamespaceSiblings called push() on inner BindingRef[] arrays inside indexes.bindings. Those arrays are deep-frozen by materializeBindings during finalize. The original workaround (cloneBindingBucket) copied them before pushing, which contradicted I8 — it mutated the finalized channel with an unfrozen replacement.

The two-channel lifecycle fixes problem 2 architecturally:

  • indexes.bindings is now the permanent immutable finalize-output channel (inner arrays frozen by materializeBindings, never touched post-finalize).
  • indexes.bindingAugmentations is a new append-only mutable channel for post-finalize hooks.
  • All readers that need cross-file visibility call lookupBindingsAt / namesAtScope, which merge both channels and dedup by def.nodeId.
  • populateCsharpNamespaceSiblings writes exclusively to bindingAugmentations via a deliberate as Map<> cast, which is the only sanctioned mutation point.
  • validateBindingsImmutability enforces the lifecycle in dev mode.

Runtime paths affected: runScopeResolution (C#), propagateImportedReturnTypes, all scope-chain walkers (findClassBindingInScope, findCallableBindingInScope, findExportedDefByName). Python and TypeScript are affected only by the buffer-size fix (they have no populateNamespaceSiblings).

Shared contracts affected: gitnexus-shared/src/scope-resolution/types.ts (lifecycle comment addition — no type changes). gitnexus/src/core/ingestion/model/scope-resolution-indexes.ts (new bindingAugmentations field). scope-resolver.ts invariant I8 rewritten.

What must remain unchanged: C# cross-namespace visibility, using static member exposure, constructor/class lookup, interface dispatch. Python and TypeScript scope capture output (changes are cache-miss buffer sizing only).


Findings

[low] Stale regression test comment describes the removed workaround, not the final fix

  • Category: Documentation / Test correctness
  • File: gitnexus/test/integration/resolvers/csharp.test.ts:2432
  • Issue: The comment above the issue #1066 regression describe block says: "populateCsharpNamespaceSiblings must clone frozen finalize-produced BindingRef[] arrays before pushing". This describes the removed cloneBindingBucket workaround (commit 1), not the final architecture. The correct statement is that the hook must write to bindingAugmentations instead of bindings. The test code is correct; only the comment is stale.
  • Why it matters here: Future contributors reading this comment will misunderstand the I8 contract and might implement a new language hook using the clone pattern rather than the augmentation channel.
  • Recommended fix: Update the comment to state the architectural invariant: "populateCsharpNamespaceSiblings must write cross-file bindings to indexes.bindingAugmentations, not to the frozen indexes.bindings channel (I8)." Fix this →

[low] validateBindingsImmutability has a documented blind spot for frozen-replacement in indexes.bindings

  • Category: Validator coverage gap (acknowledged)
  • File: gitnexus/src/core/ingestion/scope-resolution/pipeline/validate-bindings-immutability.ts
  • Issue: The validator checks Object.isFrozen(bucket) for each inner array in indexes.bindings. It cannot detect a hook that replaces a frozen bucket with a different frozen array via (indexes.bindings.get(s) as Map).set('name', Object.freeze([wrongContent])). The test at line 86–98 explicitly acknowledges this as a known limitation: "does not detect semantically wrong frozen replacements in indexes.bindings".
  • Why it matters here: In practice this failure mode requires a hook to (1) cast away ReadonlyMap, (2) write a frozen replacement, and (3) somehow have correct-looking content. Extremely unlikely as an accidental regression.
  • Recommended fix: No change required unless future hooks emerge that produce this shape. Acceptable as a documented limitation given the I8 invariant is clearly enforceable and the test explicitly pins the gap.

[low] Python and TypeScript large-file cache-miss path is not exercised by an integration fixture

  • Category: Test coverage
  • Files: gitnexus/src/core/ingestion/languages/python/captures.ts, gitnexus/src/core/ingestion/languages/typescript/captures.ts
  • Issue: The getTreeSitterBufferSize change in all three captures files is unit-tested thoroughly (including multibyte UTF-8 at ingestion-utils.test.ts:674–685). The C# path is validated end-to-end via a real integration fixture that forces worker-pool (cache-miss) re-parses. Python and TypeScript have no equivalent large-file integration fixture validating the cache-miss re-parse path. The unit-level captures tests (python-fixtures.test.ts +14, typescript-captures.test.ts +18) don't exercise the worker-pool path.
  • Why it matters here: The buffer fix in Python/TypeScript is the same function call and is unlikely to have regressed, but a large-file cache-miss Python/TypeScript file was the trigger for the original crash in the issue. No DoD §2.7 evidence of exercise on the real Python/TypeScript path.
  • Recommended fix: This is non-blocking given the function is language-agnostic and the unit tests cover multibyte behavior. Optional follow-up: add a Python or TypeScript integration fixture with a padded source file (≥256 KB) exercising the worker-pool re-parse path.

Definition of Done check

Item Status
Issue #1066 fixed through real C# ingestion/scope-resolution path (not just isolated helper) satisfied — integration test forces worker-pool (cache-miss) at csharp-large-cache-miss-resolution
Finalized indexes.bindings not mutated post-finalize; cloneBindingBucket fully removed satisfied — grep confirms zero remaining references; C# writes exclusively to bindingAugmentations
Every reader requiring augmented bindings uses lookupBindingsAt / namesAtScope satisfied — all four readers confirmed; findExportedDef's intentional bypass is correctly scoped to target-side local-export queries and documented
I8 contract is clear, enforceable, consistent across shared types, contract docs, and runtime satisfiedtypes.ts, scope-resolution-indexes.ts, scope-resolver.ts I8 section all consistent
validateBindingsImmutability is production no-op, dev-mode meaningful, correct placement satisfied — gated on process.env.NODE_ENV !== 'production' + VALIDATE_SEMANTIC_MODEL !== '0'; runs after all post-finalize hooks, before resolveReferenceSites
Adaptive tree-sitter buffer uses UTF-8 byte length; all call sites correct satisfiedBuffer.byteLength(sourceText, 'utf8'); confirmed in C#, Python, TypeScript captures, parse-worker, sequential parser, call/import/heritage processors
C#, Python, TypeScript capture behavior preserved (buffer-size fix only) satisfied — no other logic changes in captures; bidirectional reversal confirms architectural pieces carry load
Tests prove both crash modes and architectural necessity satisfied — 4 C# regression integration tests + 8 walkers-augmentations unit tests + 6 validate-bindings-immutability unit tests + bidirectional reversal documented in PR

Validation assessment

Tests added/updated:

  • walkers-augmentations.test.ts — 8 tests covering lookupBindingsAt precedence, dedup, empty fast-path, shared empty array identity, and walker helpers via augmentations
  • validate-bindings-immutability.test.ts — 6 tests covering happy path, mutable bindings violation, frozen augmentations violation, multi-scope counting, production no-op, VALIDATE_SEMANTIC_MODEL=0 no-op
  • csharp.test.ts — 4 integration tests for issue Analysis failed: Phase 'scopeResolution' failed: Cannot add property 2, object is not extensible #1066 exercising the worker-pool path
  • ingestion-utils.test.ts — 9 buffer-size tests covering ASCII, multibyte CJK, boundaries, exact 32 MB cap
  • csharp-hooks.test.ts, csharp-captures.test.ts, python-fixtures.test.ts, typescript-captures.test.ts, imported-return-types.test.ts — targeted unit coverage additions

Validation claimed by PR:

Validation verified from diff/CI:

  • CI shows 7462/7559 tests pass, 0 failures, 97 skipped — consistent with PR description. The 97 skips are exclusively Swift tests (confirmed by CI detail). The local/CI count discrepancy is explained by newly added tests and Swift skips counted differently locally.
  • tsc --noEmit confirmed by CI Typecheck: success.
  • Test count delta (7559 CI vs 7327 local) is accounted for by the ~18 new tests added in commits 2–6 and the 97 Swift tests being skipped on CI vs failing locally.
  • The bidirectional reversal (step 7 in the PR plan) is documented with specific error messages — convincing evidence.

Missing/not-verifiable validation:

  • gitnexus-shared/ build correctness: types.ts change is purely additive documentation (no new exports, no structural changes). CI confirms typecheck passes, which validates the shared package is consistent. The DoD §4.4 npm run build in gitnexus-shared/ is not explicitly called out, but the CI setup-gitnexus action handles build ordering per .github/ config. Not blocking.
  • Python/TypeScript large-file cache-miss integration coverage is absent (see finding above). Not blocking.

Final verdict

production-ready

The two-channel binding lifecycle is architecturally correct and fully wired: indexes.bindings is permanently immutable post-finalize, indexes.bindingAugmentations is the only sanctioned mutation channel, and every reader that needs augmented visibility routes through lookupBindingsAt. Reader migration is complete — confirmed by static analysis (zero stale .bindings.get() calls in ingestion code) and by unit tests proving walker helpers see augmentation-only refs. The findExportedDef bypass is correctly scoped and documented. The tree-sitter buffer fix uses Buffer.byteLength at all call sites. The validateBindingsImmutability guard is correctly gated and correctly positioned in the pipeline. CI is clean with zero failures; all 97 skips are pre-existing Swift tests unrelated to this change. Three findings are identified, all LOW severity, none blocking: a stale test comment, a documented validator blind spot, and absent Python/TypeScript large-file integration coverage. The first is a cosmetic fix that can follow in a quick cleanup; the other two are acknowledged design tradeoffs.


| PR branch

Addresses SPARC reviewer feedback on the binding-augmentation channel:

- Validator gate is now opt-in outside development. Extract
  isSemanticModelValidatorEnabled() in utils/env.ts as the single
  predicate; both validateBindingsImmutability and phase.ts's warn
  handler share it. Default CLI runs no longer pay the O(binding-buckets)
  scan, and explicit VALIDATE_SEMANTIC_MODEL=1 now emits warnings even
  when NODE_ENV is unset.
- namesAtScope returns Iterable<string> and zero-allocates when at most
  one channel is populated (returns Map.keys() directly), only
  materializing a Set when both channels carry names. The caller-side
  branching and EMPTY_NAMES escape hatch in propagateImportedReturnTypes
  are gone -- both helpers handle the empty-augmentation case internally.
- C# namespace-siblings header/JSDoc, model JSDoc, I8 contract prose, and
  the #1066 integration-test header rewritten to say post-finalize fanout
  appends only to bindingAugmentations; finalized refs come first and win
  duplicate def.nodeId metadata; local lexical Scope.bindings remains the
  first-tier shadowing channel.

Validator unit-test setup deduplicated via beforeEach and extended with
default-CLI no-op + explicit-opt-in cases.

Made-with: Cursor
@magyargergo magyargergo merged commit 98ee665 into main Apr 26, 2026
20 checks passed
@magyargergo magyargergo deleted the fix/csharp-bindings-augmentation-channel branch April 26, 2026 11:16
magyargergo pushed a commit that referenced this pull request Apr 27, 2026
…1085)

The csharp-large-cache-miss-resolution fixture added in #1082 reproduces
the freeze contract failure via tree-sitter cache-miss reparse on >32 KB
files. This adds a complementary trigger for the same root cause that
does not depend on file size: a small-file pair where the importer
locally declares a class with the same simple name as a sibling reached
through `using`.

Pre-#1082 path: scope-extractor pre-populates (and freezes) `User` in
the importer's Module bindings, then populateCsharpNamespaceSiblings'
namespace-import loop calls push() on the frozen array and throws
"Cannot add property N, object is not extensible", aborting the whole
scopeResolution phase.

Post-#1082 the augmentation channel keeps both bindings visible; the
local `Collision.App.User` shadows the namespace-imported one per
origin precedence, so `Program.Run -> new User()` resolves to the
local class.

Three assertions:
- scopeResolution completes (no throw on the colliding bucket).
- both `User` declarations are detected across the two namespaces.
- `Program.Run -> User` constructor edge points at App/Program.cs
  (not Models/User.cs), verifying origin:local shadows origin:namespace.

Verified: full csharp.test.ts suite green (207/207). tsc --noEmit clean.

Refs: #1066, #1082, #1083 (closed as superseded).
mann1x added a commit to mann1x/GitNexus that referenced this pull request Apr 27, 2026
…l scopes (closes abhigyanpatwari#1086)

When a C# file consists of a single top-level `namespace_declaration` that
ends exactly at EOF (no trailing newline, no leading content outside the
namespace's `{}` body), tree-sitter-c-sharp 0.23.1 reports identical byte
ranges for `compilation_unit` and `namespace_declaration`. Pre-fix the
scope-extractor parent-finder relied on strict containment, so the Module
was popped off the stack and the Namespace ended up with `parent === null`
→ `ScopeTreeInvariantError: non-module-requires-parent` →
`extractParsedFile` swallowed the throw and the whole file was dropped
from the registry-primary path. Cross-file IMPORTS / CALLS edges
originating in or terminating at that file vanished.

Hit on three real-world `*.Designer.cs` files in PersistentWindows
(`HotKeyWindow.Designer.cs`, `LaunchProcess.Designer.cs`,
`DbKeySelect.Designer.cs`) — all have the byte signature
`<BOM><CRLF>namespace ... { ... }<EOF>` (last hex = `... 7D 0D 0A 7D`).

The fix is a single carve-out in the parent-validity contract: a `Module`
may parent a same-range non-`Module` child. The relationship stays
acyclic because the carve-out is direction-asymmetric — only Module-as-
outer parents a same-range non-Module, never the reverse.

Two coordinated changes:

* `gitnexus/src/core/ingestion/scope-extractor.ts` — `pass1BuildScopes`
  now consults a new `canParentScope` helper instead of
  `rangeStrictlyContains` directly. Sort tie-breaker added so a same-
  range Module always sorts before a non-Module candidate, ensuring the
  Module lands on the parent-stack first regardless of tree-sitter
  capture iteration order.

* `gitnexus-shared/src/scope-resolution/scope-tree.ts` — `buildScopeTree`'s
  `parent-must-contain-child` check now uses the same `canParentScope`
  carve-out so the validator agrees with the extractor on what a
  well-formed parent edge looks like. Error message updated to spell
  out the new contract.

`rangeStrictlyContains` keeps its strict semantics in both files —
position-index lookups, hook-side range comparisons, and other call
sites are unchanged.

* `gitnexus/test/fixtures/lang-resolution/csharp-namespace-as-root-no-trailing-newline/`
  — minimal regression fixture mirroring the PersistentWindows shape:
  both `Models/User.cs` and `App/Program.cs` end exactly on the closing
  `}` of their namespace with no trailing newline. The trigger is shape-
  driven, not size-driven, so the fixture stays small (~250 bytes total).
* New `csharp.test.ts` describe block: scope extraction completes for
  both files, and the cross-file `IMPORTS` edge resolves through the
  scope-resolution path with `reason: 'csharp-scope: using'`.
* `scope-tree.test.ts`: replaced the prior "rejects child ranges
  identical to the parent" case with three new ones — non-Module parent
  still rejected at equal range; Module-as-parent of a same-range non-
  Module accepted (the abhigyanpatwari#1086 carve-out); Module-as-parent of another
  Module still rejected (the asymmetry guard).

* `npx vitest run test/unit/scope-resolution test/integration/resolvers`
  → 2514 passed / 77 skipped / 0 failed (52 test files).
* `npx tsc --noEmit` clean in both `gitnexus/` and `gitnexus-shared/`.
* End-to-end on PersistentWindows (after rebuilding the Docker image
  with this branch): 3 prior `scope extraction failed for *.Designer.cs`
  warnings → 0. Pre-fix index numbers will be re-checked here once the
  branch is built and indexed; the existing post-abhigyanpatwari#1082 baseline is
  1113 nodes / 2987 edges / 39 clusters / 97 flows.

`canParentScope` is language-agnostic. Other languages whose query emits
`(compilation_unit) @scope.module` plus a single same-range top-level
scope can naturally hit the same byte shape on minimal files; this fix
applies to all of them uniformly.

Refs: abhigyanpatwari#1086 (issue with full root-cause analysis + 4-case empirical
repro through `extractParsedFile`).
magyargergo pushed a commit that referenced this pull request Apr 27, 2026
…l scopes (closes #1086) (#1087)

* fix(scope-resolution): allow same-range Module-as-parent for top-level scopes (closes #1086)

When a C# file consists of a single top-level `namespace_declaration` that
ends exactly at EOF (no trailing newline, no leading content outside the
namespace's `{}` body), tree-sitter-c-sharp 0.23.1 reports identical byte
ranges for `compilation_unit` and `namespace_declaration`. Pre-fix the
scope-extractor parent-finder relied on strict containment, so the Module
was popped off the stack and the Namespace ended up with `parent === null`
→ `ScopeTreeInvariantError: non-module-requires-parent` →
`extractParsedFile` swallowed the throw and the whole file was dropped
from the registry-primary path. Cross-file IMPORTS / CALLS edges
originating in or terminating at that file vanished.

Hit on three real-world `*.Designer.cs` files in PersistentWindows
(`HotKeyWindow.Designer.cs`, `LaunchProcess.Designer.cs`,
`DbKeySelect.Designer.cs`) — all have the byte signature
`<BOM><CRLF>namespace ... { ... }<EOF>` (last hex = `... 7D 0D 0A 7D`).

The fix is a single carve-out in the parent-validity contract: a `Module`
may parent a same-range non-`Module` child. The relationship stays
acyclic because the carve-out is direction-asymmetric — only Module-as-
outer parents a same-range non-Module, never the reverse.

Two coordinated changes:

* `gitnexus/src/core/ingestion/scope-extractor.ts` — `pass1BuildScopes`
  now consults a new `canParentScope` helper instead of
  `rangeStrictlyContains` directly. Sort tie-breaker added so a same-
  range Module always sorts before a non-Module candidate, ensuring the
  Module lands on the parent-stack first regardless of tree-sitter
  capture iteration order.

* `gitnexus-shared/src/scope-resolution/scope-tree.ts` — `buildScopeTree`'s
  `parent-must-contain-child` check now uses the same `canParentScope`
  carve-out so the validator agrees with the extractor on what a
  well-formed parent edge looks like. Error message updated to spell
  out the new contract.

`rangeStrictlyContains` keeps its strict semantics in both files —
position-index lookups, hook-side range comparisons, and other call
sites are unchanged.

* `gitnexus/test/fixtures/lang-resolution/csharp-namespace-as-root-no-trailing-newline/`
  — minimal regression fixture mirroring the PersistentWindows shape:
  both `Models/User.cs` and `App/Program.cs` end exactly on the closing
  `}` of their namespace with no trailing newline. The trigger is shape-
  driven, not size-driven, so the fixture stays small (~250 bytes total).
* New `csharp.test.ts` describe block: scope extraction completes for
  both files, and the cross-file `IMPORTS` edge resolves through the
  scope-resolution path with `reason: 'csharp-scope: using'`.
* `scope-tree.test.ts`: replaced the prior "rejects child ranges
  identical to the parent" case with three new ones — non-Module parent
  still rejected at equal range; Module-as-parent of a same-range non-
  Module accepted (the #1086 carve-out); Module-as-parent of another
  Module still rejected (the asymmetry guard).

* `npx vitest run test/unit/scope-resolution test/integration/resolvers`
  → 2514 passed / 77 skipped / 0 failed (52 test files).
* `npx tsc --noEmit` clean in both `gitnexus/` and `gitnexus-shared/`.
* End-to-end on PersistentWindows (after rebuilding the Docker image
  with this branch): 3 prior `scope extraction failed for *.Designer.cs`
  warnings → 0. Pre-fix index numbers will be re-checked here once the
  branch is built and indexed; the existing post-#1082 baseline is
  1113 nodes / 2987 edges / 39 clusters / 97 flows.

`canParentScope` is language-agnostic. Other languages whose query emits
`(compilation_unit) @scope.module` plus a single same-range top-level
scope can naturally hit the same byte shape on minimal files; this fix
applies to all of them uniformly.

Refs: #1086 (issue with full root-cause analysis + 4-case empirical
repro through `extractParsedFile`).

* refactor(scope-resolution): export canParentScope from gitnexus-shared

Addresses #1087 review (medium): the helper was previously duplicated
byte-for-byte in `scope-extractor.ts` and `scope-tree.ts`. Per DoD
"single source of truth in shared", the contract piece belongs in
gitnexus-shared (Ring 2 SHARED #912) and the consuming layer should
import it. Eliminates the silent-drift surface where a future edit
to one copy would produce extractor/validator disagreement on what
a well-formed parent edge looks like.

Changes:
- gitnexus-shared/src/scope-resolution/scope-tree.ts: add `export`
  to `canParentScope`.
- gitnexus-shared/src/index.ts: re-export `canParentScope`.
- gitnexus/src/core/ingestion/scope-extractor.ts: remove the local
  `canParentScope` definition (and its now-unused local copy of
  `rangeStrictlyContains`), import from `gitnexus-shared`. The local
  `rangesEqual` stays — it's still used in capture-anchor logic at
  two unrelated sites.

Validation (per DoD §4.4 — both CLI and web consumers verified):
- npx tsc --noEmit clean in gitnexus/ and gitnexus-shared/
- cd gitnexus-web && npx tsc -b --noEmit clean
- gitnexus-shared `npm run build` clean
- Targeted: vitest run test/unit/scope-resolution test/integration/resolvers
  → 2522 passed / 0 failed / 77 skipped (54 files)
- Full suite: vitest run → 7238 passed / 1 failed / 97 skipped.
  The single failure is `test/unit/ignore-service.test.ts > warns
  on EACCES but does not throw`, which cannot run when uid=0 (root
  bypasses POSIX permission checks). Pre-existing on this branch
  before the refactor; unrelated to scope-resolution.
@magyargergo magyargergo mentioned this pull request May 10, 2026
4 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant