fix(scope-resolution): allow same-range Module-as-parent for top-level scopes (closes #1086)#1087
Conversation
|
@mann1x is attempting to deploy a commit to the NexusCore Team on Vercel. A member of the Team first needs to authorize it. |
|
Nice! Thanks for this PR! 🎉 |
|
Claude finished @magyargergo's task in 4m 39s —— View job PR #1087 — Production Readiness ReviewTasks:
Review bar for this PR
Understanding of the changeIssue #1086: The carve-out: A new helper Runtime paths affected: What must remain unchanged: Findings[medium]
|
| Item | Status |
|---|---|
| #1086 fixed through the real scope-extraction / scope-resolution path (not only ScopeTree isolation) | satisfied — integration test pins reason === 'csharp-scope: using', proving the scope-resolution path ran |
| Carve-out is narrow: only Module-as-outer at equal range; all other equal-range pairs rejected | satisfied — canParentScope condition is outerKind === 'Module' && innerKind !== 'Module' && rangesEqual(...) |
| Extractor and validator agree on parent-validity semantics | satisfied (currently) — both helpers are byte-for-byte identical; the drift risk is documented under Findings |
rangeStrictlyContains keeps strict semantics for all other callers |
satisfied — rangeStrictlyContains is unchanged and used only inside canParentScope in both files |
| Sort tie-breaker is deterministic and ensures Module sorts before same-range non-Module | satisfied — sort explicitly puts a.kind === 'Module' first (return -1) after all positional keys tie |
| Change is language-agnostic without creating invalid scope trees for non-C# languages | satisfied — carve-out applies uniformly to any Module/non-Module equal-range pair; malformed multi-same-range-sibling cases still hit the sibling-overlap guard |
| Tests cover regression, carve-out, rejected equal-range cases, and real C# resolver path | satisfied — three new scope-tree.test.ts cases + integration test with reason pin |
| Validation sufficient for a shared scope-resolution contract change | partially satisfied — gitnexus/ and gitnexus-shared/ validated; gitnexus-web/ typecheck gap per DoD §4.4; CI pending |
Validation assessment
Tests added / updated:
scope-tree.test.ts: three new cases — non-Module equal-range rejection (Namespace→Class), Module→Namespace equal-range acceptance withgetParent/getChildrenround-trip, Module→Module equal-range rejection (asymmetry guard).csharp.test.ts: newdescribe('C# namespace-as-root with no trailing newline (issue #1086)')with twoitblocks: class detection proving extraction completed, andIMPORTSedge withreason === 'csharp-scope: using'proving the scope-resolution path.- Fixtures:
csharp-namespace-as-root-no-trailing-newline/with two source files. Read tool confirms both end on}at the last line with no trailing blank line (consistent with no trailing newline byte, as described by the PR's pre-committail -c 1guard).
Validation claimed by PR:
npx vitest run test/unit/scope-resolution test/integration/resolvers→ 2514 passed / 77 skipped / 0 failed (52 files)npx tsc --noEmitclean ingitnexus/andgitnexus-shared/- Pre-commit typecheck green
- Local end-to-end on PersistentWindows: 3 prior
scope extraction failed for *.Designer.cswarnings → 0
Validation verified from diff / CI evidence:
- Code structure (two
canParentScopehelpers, sort, test assertions) is consistent with the claimed test results. - CI: labeler and description checks passed; main
CIworkflow is in_progress — no CI green confirmation available.
Missing / not-verifiable validation:
gitnexus-web/typecheck per DoD §4.4 (not mentioned).npm run buildingitnexus-shared/(not mentioned; the shared dist is consumed by downstream typechecks).- CI results (pending).
Whether missing validation blocks production readiness:
The gitnexus-web/ gap is low actual risk (type signature of buildScopeTree unchanged, canParentScope is module-private). Pending CI is the more meaningful gating item — the PR should not merge until CI is green.
Final verdict
production-ready with minor follow-ups
The fix is semantically correct: canParentScope implements exactly the intended asymmetric carve-out, the sort tie-breaker is deterministic, and the extractor/validator currently agree byte-for-byte. The integration test's reason === 'csharp-scope: using' pin is a meaningful assertion that the scope-resolution path actually ran rather than a legacy fallback — this is the right way to prove the real bug surface is covered. The two medium findings are non-blocking: the gitnexus-web/ typecheck gap carries negligible actual risk given the unchanged type contract, and the helper duplication is documented and cross-referenced. The recommended follow-up is to export canParentScope from gitnexus-shared to eliminate the drift surface once the immediate bug is shipped. Merge should wait for CI to complete.
CI Report✅ All checks passed Pipeline Status
Test Results
✅ All 7478 tests passed 98 test(s) skipped — expand for details
Code CoverageTests
📋 View full run · Generated by CI |
|
It seems this bug wasn't covered in the legacy path either. We need to be able to define a set of tests that we expect to fail during legacy analysis in the parity tests. Could you please look into this? 🙏 your implementation is nice and i'll merge it in regardless, I just want to turn the CI green. |
|
I'm adding this feature now. |
…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`).
Addresses abhigyanpatwari#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 abhigyanpatwari#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.
110c817 to
d39482d
Compare
|
Thanks for the review @magyargergo / Claude — both medium findings addressed and the branch is rebased on the latest 1. Per the recommendation, the helper now lives only in Net diff in this commit ( 2. Type contract of 3. Rebased on upstream/main Branch was 4. Validation
The single failure is Final shape of the PR Two commits on top of
Happy to squash on merge if preferred. |
|
Correction on the prior comment's "1 failure": I shouldn't have dismissed it as environmental and moved on. It's a real test-stability bug — the Sent as a separate one-line PR (extends the existing PR #1087 itself remains pure scope-resolution + the dedup refactor; the test fix is unrelated and properly belongs on its own PR. |
|
Claude finished @magyargergo's task in 3m 35s —— View job PR #1087 — Production Readiness Review (Round 2)
|
| Item | Status |
|---|---|
| #1086 fixed through the real scope-extraction / scope-resolution path | satisfied — integration test pins reason === 'csharp-scope: using', proving scope-resolution ran; if extraction aborted on User.cs, the IMPORTS edge would be absent or carry a different reason |
Carve-out is narrow: only Module-as-outer at equal range; all other equal-range pairs rejected |
satisfied — canParentScope condition is outerKind === 'Module' && innerKind !== 'Module' && rangesEqual(outer, inner); Module→Module rejected; Namespace→Class rejected; verified in both canParentScope and unit tests |
| Extractor and validator agree on parent-validity semantics (single source of truth) | satisfied — canParentScope now lives exclusively in gitnexus-shared/scope-tree.ts and is imported by scope-extractor.ts; no duplicate definition; no drift surface |
rangeStrictlyContains keeps strict semantics for all other callers |
satisfied — rangeStrictlyContains is unexported, unchanged, and called only inside canParentScope in scope-tree.ts; all other range logic is unaffected |
| Sort tie-breaker is deterministic and ensures Module sorts before same-range non-Module | satisfied — sort keys: startLine ASC, startCol ASC, endLine DESC, endCol DESC, then Module before non-Module, then 0; deterministic for the intended case |
| Two same-range non-Module siblings under Module are still rejected | satisfied — rangesOverlap returns true for identical ranges; Pass 3 sibling check catches this; the carve-out cannot be exploited to admit overlapping siblings |
gitnexus-shared export is intentional, type-safe, does not expose unstable internals |
satisfied — canParentScope takes Range and Scope['kind'], both already in the public API; the export rationale (eliminate duplication) is documented in the PR and in the code comment |
| Tests cover regression, carve-out, rejected equal-range cases, and real C# resolver path | satisfied — three new scope-tree.test.ts cases (non-Module parent rejected, Module→Namespace accepted with round-trip, Module→Module rejected) plus integration test with reason pin |
| Validation sufficient for a shared scope-resolution contract change | satisfied — gitnexus-shared built, gitnexus typechecked, gitnexus-web typechecked (exit 0 per last comment + CI ci-quality.yml), full test suite 7478/0 in CI |
Validation assessment
Tests added / updated:
scope-tree.test.ts: three newinvariant violationscases — Namespace-as-parent of same-range Class rejected; Module-as-parent of same-range Namespace accepted withgetParent/getChildrenround-trip; Module-as-parent of same-range Module rejected (asymmetry guard).csharp.test.ts: newdescribe('C# namespace-as-root with no trailing newline (issue #1086)')— class detection proving extraction completed for both files; IMPORTS edge withreason === 'csharp-scope: using'proving the scope-resolution path ran.- Fixtures:
csharp-namespace-as-root-no-trailing-newline/Models/User.csand.../App/Program.cs.User.csconfirmed to end exactly on}with no trailing newline (7 lines, no blank last line).Program.cshas a leadingusingso itscompilation_unitandnamespace_declarationare NOT coincident — but this doesn't weaken the test (see Finding above).
Validation claimed by PR (round 2):
gitnexus-shared npm run build— cleangitnexus npx tsc --noEmit— cleangitnexus-web npx tsc -b --noEmit— clean (exit 0)vitest run test/unit/scope-resolution test/integration/resolvers— 2522 passed / 0 failed / 77 skipped (54 files)
Validation verified from CI evidence:
- CI run 24987210975: 7478 passed / 0 failed / 98 skipped across unit + integration + cross-platform. Typecheck stage:
success. E2E:success. - All coverage metrics stable vs base (no regression).
Missing / not-verifiable validation: None. All DoD §4.4 items are addressed in both the PR comments and CI.
Final verdict
production-ready
The round-2 state of this PR is correct. The single medium finding from the first review — canParentScope duplication creating a drift surface between extractor and validator — has been resolved cleanly: the helper now has exactly one definition in gitnexus-shared, is re-exported as a narrow, intentional public API, and is imported by scope-extractor.ts. The gitnexus-web typecheck gap from the first review is also closed. The canParentScope contract is direction-asymmetric by construction, the sort tie-breaker is deterministic, the sibling-overlap guard still catches multi-same-range-child pathology, and the integration test's reason === 'csharp-scope: using' pin is a meaningful signal that the real scope-resolution path ran. CI is fully green (7478/0). The one remaining low finding is a documentation imprecision in the fixture comment that does not affect correctness or test strength.
The Run Claude Code Review step passed an invalid PR ref (owner/repo/pull/N) which gh interprets as a branch name, causing early gh pr view failures. More importantly, the prompt omitted --comment, so the code-review plugin only displayed findings in terminal output and never invoked gh pr comment to post to the PR. Switch to a full PR URL and add --comment so the plugin posts the review during the session, which also routes around upstream bugs anthropics/claude-code-action#1061 and abhigyanpatwari#1087 where the action's post-step capture can silently drop output on issue_comment triggers.

Summary
Closes #1086 —
scopeResolutionaborts (silently, viaextractParsedFile's try/catch) for any C# file whosecompilation_unitand single top-levelnamespace_declarationhave identical tree-sitter ranges. The triggering shape is shape-driven, not size-driven: any file with no leading content outside the namespace block AND no trailing newline after the closing}produces coincident ranges. Hit on three real-world*.Designer.csfiles in PersistentWindows; the four-case empirical repro (2 fail, 2 ok) is in #1086.The fix is a single carve-out in the parent-validity contract: a
Modulemay parent a same-range non-Modulechild. The relationship stays acyclic because the carve-out is direction-asymmetric — only Module-as-outer parents a same-range non-Module, never the reverse.This is option (b) from the issue (named
canParentScopehelper at the contract layer). Option (a) (in-place tightening ofpass1BuildScopes's while-loop with a sort tie-breaker) was also feasible and slightly smaller, but option (b) keeps the carve-out as a single named, documented helper that lives in bothscope-extractor.tsandscope-tree.ts— the two places that previously duplicated the samerangeStrictlyContainsand that need to agree on what a well-formed parent edge looks like. Putting the carve-out behind a named helper makes the contract greppable and the asymmetry guard impossible to miss.Changes
gitnexus/src/core/ingestion/scope-extractor.ts— newcanParentScope(outer, inner, outerKind, innerKind)helper next torangeStrictlyContains.pass1BuildScopesconsults it for parent-finding. Sort tie-breaker added so a same-range Module 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— mirrors the helper.buildScopeTree'sparent-must-contain-childcheck now usescanParentScopeso the validator agrees with the extractor. Error message updated to spell out the new contract: "… does not contain child '...' (allowed: strict containment, or equal-range Module-as-parent)".rangeStrictlyContainskeeps its strict semantics in both files — position-index lookups, hook-side range comparisons, and other call sites are unchanged.Tests
test/fixtures/lang-resolution/csharp-namespace-as-root-no-trailing-newline/— bothModels/User.csandApp/Program.csend exactly on the closing}of their namespace with no trailing newline. ~250 bytes total. Each commit's pre-commit hook verifies the trigger byte (tail -c 1is0x7D).csharp.test.tsdescribe — scope extraction completes for both files (the bug surface), and the cross-fileIMPORTSedge resolves through the scope-resolution path withreason: 'csharp-scope: using'. Thecsharp-scope:reason prefix is the signal that scope-resolution drove the resolution rather than the legacy DAG fallback.scope-tree.test.ts— replaced the prior "rejects child ranges identical to the parent" case with three new ones:getParent/getChildrenround-trip).Validation
npx vitest run test/unit/scope-resolution test/integration/resolvers→ 2514 passed / 77 skipped / 0 failed (52 test files, 85 s).npx tsc --noEmitclean in bothgitnexus/andgitnexus-shared/.scope extraction failed for *.Designer.cswarnings → 0. Existing post-fix(ingestion): two-channel binding lifecycle (closes #1066) + scope-resolution I8 hardening #1082 baseline is 1113 nodes / 2987 edges / 39 clusters / 97 flows; the 3 newly-recovered files contribute additional cross-file resolution beyond that baseline.Cross-language note
canParentScopeis language-agnostic. Other languages whose query emits(compilation_unit) @scope.moduleplus a single same-range top-level scope (e.g. Python'smodulematching a single top-levelclass_definitionon a file with no leading/trailing trivia) can naturally hit the same byte shape on minimal files; this fix applies to all of them uniformly.Test plan
npx vitest run test/unit/scope-resolution test/integration/resolvers(52 files / 2514 tests)npx tsc --noEmitin both packagesscope-tree.test.tscase