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(