From 30677d4e9ff564c6ca43d2937d0cc81e9a0960ec Mon Sep 17 00:00:00 2001 From: Gergo Magyar Date: Fri, 10 Apr 2026 10:16:37 +0100 Subject: [PATCH] fix(SM-15): address PR #763 third-pass review findings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. Update stale dispose() JSDoc — remove forward-reference to Phase 9 wiring that is now complete; document actual consumers. 2. Add processAssignmentsFromExtracted Phase 9 unit test — verifies the accumulator fallback produces ACCESSES write edges when the SymbolTable has no returnType for the callee. --- .../src/core/ingestion/binding-accumulator.ts | 11 ++-- gitnexus/test/unit/call-processor.test.ts | 58 +++++++++++++++++++ 2 files changed, 63 insertions(+), 6 deletions(-) diff --git a/gitnexus/src/core/ingestion/binding-accumulator.ts b/gitnexus/src/core/ingestion/binding-accumulator.ts index d3acbf7a1..0f7199b2b 100644 --- a/gitnexus/src/core/ingestion/binding-accumulator.ts +++ b/gitnexus/src/core/ingestion/binding-accumulator.ts @@ -276,12 +276,11 @@ export class BindingAccumulator { * **after** `finalize()`, subsequent `appendFile` calls throw the existing * "finalized" error. * - * Lifecycle note: the pipeline disposes the accumulator after the - * ExportedTypeMap enrichment loop consumes its file-scope entries, so - * the heap is released before Phase 14 (`runCrossFileBindingPropagation`) - * and `runGraphAnalysisPhases` begin their long-running work. When Phase 9 - * wires a consumer into that stage, the dispose call should move later in - * the pipeline or be removed entirely. + * Lifecycle note: the pipeline disposes the accumulator after both Phase 9 + * consumers (`processCallsFromExtracted`, `processAssignmentsFromExtracted`) + * and the ExportedTypeMap enrichment loop have completed, so the heap is + * released before Phase 14 (`runCrossFileBindingPropagation`) and + * `runGraphAnalysisPhases` begin their long-running work. */ dispose(): void { this._allByFile.clear(); diff --git a/gitnexus/test/unit/call-processor.test.ts b/gitnexus/test/unit/call-processor.test.ts index 840dcd0ea..a0754082d 100644 --- a/gitnexus/test/unit/call-processor.test.ts +++ b/gitnexus/test/unit/call-processor.test.ts @@ -2,6 +2,7 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'; import { processCalls, processCallsFromExtracted, + processAssignmentsFromExtracted, seedCrossFileReceiverTypes, extractConsumerAccessedKeys, processNextjsFetchRoutes, @@ -16,6 +17,7 @@ import { import { createKnowledgeGraph } from '../../src/core/graph/graph.js'; import { BindingAccumulator } from '../../src/core/ingestion/binding-accumulator.js'; import type { + ExtractedAssignment, ExtractedCall, ExtractedFetchCall, ExtractedHeritage, @@ -2492,3 +2494,59 @@ describe('processCalls — D0 MRO fast path (SM-10)', () => { expect(userSave).toBeUndefined(); }); }); + +// ---- processAssignmentsFromExtracted: Phase 9 accumulator fallback ---- + +describe('processAssignmentsFromExtracted', () => { + let graph: ReturnType; + let ctx: ResolutionContext; + + beforeEach(() => { + graph = createKnowledgeGraph(); + ctx = createResolutionContext(); + }); + + it('Phase 9: accumulator fallback resolves receiver type for ACCESSES write edge', () => { + // getUser is in the SymbolTable WITHOUT a returnType. The accumulator + // carries getUser → User from the source file. The constructor binding + // binds x = getUser(). The assignment x.address = value should produce + // an ACCESSES write edge to User.address via the accumulator fallback. + ctx.symbols.add('src/api.ts', 'getUser', 'Function:src/api.ts:getUser', 'Function'); + ctx.symbols.add('src/models.ts', 'User', 'Class:src/models.ts:User', 'Class'); + ctx.symbols.add('src/models.ts', 'address', 'Property:src/models.ts:address', 'Property', { + ownerId: 'Class:src/models.ts:User', + }); + ctx.importMap.set('src/consumer.ts', new Set(['src/api.ts', 'src/models.ts'])); + ctx.namedImportMap.set( + 'src/consumer.ts', + new Map([['getUser', { sourcePath: 'src/api.ts', exportedName: 'getUser' }]]), + ); + + const acc = new BindingAccumulator(); + acc.appendFile('src/api.ts', [{ scope: '', varName: 'getUser', typeName: 'User' }]); + + const constructorBindings: FileConstructorBindings[] = [ + { + filePath: 'src/consumer.ts', + bindings: [{ scope: 'main@0', varName: 'x', calleeName: 'getUser' }], + }, + ]; + + const assignments: ExtractedAssignment[] = [ + { + filePath: 'src/consumer.ts', + sourceId: 'Function:src/consumer.ts:main', + receiverText: 'x', + propertyName: 'address', + }, + ]; + + processAssignmentsFromExtracted(graph, assignments, ctx, constructorBindings, acc); + + const accesses = graph.relationships.filter( + (r) => r.type === 'ACCESSES' && r.reason === 'write', + ); + expect(accesses).toHaveLength(1); + expect(accesses[0].targetId).toBe('Property:src/models.ts:address'); + }); +});