From d39482dffeabcb000cb8bcd0eec3c56fc1ceab66 Mon Sep 17 00:00:00 2001 From: mann1x <20623405+mann1x@users.noreply.github.com> Date: Mon, 27 Apr 2026 11:27:21 +0200 Subject: [PATCH] refactor(scope-resolution): export canParentScope from gitnexus-shared MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- gitnexus-shared/src/index.ts | 6 ++- .../src/scope-resolution/scope-tree.ts | 2 +- .../src/core/ingestion/scope-extractor.ts | 52 +------------------ 3 files changed, 7 insertions(+), 53 deletions(-) diff --git a/gitnexus-shared/src/index.ts b/gitnexus-shared/src/index.ts index d4efe8a89..ea66c3855 100644 --- a/gitnexus-shared/src/index.ts +++ b/gitnexus-shared/src/index.ts @@ -134,7 +134,11 @@ export type { // Scope tree spine + position lookup (RFC §2.2 + §3.1; Ring 2 SHARED #912) export { makeScopeId, clearScopeIdInternPool } from './scope-resolution/scope-id.js'; export type { ScopeIdInput } from './scope-resolution/scope-id.js'; -export { buildScopeTree, ScopeTreeInvariantError } from './scope-resolution/scope-tree.js'; +export { + buildScopeTree, + canParentScope, + ScopeTreeInvariantError, +} from './scope-resolution/scope-tree.js'; export type { ScopeTree } from './scope-resolution/scope-tree.js'; export { buildPositionIndex } from './scope-resolution/position-index.js'; export type { PositionIndex } from './scope-resolution/position-index.js'; diff --git a/gitnexus-shared/src/scope-resolution/scope-tree.ts b/gitnexus-shared/src/scope-resolution/scope-tree.ts index 96aa222ec..5011f2a36 100644 --- a/gitnexus-shared/src/scope-resolution/scope-tree.ts +++ b/gitnexus-shared/src/scope-resolution/scope-tree.ts @@ -260,7 +260,7 @@ function rangesEqual(a: Range, b: Range): boolean { * helper in `scope-extractor.ts` so `pass1BuildScopes` and the validator * agree on what a well-formed parent edge looks like. */ -function canParentScope( +export function canParentScope( outer: Range, inner: Range, outerKind: Scope['kind'], diff --git a/gitnexus/src/core/ingestion/scope-extractor.ts b/gitnexus/src/core/ingestion/scope-extractor.ts index 829db89da..063b8a62d 100644 --- a/gitnexus/src/core/ingestion/scope-extractor.ts +++ b/gitnexus/src/core/ingestion/scope-extractor.ts @@ -74,7 +74,7 @@ import type { SymbolDefinition, TypeRef, } from 'gitnexus-shared'; -import { buildPositionIndex, buildScopeTree, makeScopeId } from 'gitnexus-shared'; +import { buildPositionIndex, buildScopeTree, canParentScope, makeScopeId } from 'gitnexus-shared'; import type { LanguageProvider } from './language-provider.js'; // ─── Narrow hook surface the extractor actually uses ─────────────────────── @@ -924,56 +924,6 @@ function rangesEqual(a: Range, b: Range): boolean { ); } -function rangeStrictlyContains(outer: Range, inner: Range): boolean { - if ( - outer.startLine === inner.startLine && - outer.startCol === inner.startCol && - outer.endLine === inner.endLine && - outer.endCol === inner.endCol - ) { - return false; - } - const startsBefore = - outer.startLine < inner.startLine || - (outer.startLine === inner.startLine && outer.startCol <= inner.startCol); - const endsAfter = - outer.endLine > inner.endLine || - (outer.endLine === inner.endLine && outer.endCol >= inner.endCol); - return startsBefore && endsAfter; -} - -/** - * Whether `outer` (kind `outerKind`) is a valid parent for `inner` (kind - * `innerKind`) by tree-sitter range geometry. - * - * Strict containment is the general rule. The single carve-out is the - * `Module`/non-Module pair whose ranges are exactly equal — this happens - * naturally when tree-sitter reports identical byte spans for the - * `compilation_unit` and the file's single top-level scope (e.g. a C# file - * consisting of nothing but `namespace X { ... }` with no leading or - * trailing trivia outside the namespace's `{}` body). The `Module` - * is the universal outer of any file-level scope by language semantics, so - * coincident ranges should not break the parent chain. The `rangesEqual` - * Module carve-out is direction-asymmetric: only Module-as-outer parents - * a same-range non-Module, never the reverse, so the relationship stays - * acyclic and `buildScopeTree`'s parent-must-share-filePath / no-cycle - * invariants continue to hold. - * - * `rangeStrictlyContains` keeps its strict semantics so position-index - * lookups, hook-side range comparisons, and other call sites are - * unchanged. - */ -function canParentScope( - outer: Range, - inner: Range, - outerKind: ScopeKind, - innerKind: ScopeKind, -): boolean { - if (rangeStrictlyContains(outer, inner)) return true; - if (outerKind === 'Module' && innerKind !== 'Module' && rangesEqual(outer, inner)) return true; - return false; -} - /** * Capture names that are never anchors — they are sub-tags nested inside a * larger anchor (e.g., the receiver expression inside a `@reference.call`