From 6323013d83c25e342855e7651586341201768991 Mon Sep 17 00:00:00 2001 From: morluto <76467478+morluto@users.noreply.github.com> Date: Sat, 24 Oct 2026 07:10:46 +0800 Subject: [PATCH] fix(javascript): bound query unions and refresh semantic fixtures --- .../JavaScriptSemanticGraphResources.test.ts | 48 +++++- ...tApplicationEndpointCorrespondence.test.ts | 53 +++---- .../javascriptRouteInventory.test.ts | 28 +++- .../javascriptSemanticDependencies.ts | 67 +++++++-- .../javascriptSemanticGraphQuery.test.ts | 2 + .../javascriptSemanticValues.memo.test.ts | 138 ++++++++++++++++-- .../javascript/javascriptSemanticValues.ts | 10 +- 7 files changed, 282 insertions(+), 64 deletions(-) diff --git a/src/application/javascript/JavaScriptSemanticGraphResources.test.ts b/src/application/javascript/JavaScriptSemanticGraphResources.test.ts index 69d73b5a44..0b4108bc64 100644 --- a/src/application/javascript/JavaScriptSemanticGraphResources.test.ts +++ b/src/application/javascript/JavaScriptSemanticGraphResources.test.ts @@ -290,12 +290,56 @@ it("projects literal seeds and static object flow", () => { direction: "forward-influence", }); expect(literal.status).toBe("ambiguous"); - expect(literal.summary.total_seed_matches).toBe(3); + const targetName = (targetId: string): string => { + const target = graph.nodes.find(({ node_id }) => node_id === targetId); + if (target === undefined) throw new Error("Missing value target"); + if (target.kind === "binding" && target.label !== null) return target.label; + const owner = graph.relations.find( + ({ relation, target_node_id }) => + relation === "writes-property" && target_node_id === target.node_id, + ); + const binding = graph.nodes.find( + ({ node_id }) => node_id === owner?.source_node_id, + ); + if (binding === undefined) throw new Error("Missing property owner"); + return `${binding.label}.${String(target.properties.name)}`; + }; + const literalTargets = graph.relations + .filter( + ({ relation, source_node_id }) => + relation === "defines" && + literal.seed_node_ids.includes(source_node_id), + ) + .map(({ target_node_id }) => targetName(target_node_id)); + expect(literalTargets.sort()).toEqual([ + "copy.token", + "read", + "source.token", + "token", + ]); + expect(literal.summary.total_seed_matches).toBe(literalTargets.length); const property = queryJavaScriptSemanticGraph(graph, { seed: { kind: "property", name: "token" }, direction: "forward-influence", }); - expect(property.status).toBe("found"); + expect(property.status).toBe("ambiguous"); + expect(property.seed_node_ids.map(targetName).sort()).toEqual([ + "copy.token", + "source.token", + ]); + expect(property.unknowns).toEqual([]); + expect(property.relations).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + relation: "destructures", + resolution: "resolved", + }), + expect.objectContaining({ + relation: "reads-property", + resolution: "resolved", + }), + ]), + ); }); it("does not promote transformed return expressions to resolved aliases", () => { diff --git a/src/domain/javascript/javascriptApplicationEndpointCorrespondence.test.ts b/src/domain/javascript/javascriptApplicationEndpointCorrespondence.test.ts index 1b185154aa..4100255dc4 100644 --- a/src/domain/javascript/javascriptApplicationEndpointCorrespondence.test.ts +++ b/src/domain/javascript/javascriptApplicationEndpointCorrespondence.test.ts @@ -18,6 +18,8 @@ const endpoint = (options: { readonly method?: string; readonly candidate?: boolean; readonly value?: string; + readonly resolution?: null | "unresolved"; + readonly state?: "unknown" | "unavailable"; }): ApplicationNode => { const path = options.path ?? "main.js"; const line = options.line ?? 1; @@ -35,11 +37,28 @@ const endpoint = (options: { endpoint_kind: options.kind ?? "fetch", value: options.value ?? "API_BASE", ...(options.partial ? { resolution: "partial" } : {}), + ...(options.resolution === undefined + ? {} + : { resolution: options.resolution }), ...(options.candidate ? { interpretation: "candidate" } : {}), ...(options.method === undefined ? {} : { method: options.method }), }, { ...artifactEvidence(options.sha, path, "ast-static-analysis"), + ...(options.state === undefined + ? {} + : { + state: options.state, + authority: "unknown", + confidence: "unknown", + coverage: { + status: "unknown", + truncated: false, + omitted_count: null, + limits: [], + }, + limitations: ["Endpoint target was not established."], + }), location: { available: true, value: { @@ -162,14 +181,10 @@ it("keeps unrelated unresolved endpoint kinds from hiding exact target absence", it.each([null, "unresolved"] as const)( "preserves explicit unknown endpoint resolution %s", (resolution) => { - const right = endpoint({ sha: "b".repeat(64) }); - const result = compare(endpoint({ sha: "a".repeat(64) }), { - ...right, - observations: right.observations.map((observation) => ({ - ...observation, - properties: { ...observation.properties, resolution }, - })), - }); + const result = compare( + endpoint({ sha: "a".repeat(64) }), + endpoint({ sha: "b".repeat(64), resolution }), + ); expect(result.items).toHaveLength(2); expect( result.items.every( @@ -183,29 +198,9 @@ it.each([null, "unresolved"] as const)( it.each(["unknown", "unavailable"] as const)( "keeps %s endpoint Evidence from proving another target absent", (state) => { - const right = endpoint({ sha: "b".repeat(64), value: "unestablished" }); - const uncertain: ApplicationNode = { - ...right, - observations: right.observations.map((observation) => ({ - ...observation, - evidence: { - ...observation.evidence, - state, - authority: "unknown", - confidence: "unknown", - coverage: { - status: "unknown", - truncated: false, - omitted_count: null, - limits: [], - }, - limitations: ["Endpoint target was not established."], - }, - })), - }; const result = compare( endpoint({ sha: "a".repeat(64), value: "known" }), - uncertain, + endpoint({ sha: "b".repeat(64), value: "unestablished", state }), ); expect(result.items).toHaveLength(2); expect( diff --git a/src/domain/javascript/javascriptRouteInventory.test.ts b/src/domain/javascript/javascriptRouteInventory.test.ts index a3b528a894..a1423210fb 100644 --- a/src/domain/javascript/javascriptRouteInventory.test.ts +++ b/src/domain/javascript/javascriptRouteInventory.test.ts @@ -81,9 +81,35 @@ it("coalesces exact patterns while retaining independent occurrences and stable "/reports/settings", ]); expect(result.routes[0]).toMatchObject({ - occurrence_count: 3, + occurrence_count: 2, source_count: 2, }); + const reportOccurrences = result.occurrences.filter( + ({ route_id }) => route_id === result.routes[0]!.id, + ); + expect( + reportOccurrences + .map(({ source_id, mechanism, segments }) => ({ + source: result.sources.find(({ id }) => id === source_id)?.key, + mechanism, + segments: segments.map(({ value }) => value), + })) + .sort((left, right) => left.source!.localeCompare(right.source!)), + ).toEqual([ + { + source: "one.js", + mechanism: "navigation-target", + segments: ["/reports"], + }, + { + source: "two.js", + mechanism: "nested-route-object", + segments: ["/reports"], + }, + ]); + expect(new Set(result.occurrences.map(({ id }) => id)).size).toBe( + result.occurrences.length, + ); expect(result.routes.map(({ id }) => id)).toEqual( repeated.routes.map(({ id }) => id), ); diff --git a/src/domain/javascript/javascriptSemanticDependencies.ts b/src/domain/javascript/javascriptSemanticDependencies.ts index 3fca67577c..3c71e6bb57 100644 --- a/src/domain/javascript/javascriptSemanticDependencies.ts +++ b/src/domain/javascript/javascriptSemanticDependencies.ts @@ -7,35 +7,58 @@ export type SemanticDependencyProof = } | undefined; +interface SemanticDependencyLayout { + readonly indices: ReadonlyMap; + readonly singletons: Map; + readonly height: number; +} + /** * Identity layout belongs to the indexed state, not to a query or its effects. * Every proof has the same height. Union shares unchanged branches, so aliases * add a path rather than copying their dependency's transitive membership set. */ export class SemanticDependencyProofs { - private readonly indices = new Map(); - private readonly singletons = new Map(); - private readonly height: number; + // Proofs are immutable; the same pair always produces the same union. + // This cache belongs to one query, while its immutable identity layout is + // shared across queries. Weak keys also release unused pairs within a query. + private readonly unions = new WeakMap< + Exclude, + WeakMap< + Exclude, + SemanticDependencyProof + > + >(); + private constructor(private readonly layout: SemanticDependencyLayout) {} - constructor(bindings: Iterable) { - for (const binding of bindings) - this.indices.set(binding, this.indices.size); - this.height = Math.ceil(Math.log2(Math.max(1, this.indices.size))); + static forBindings(bindings: Iterable): SemanticDependencyProofs { + const indices = new Map(); + for (const binding of bindings) indices.set(binding, indices.size); + return new SemanticDependencyProofs({ + indices, + singletons: new Map(), + height: Math.ceil(Math.log2(Math.max(1, indices.size))), + }); + } + + /** Share identity proofs while retaining unions for only one query. */ + forQuery(): SemanticDependencyProofs { + return new SemanticDependencyProofs(this.layout); } binding(binding: string): SemanticDependencyProof { - const prior = this.singletons.get(binding); + const prior = this.layout.singletons.get(binding); if (prior !== undefined) return prior; const index = this.index(binding); let proof: SemanticDependencyProof = true; - for (let bit = 0; bit < this.height; bit++) { + for (let bit = 0; bit < this.layout.height; bit++) { const one = Math.floor(index / 2 ** bit) % 2 === 1; proof = Object.freeze({ zero: one ? undefined : proof, one: one ? proof : undefined, }); } - this.singletons.set(binding, proof); + this.layout.singletons.set(binding, proof); return proof; } @@ -46,11 +69,25 @@ export class SemanticDependencyProofs { if (first === second || second === undefined) return first; if (first === undefined) return second; if (first === true || second === true) return true; + const cached = + this.unions.get(first)?.get(second) ?? + this.unions.get(second)?.get(first); + if (cached !== undefined) return cached; const zero = this.union(first.zero, second.zero); const one = this.union(first.one, second.one); - if (zero === first.zero && one === first.one) return first; - if (zero === second.zero && one === second.one) return second; - return Object.freeze({ zero, one }); + const result = + zero === first.zero && one === first.one + ? first + : zero === second.zero && one === second.one + ? second + : Object.freeze({ zero, one }); + let unions = this.unions.get(first); + if (unions === undefined) { + unions = new WeakMap(); + this.unions.set(first, unions); + } + unions.set(second, result); + return result; } intersects( @@ -64,7 +101,7 @@ export class SemanticDependencyProofs { has(proof: SemanticDependencyProof, binding: string): boolean { const index = this.index(binding); - for (let bit = this.height - 1; bit >= 0; bit--) { + for (let bit = this.layout.height - 1; bit >= 0; bit--) { if (proof === undefined) return false; if (proof === true) return true; proof = Math.floor(index / 2 ** bit) % 2 === 1 ? proof.one : proof.zero; @@ -73,7 +110,7 @@ export class SemanticDependencyProofs { } private index(binding: string): number { - const index = this.indices.get(binding); + const index = this.layout.indices.get(binding); if (index === undefined) throw new TypeError(`Unindexed semantic dependency: ${binding}`); return index; diff --git a/src/domain/javascript/javascriptSemanticGraphQuery.test.ts b/src/domain/javascript/javascriptSemanticGraphQuery.test.ts index 562a78ca55..25fcd3233a 100644 --- a/src/domain/javascript/javascriptSemanticGraphQuery.test.ts +++ b/src/domain/javascript/javascriptSemanticGraphQuery.test.ts @@ -68,6 +68,7 @@ it("accepts truncated partial coverage with unknown omission extents", () => { expect( createJavaScriptSemanticGraph({ ...input, + limitations: ["Fixture resource limit prevents complete graph coverage."], coverage: { ...input.coverage, status: "partial", @@ -86,6 +87,7 @@ it("accepts omitted unknown occurrences as the reason for semantic truncation", const family = input.coverage.families[0]!.family; const graph = createJavaScriptSemanticGraph({ ...input, + limitations: ["Fixture resource limit prevents complete graph coverage."], coverage: { ...input.coverage, status: "partial", diff --git a/src/domain/javascript/javascriptSemanticValues.memo.test.ts b/src/domain/javascript/javascriptSemanticValues.memo.test.ts index cfa9a7f6e4..5833da822f 100644 --- a/src/domain/javascript/javascriptSemanticValues.memo.test.ts +++ b/src/domain/javascript/javascriptSemanticValues.memo.test.ts @@ -178,7 +178,7 @@ it.each([ [false, true], [true, true], ])( - "recomputes a shared cyclic value under a different active ancestor (prewarmed: %s, rebound: %s)", + "keeps forward object references unknown across read snapshots (prewarmed: %s, rebound: %s)", (prewarmed, rebound) => { const parsed = parseJavaScriptSource( ` @@ -204,18 +204,22 @@ ${rebound ? "a=0; b=0;" : ""} if (entry.name === "a" || entry.name === "b") evaluateSemanticBinding(entry, state); const value = evaluateSemanticBinding(binding, state); - const firstCommon = property(property(value, "first"), "shared"); - const secondCommon = property(property(value, "second"), "shared"); - expect(property(firstCommon, "a").status).toBe("cycle"); - expect(property(property(firstCommon, "b"), "own")).toEqual({ - status: "literal", - value: "b", - }); - expect(property(secondCommon, "b").status).toBe("cycle"); - expect(property(property(secondCommon, "a"), "own")).toEqual({ - status: "literal", - value: "a", - }); + // These object initializers read common before its lexical declaration. + // Expanding them into an abstract cycle would invent a runtime value. + for (const [side, own] of [ + ["first", "a"], + ["second", "b"], + ] as const) { + const object = property(value, side); + expect(property(object, "shared")).toEqual({ + status: "unknown", + reason: "Binding common has no proven initializer at this read.", + }); + expect(property(object, "own")).toEqual({ + status: "literal", + value: own, + }); + } }, ); @@ -362,7 +366,13 @@ const root = ${balancedOr(aliases)}; }); it("uses immutable exact identity proofs and shares unchanged union branches", () => { - const proofs = new SemanticDependencyProofs(["a", "b", "c", "d", "e"]); + const proofs = SemanticDependencyProofs.forBindings([ + "a", + "b", + "c", + "d", + "e", + ]); const a = proofs.binding("a"); const b = proofs.binding("b"); const c = proofs.binding("c"); @@ -381,10 +391,108 @@ it("uses immutable exact identity proofs and shares unchanged union branches", ( expect(proofs.has(undefined, "a")).toBe(false); expect(proofs.intersects(abc, new Map([["d", 0]]))).toBe(false); expect(proofs.intersects(abc, new Map([["c", 0]]))).toBe(true); - const single = new SemanticDependencyProofs(["only"]); + const single = SemanticDependencyProofs.forBindings(["only"]); expect(single.has(single.binding("only"), "only")).toBe(true); expect(single.has(undefined, "only")).toBe(false); expect(() => proofs.binding("unindexed")).toThrow( "Unindexed semantic dependency", ); }); + +it("preserves exact union membership through repeated overlapping proof pairs", () => { + const names = ["a", "b", "c", "d", "e"]; + const proofs = SemanticDependencyProofs.forBindings(names); + const subsets = Array.from({ length: 2 ** names.length }, (_, mask) => { + const members = names.filter((_, index) => (mask & (1 << index)) !== 0); + return { + members, + proof: members.reduce>( + (proof, name) => proofs.union(proof, proofs.binding(name)), + undefined, + ), + }; + }); + for (const first of subsets) + for (const second of subsets) { + const expected = new Set([...first.members, ...second.members]); + for (const union of [ + proofs.union(first.proof, second.proof), + proofs.union(second.proof, first.proof), + proofs.union(first.proof, second.proof), + ]) + expect(names.filter((name) => proofs.has(union, name))).toEqual( + [...expected].sort(), + ); + expect(names.filter((name) => proofs.has(first.proof, name))).toEqual( + first.members, + ); + expect(names.filter((name) => proofs.has(second.proof, name))).toEqual( + second.members, + ); + } +}); + +it("shares identity layout without retaining unions across query lifetimes", () => { + const layout = SemanticDependencyProofs.forBindings(["a", "b", "c"]); + const first = layout.forQuery(); + const second = layout.forQuery(); + const a = first.binding("a"); + const b = first.binding("b"); + const ab = first.union(a, b); + expect(first.union(a, b)).toBe(ab); + expect(first.union(b, a)).toBe(ab); + expect(second.binding("a")).toBe(a); + const fresh = second.union(a, b); + expect(fresh).not.toBe(ab); + expect(["a", "b", "c"].map((name) => second.has(fresh, name))).toEqual([ + true, + true, + false, + ]); +}); + +it("releases query union ownership when state-owned values are invalidated", () => { + const parsed = parseJavaScriptSource( + "const a=1; const result=a+a;", + "queries.js", + ); + if (parsed === null) throw new Error("Expected parsed source"); + const steps = indexJavaScriptBindingsSteps(parsed.program); + let step = steps.next(); + while (step.done !== true) step = steps.next(); + const state = step.value; + const result = [...state.bindingsById.values()].find( + ({ name }) => name === "result", + ); + if (result === undefined) throw new Error("Missing result binding"); + const union = SemanticDependencyProofs.prototype.union; + const owners = new Set(); + const spy = vi + .spyOn(SemanticDependencyProofs.prototype, "union") + .mockImplementation(function ( + this: SemanticDependencyProofs, + first, + second, + ) { + owners.add(this); + return union.call(this, first, second); + }); + try { + expect(evaluateSemanticBinding(result, state)).toEqual({ + status: "literal", + value: 2, + }); + expect(owners.size).toBe(1); + const firstOwners = new Set(owners); + owners.clear(); + clearSemanticValueCaches(state); + expect(evaluateSemanticBinding(result, state)).toEqual({ + status: "literal", + value: 2, + }); + expect(owners.size).toBe(1); + expect([...owners].some((owner) => firstOwners.has(owner))).toBe(false); + } finally { + spy.mockRestore(); + } +}); diff --git a/src/domain/javascript/javascriptSemanticValues.ts b/src/domain/javascript/javascriptSemanticValues.ts index af5d487b7c..38d10314f1 100644 --- a/src/domain/javascript/javascriptSemanticValues.ts +++ b/src/domain/javascript/javascriptSemanticValues.ts @@ -128,10 +128,16 @@ const createEvaluationMemo = ( ): EvaluationMemo => { let dependencies = dependencyProofsByState.get(state); if (dependencies === undefined) { - dependencies = new SemanticDependencyProofs(state.bindingsById.keys()); + dependencies = SemanticDependencyProofs.forBindings( + state.bindingsById.keys(), + ); dependencyProofsByState.set(state, dependencies); } - return { provenance: new Map(), dependencyFrames: [], dependencies }; + return { + provenance: new Map(), + dependencyFrames: [], + dependencies: dependencies.forQuery(), + }; }; interface EvaluationContext {