From e3f4d8b1eb30acbde647d4b4b917fcd5bae47331 Mon Sep 17 00:00:00 2001 From: Gergo Magyar Date: Wed, 22 Apr 2026 16:59:27 +0100 Subject: [PATCH] refactor(scope-resolution): narrow handles + tighten required params Two small hygiene fixes that fell out of the unified-model work: * Introduce `readonlyModel: SemanticModel` in `runScopeResolution` immediately after reconciliation so the write/read phase boundary is explicit at the code level. Downstream passes (receiver-bound, free-call) receive the narrowed `SemanticModel` rather than the `MutableSemanticModel` that only the reconciliation pass needs. The type system now rejects accidental writes in the read phase. * Make `emitFreeCallFallback`'s `workspaceIndex` parameter required. It's now always passed (every caller threads it through), and the `workspaceIndex?` guard was dead code. Also drops the `| undefined` branch from `pickConstructorOrClass` which no caller can hit. No behavior change. --- .../scope-resolution/passes/free-call-fallback.ts | 7 +++---- .../ingestion/scope-resolution/pipeline/run.ts | 15 +++++++++++---- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/gitnexus/src/core/ingestion/scope-resolution/passes/free-call-fallback.ts b/gitnexus/src/core/ingestion/scope-resolution/passes/free-call-fallback.ts index b2f7fefb0..b5021eded 100644 --- a/gitnexus/src/core/ingestion/scope-resolution/passes/free-call-fallback.ts +++ b/gitnexus/src/core/ingestion/scope-resolution/passes/free-call-fallback.ts @@ -34,7 +34,7 @@ export function emitFreeCallFallback( _referenceIndex: { readonly bySourceScope: ReadonlyMap }, handledSites: Set, model: SemanticModel, - workspaceIndex?: WorkspaceResolutionIndex, + workspaceIndex: WorkspaceResolutionIndex, ): number { let emitted = 0; const seen = new Set(); @@ -60,7 +60,7 @@ export function emitFreeCallFallback( // enclosing class. When the workspace has multiple methods of // the same name in a single class, choose the best match by // arity + argument types. - if (fnDef === undefined && workspaceIndex !== undefined) { + if (fnDef === undefined) { fnDef = pickImplicitThisOverload(site, scopes, workspaceIndex, model); } if (fnDef === undefined) { @@ -101,9 +101,8 @@ export function emitFreeCallFallback( * ctors and targetLabel === 'Constructor' for explicit ones. */ function pickConstructorOrClass( classDef: SymbolDefinition, - workspaceIndex: WorkspaceResolutionIndex | undefined, + workspaceIndex: WorkspaceResolutionIndex, ): SymbolDefinition { - if (workspaceIndex === undefined) return classDef; const classScope = workspaceIndex.classScopeByDefId.get(classDef.nodeId); if (classScope === undefined) return classDef; for (const def of classScope.ownedDefs) { diff --git a/gitnexus/src/core/ingestion/scope-resolution/pipeline/run.ts b/gitnexus/src/core/ingestion/scope-resolution/pipeline/run.ts index c548597c9..d96bb70c1 100644 --- a/gitnexus/src/core/ingestion/scope-resolution/pipeline/run.ts +++ b/gitnexus/src/core/ingestion/scope-resolution/pipeline/run.ts @@ -25,7 +25,7 @@ import type { ParsedFile, RegistryProviders } from 'gitnexus-shared'; import type { KnowledgeGraph } from '../../../graph/types.js'; -import type { MutableSemanticModel } from '../../model/semantic-model.js'; +import type { MutableSemanticModel, SemanticModel } from '../../model/semantic-model.js'; import { reconcileOwnership, validateOwnershipParity } from './reconcile-ownership.js'; import { extractParsedFile } from '../../scope-extractor-bridge.js'; import { finalizeScopeModel } from '../../finalize-orchestrator.js'; @@ -77,7 +77,7 @@ export function runScopeResolution( input: RunScopeResolutionInput, provider: ScopeResolver, ): RunScopeResolutionStats { - const { graph, files, model } = input; + const { graph, files } = input; const onWarn = input.onWarn ?? (() => {}); const PROF = process.env.PROF_SCOPE_RESOLUTION === '1'; const tStart = PROF ? process.hrtime.bigint() : 0n; @@ -107,8 +107,15 @@ export function runScopeResolution( // See `reconcile-ownership.ts` for the full rationale (Contract // Invariant I9). Debug-mode validator runs immediately after to // catch drift between `parsed.localDefs` and the registries. + // + // PHASE BOUNDARY: `input.model` is `MutableSemanticModel` up to this + // point (write phase: reconciliation). After this line no further + // writes are expected — downstream passes consume `readonlyModel` + // (narrowed to `SemanticModel`) so accidental writes would surface + // as type errors. reconcileOwnership(parsedFiles, input.model); validateOwnershipParity(parsedFiles, input.model, onWarn); + const readonlyModel: SemanticModel = input.model; if (parsedFiles.length === 0) { return { @@ -194,7 +201,7 @@ export function runScopeResolution( handledSites, provider, workspaceIndex, - model, + readonlyModel, ); const freeCallExtras = emitFreeCallFallback( graph, @@ -203,7 +210,7 @@ export function runScopeResolution( nodeLookup, referenceIndex, handledSites, - model, + readonlyModel, workspaceIndex, ); const { emitted, skipped } = emitReferencesViaLookup(