diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index d190486ab..e323c15f5 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -231,7 +231,35 @@ The CI parity workflow (`.github/workflows/ci-scope-parity.yml`) runs both paths #### Semantic-model source of truth -`ParsedFile` (`gitnexus-shared/src/scope-resolution/parsed-file.ts`) is the single semantic model both paths consume. Scope-resolution passes MUST NOT build a parallel parse representation. If a per-language hook needs AST-level facts that `ParsedFile` doesn't expose, it should reuse the orchestrator's `treeCache` (`RunScopeResolutionInput.treeCache`) rather than re-invoking `parser.parse(...)` on its own — the C# `populateNamespaceSiblings` hook is the reference implementation of this pattern. +Two independent invariants. + +**ParsedFile = the AST-level truth.** `ParsedFile` (`gitnexus-shared/src/scope-resolution/parsed-file.ts`) is the single per-file artifact both resolution paths consume. Scope-resolution passes MUST NOT build a parallel parse representation. If a per-language hook needs AST-level facts that `ParsedFile` doesn't expose, it should reuse the orchestrator's `treeCache` (`RunScopeResolutionInput.treeCache`) rather than re-invoking `parser.parse(...)` on its own — the C# `populateNamespaceSiblings` hook is the reference implementation of this pattern. + +**SemanticModel = the symbol-level truth.** `SemanticModel` (`gitnexus/src/core/ingestion/model/semantic-model.ts`) is the authoritative store for every symbol-indexed lookup (by `nodeId`, `simpleName`, `qualifiedName`, or `filePath`). Both paths read from here: + +- Legacy Call-Resolution DAG → `call-processor` Tier 1/2/3 via `model.symbols.lookupExactAll`, `model.methods.lookupMethodByName`, `model.types.lookupClassByName`, `lookupMethodByOwnerWithMRO`. +- Scope-resolution pipeline → `findOwnedMember`, `pickOverload`, `findExportedDefByName` all consult `model.methods` / `model.fields` / `model.symbols`. + +The scope-resolution pipeline additionally carries `WorkspaceResolutionIndex` for `Scope`-valued lookups (`classScopeByDefId`, `moduleScopeByFile`) that `SemanticModel` structurally cannot hold. No symbol-indexed duplicates exist outside `SemanticModel`. + +**Write / read phase contract.** The model is mutable during three ordered phases and read-only afterward: + +``` + Phase 1: legacy parse ──► symbolTable.add fans into types/methods/fields + Phase 2: scope-resolution ──► reconcileOwnership() registers corrected ownerIds + Phase 3: finalize ──► model.attachScopeIndexes(bundle) — one-shot freeze + ─────────────────────────── phase boundary ─────────────────────────── + Read phase: all resolution passes + MCP + HTTP + embeddings see + SemanticModel (read-only handle); writes are type-errors. +``` + +`runScopeResolution` narrows `MutableSemanticModel` → `SemanticModel` at the phase boundary so downstream passes physically cannot mutate the model even accidentally. + +**Transitional: reconciliation pass.** `reconcileOwnership` (`scope-resolution/pipeline/reconcile-ownership.ts`) is a shim for languages whose legacy extractor doesn't resolve `enclosingClassId` at parse time (Python class-body methods are the canonical case). It walks `parsed.localDefs[i].ownerId` after `populateOwners` and registers any missed methods/fields into the model. Idempotent — safe to re-run, safe alongside languages whose legacy extractor already carries `ownerId` (C#). + +The architectural end state is for every language's parse-time extractor to emit the correct `ownerId` directly, making reconciliation a no-op (tracked as a follow-up refactor). The dev-mode validator `validateOwnershipParity` surfaces any drift via `onWarn` under `NODE_ENV !== 'production' && VALIDATE_SEMANTIC_MODEL !== '0'`. + +References: `semantic-model.ts` file-head (full write/read contract); `contract/scope-resolver.ts` Contract Invariant I9 (scope-resolution-side rule). --- diff --git a/gitnexus/src/core/ingestion/model/semantic-model.ts b/gitnexus/src/core/ingestion/model/semantic-model.ts index d1c5f1446..b989ff5b3 100644 --- a/gitnexus/src/core/ingestion/model/semantic-model.ts +++ b/gitnexus/src/core/ingestion/model/semantic-model.ts @@ -44,6 +44,45 @@ * direct `createSymbolTable()` caller (e.g. an isolated unit test) gets * the pure, registry-free behavior — no surprises, no hidden side * effects. + * + * ## Single-source-of-truth invariant + * + * `SemanticModel` is the authoritative symbol store for the whole + * ingestion pipeline. Both the legacy Call-Resolution DAG and the + * new scope-resolution pipeline read symbol-keyed lookups from here + * exclusively — no parallel owner-keyed, name-keyed, or file-keyed + * symbol indexes exist outside this module. The scope-resolution + * pipeline does carry a small `WorkspaceResolutionIndex` for + * `Scope`-valued maps (`classScopeByDefId`, `moduleScopeByFile`) that + * `SemanticModel` structurally cannot hold, but nothing else. + * + * ## Write / read phase contract + * + * Writes to the model happen in three clearly-ordered phases during a + * single ingestion run: + * + * 1. **Legacy parse phase** (`parsing-processor`) calls + * `symbols.add(...)` per extracted symbol, which fans out via + * the dispatch table into `types` / `methods` / `fields`. + * 2. **Scope-resolution reconciliation** (`reconcileOwnership` in + * `scope-resolution/pipeline/reconcile-ownership.ts`) registers + * any `parsed.localDefs[i]` with a scope-resolution-corrected + * `ownerId` that the legacy pass missed (Python class-body + * methods are the canonical case). Idempotent. + * 3. **Finalize-orchestrator** calls `attachScopeIndexes(...)` to + * stamp the materialized `ScopeResolutionIndexes` bundle onto + * `model.scopes`. One-shot; throws on a second call. + * + * After these three phases, the model is effectively frozen: + * - `attachScopeIndexes` applied `Object.freeze` to its bundle. + * - Downstream passes receive the narrowed `SemanticModel` reader + * handle (not `MutableSemanticModel`), so `.register()` / + * `.clear()` / `attachScopeIndexes()` are structurally absent. + * + * See `scope-resolution/contract/scope-resolver.ts` Contract + * Invariant I9 for the scope-resolution-side rule and + * `ARCHITECTURE.md` § "Semantic-model source of truth" for the + * overall architecture. */ import type { NodeLabel } from 'gitnexus-shared'; diff --git a/gitnexus/src/core/ingestion/scope-resolution/contract/scope-resolver.ts b/gitnexus/src/core/ingestion/scope-resolution/contract/scope-resolver.ts index d04afbc3d..c23e90c74 100644 --- a/gitnexus/src/core/ingestion/scope-resolution/contract/scope-resolver.ts +++ b/gitnexus/src/core/ingestion/scope-resolution/contract/scope-resolver.ts @@ -138,6 +138,44 @@ * `ScopeResolutionIndexes` is a read-guidance surface for * consumers, NOT an immutability promise during the resolve phase. * + * - **I9 — `SemanticModel` is the single authoritative symbol store.** + * Every symbol-indexed lookup (key = `nodeId | simpleName | + * qualifiedName | filePath`) resolves through + * `SemanticModel.{symbols,types,methods,fields}`. Scope-resolution + * passes MUST NOT maintain parallel owner-keyed or name-keyed + * symbol indexes — `WorkspaceResolutionIndex` is reserved for + * `Scope`-valued lookups that `SemanticModel` structurally cannot + * carry. + * + * The `runScopeResolution` orchestrator guarantees this invariant + * in two steps: + * 1. The legacy `parse` phase populates `SemanticModel` via + * `symbolTable.add(...)`. For languages whose extractor + * resolves `enclosingClassId` at parse time, class-body defs + * are correctly owner-keyed there. + * 2. The `reconcileOwnership` pass runs after + * `provider.populateOwners(parsed)` and registers any def in + * `parsed.localDefs[i]` with a corrected `ownerId` that the + * legacy pass missed (primarily Python class-body methods). + * Idempotent — duplicates are skipped by `nodeId`. + * + * Contract for consumers: `model` is `MutableSemanticModel` only + * during those two write phases. Downstream passes receive a + * narrowed `SemanticModel` (read-only) handle. This is enforced by + * `runScopeResolution`'s type-level narrowing at the phase + * boundary. + * + * The dev-mode runtime validator (`validateOwnershipParity`) + * surfaces any drift between `parsed.localDefs` ownership and the + * registries via `onWarn` when + * `NODE_ENV !== 'production' && VALIDATE_SEMANTIC_MODEL !== '0'`. + * + * This invariant is a **transitional shim**: the architectural + * end state is for every language's parse-time extractor to emit + * the correct `ownerId` directly, removing the need for + * reconciliation. Tracked as a follow-up; see ARCHITECTURE.md § + * "Semantic-model source of truth". + * * ## Semantic-model source of truth * * `ParsedFile` (from `gitnexus-shared/src/scope-resolution/parsed-file.ts`)