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.
This commit is contained in:
mann1x 2026-04-27 11:27:21 +02:00
parent fbdf2ede54
commit d39482dffe
3 changed files with 7 additions and 53 deletions

View file

@ -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';

View file

@ -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'],

View file

@ -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`