From 8ba0a887ff339be32aa3ca4d84d88732854d7b0a Mon Sep 17 00:00:00 2001 From: morluto <76467478+morluto@users.noreply.github.com> Date: Sun, 11 Oct 2026 03:18:46 +0800 Subject: [PATCH] refactor(javascript): bind application links only at graph finish Remove the empty application-node index from incremental projection state. The completed application graph now owns the only binding pass, and indexing returns the map directly instead of a wrapper. Validation: 76 builder/composition tests and 19 filesystem, CLI-output, and SDK MCP trace boundary tests pass. Identity budgets and streaming/full-projection parity remain covered; no throughput improvement is claimed. --- .../JavaScriptSemanticGraphBuilder.test.ts | 4 +- .../JavaScriptSemanticGraphBuilder.ts | 2 +- .../JavaScriptSemanticGraphConstruction.ts | 38 ++++++------------- 3 files changed, 14 insertions(+), 30 deletions(-) diff --git a/src/application/javascript/JavaScriptSemanticGraphBuilder.test.ts b/src/application/javascript/JavaScriptSemanticGraphBuilder.test.ts index 216aa309c..590f4950b 100644 --- a/src/application/javascript/JavaScriptSemanticGraphBuilder.test.ts +++ b/src/application/javascript/JavaScriptSemanticGraphBuilder.test.ts @@ -355,7 +355,7 @@ const semanticNodeFor = (roleKey: string) => { unpacked: false, text: { included: true, value: "" }, }; - const state = createSemanticGraphProjectionState({ nodes: [] }); + const state = createSemanticGraphProjectionState(); return constructSemanticGraphNode( file, { @@ -565,7 +565,7 @@ it("does not mark a file truncated when it exactly fills its budget", () => { it("only records node loss when the budget rejects a new identity", () => { // The truncation signal must record blocked node creation, not a zero // remaining budget: a file using exactly its share drops nothing. - const state = createSemanticGraphProjectionState({ nodes: [] }); + const state = createSemanticGraphProjectionState(); state.fileNodeBudget = 2; state.fileNodesDropped = false; const first = addSemanticGraphNode(state, semanticNodeFor("first")); diff --git a/src/application/javascript/JavaScriptSemanticGraphBuilder.ts b/src/application/javascript/JavaScriptSemanticGraphBuilder.ts index a0cc8ccf8..3ed0cafda 100644 --- a/src/application/javascript/JavaScriptSemanticGraphBuilder.ts +++ b/src/application/javascript/JavaScriptSemanticGraphBuilder.ts @@ -148,7 +148,7 @@ export interface JavaScriptSemanticGraphProjection { /** Accumulate semantic nodes while each source IR is still file-local. */ export const createJavaScriptSemanticGraphProjection = (): JavaScriptSemanticGraphProjection => { - const state = emptyState({ nodes: [] }); + const state = emptyState(); const fingerprints: JavaScriptSemanticFingerprint[] = []; let truncatedFiles = 0; const projectSourceSteps = function* ( diff --git a/src/application/javascript/JavaScriptSemanticGraphConstruction.ts b/src/application/javascript/JavaScriptSemanticGraphConstruction.ts index ebe27fe43..c61047d5b 100644 --- a/src/application/javascript/JavaScriptSemanticGraphConstruction.ts +++ b/src/application/javascript/JavaScriptSemanticGraphConstruction.ts @@ -28,7 +28,6 @@ export interface SemanticGraphProjectionState { readonly unknowns: Map; readonly evidenceContexts: JavaScriptSemanticEvidenceContextRegistry; readonly roots: Set; - readonly applicationNodeIdsByLocation: ReadonlyMap; /** * Remaining node budget for the file currently being projected. * @@ -69,32 +68,29 @@ export interface SemanticRelationConstructionInput { readonly properties?: Readonly>; } -/** Create empty projection state with structural application-node mappings. */ -export const createSemanticGraphProjectionState = ( - applicationGraph: Pick, -): SemanticGraphProjectionState => { - const index = indexApplicationNodes(applicationGraph.nodes); - return { +/** Create empty projection state; application-node links are bound at finish. */ +export const createSemanticGraphProjectionState = + (): SemanticGraphProjectionState => ({ nodes: new Map(), relations: new Map(), unknowns: new Map(), evidenceContexts: new JavaScriptSemanticEvidenceContextRegistry(), roots: new Set(), - applicationNodeIdsByLocation: index.identifiers, - }; -}; + }); /** Bind retained semantic identities after all application observations exist. */ export const bindSemanticGraphApplicationNodes = ( state: SemanticGraphProjectionState, applicationGraph: Pick, ): void => { - const index = indexApplicationNodes(applicationGraph.nodes).identifiers; + const applicationNodeIdsByLocation = indexApplicationNodes( + applicationGraph.nodes, + ); for (const [identifier, node] of state.nodes) { state.nodes.set(identifier, { ...node, application_node_ids: [ - ...(index.get( + ...(applicationNodeIdsByLocation.get( applicationLocationKey( node.identity.artifact_sha256, node.identity.module_path, @@ -122,7 +118,7 @@ export const constructSemanticGraphNode = ( role_key: input.roleKey, }, function_node_id: input.functionNodeId, - application_node_ids: matchingApplicationNodeIds(file, input, state), + application_node_ids: [], label: input.label === null ? null : javascriptDisplayText(input.label), properties: input.properties ?? {}, evidence: observedSemanticEvidence(file, input.location), @@ -247,9 +243,7 @@ export const addSemanticFallbackRoot = ( const indexApplicationNodes = ( nodes: JavaScriptApplicationGraph["nodes"], -): { - readonly identifiers: ReadonlyMap; -} => { +): ReadonlyMap => { const identifiers = new Map>(); for (const node of nodes) { for (const observation of node.observations) { @@ -280,19 +274,9 @@ const indexApplicationNodes = ( return [key, sorted]; }), ); - return { identifiers: indexed }; + return indexed; }; -const matchingApplicationNodeIds = ( - file: JavaScriptArtifactFile, - input: SemanticNodeConstructionInput, - state: SemanticGraphProjectionState, -): string[] => [ - ...(state.applicationNodeIdsByLocation.get( - applicationLocationKey(file.sha256, file.path, input.location), - ) ?? []), -]; - const addApplicationNodeIdentifier = ( identifiers: Map>, key: string,