fix(javascript): bound semantic unknown retention at projection

Admit unknowns before canonical construction and provenance interning. Offer independent per-file and application capacities through CLI, Evidence and the owned analysis worker, and reject over-allocation during streamed decoding.

Report examined omitted emission occurrences by family and reason without retaining discarded identities. Preserve omission-free graph identities and distinguish node truncation from overall partial coverage.

Fixes #4797
This commit is contained in:
morluto
2026-10-17 21:57:31 +08:00
parent 926e18b3fc
commit f4322b15ed
34 changed files with 892 additions and 293 deletions
@@ -97,6 +97,12 @@ const parameters = (
...(input.max_semantic_nodes_per_file === undefined
? {}
: { max_semantic_nodes_per_file: input.max_semantic_nodes_per_file }),
...(input.max_semantic_unknowns === undefined
? {}
: { max_semantic_unknowns: input.max_semantic_unknowns }),
...(input.max_semantic_unknowns_per_file === undefined
? {}
: { max_semantic_unknowns_per_file: input.max_semantic_unknowns_per_file }),
...(input.source_paths === undefined
? {}
: { source_paths: input.source_paths }),
@@ -173,7 +173,7 @@ export const analyzeAndAdoptJavaScriptArtifactFiles = async (
}
const result = await worker.analyze(
file,
projection.remainingFileNodeBudget(),
projection.remainingFileCapacity(),
signal,
);
const facts = result.ok ? result.value : result.error;
@@ -145,6 +145,12 @@ export const reconstructJavaScriptArtifact = async (
`Processing ${String(files.files.length - (files.source_selection?.unselected_files ?? 0))} selected application files`,
);
const semanticProjection = createJavaScriptSemanticGraphProjection({
...(input.max_semantic_unknowns === undefined
? {}
: { totalUnknowns: input.max_semantic_unknowns }),
...(input.max_semantic_unknowns_per_file === undefined
? {}
: { fileUnknowns: input.max_semantic_unknowns_per_file }),
...(input.max_semantic_nodes === undefined
? {}
: { totalNodes: input.max_semantic_nodes }),
@@ -1,5 +1,4 @@
import { compactStringIdentityKey } from "../../domain/compactStringIdentity.js";
import { createJavaScriptSemanticGraphUnknown } from "../../domain/javascript/javascriptSemanticGraph.js";
import type { JavaScriptSemanticGraphNode } from "../../domain/javascript/javascriptSemanticGraphSchemas.js";
import type {
JavaScriptSemanticEventOperation,
@@ -125,24 +124,18 @@ const addEventUnknown = (
event: JavaScriptSemanticGraphNode,
relation: ReturnType<typeof eventRelation>,
): void => {
addSemanticGraphUnknown(
context.state,
createJavaScriptSemanticGraphUnknown(
{
node_id: event.node_id,
family: "event",
relation_kinds: [relation],
reason: "ambiguous-target",
detail:
operation.eventName === null
? `Dynamic ${operation.method} event name remains unresolved.`
: `Static ${operation.method} emitter identity is partial.`,
candidate_node_ids: [],
evidence: unknownSemanticEvidence(context.file, operation.location),
},
context.state.evidenceContexts,
),
);
addSemanticGraphUnknown(context.state, {
node_id: event.node_id,
family: "event",
relation_kinds: [relation],
reason: "ambiguous-target",
detail:
operation.eventName === null
? `Dynamic ${operation.method} event name remains unresolved.`
: `Static ${operation.method} emitter identity is partial.`,
candidate_node_ids: [],
evidence: unknownSemanticEvidence(context.file, operation.location),
});
};
/** Project Node timer scheduling and exact local handle cancellation. */
@@ -218,19 +211,13 @@ const projectTimerCancellation = (
resolution: operation.resolution === "complete" ? "resolved" : "candidate",
});
if (operation.resolution === "complete") return;
addSemanticGraphUnknown(
context.state,
createJavaScriptSemanticGraphUnknown(
{
node_id: callSite?.node_id ?? null,
family: "timer",
relation_kinds: ["cancels-timer"],
reason: "ambiguous-target",
detail: `Static ${operation.method} handle resolution was ${operation.resolution}.`,
candidate_node_ids: [],
evidence: unknownSemanticEvidence(context.file, operation.location),
},
context.state.evidenceContexts,
),
);
addSemanticGraphUnknown(context.state, {
node_id: callSite?.node_id ?? null,
family: "timer",
relation_kinds: ["cancels-timer"],
reason: "ambiguous-target",
detail: `Static ${operation.method} handle resolution was ${operation.resolution}.`,
candidate_node_ids: [],
evidence: unknownSemanticEvidence(context.file, operation.location),
});
};
@@ -1050,18 +1050,18 @@ it("projects and adopts more than the default per-file capacity when explicitly
const steps = projectJavaScriptSemanticFileSteps(
file,
analyzeJavaScriptSemantics(text),
20_002,
{ nodes: 20_002, unknowns: 20_002 },
);
let result = steps.next();
while (!result.done) result = steps.next();
expect(result.value.nodes).toHaveLength(20_002);
expect(result.value.truncated).toBe(false);
expect(result.value.nodesTruncated).toBe(false);
const projection = createJavaScriptSemanticGraphProjection({
totalNodes: 20_010,
fileNodes: 20_002,
});
projection.adoptFileProjection(result.value);
expect(projection.remainingFileNodeBudget()).toBe(8);
expect(projection.remainingFileCapacity().nodes).toBe(8);
const graph = projection.finish(SHA256, {
graph_id: `jag_${"b".repeat(64)}`,
nodes: [],
@@ -1,6 +1,5 @@
import {
createJavaScriptSemanticGraph,
createJavaScriptSemanticGraphUnknown,
sealTransferredJavaScriptSemanticGraphSteps,
type JavaScriptSemanticGraph,
} from "../../domain/javascript/javascriptSemanticGraph.js";
@@ -51,7 +50,7 @@ import {
import type { JsonValue } from "../../domain/jsonValue.js";
import {
JAVASCRIPT_ANALYSIS_RESOURCE_DEFAULTS,
JAVASCRIPT_SEMANTIC_NODE_MAXIMUM,
JAVASCRIPT_SEMANTIC_ITEM_MAXIMUM,
javaScriptAnalysisCoverageLimits,
} from "../../domain/javascript/javascriptAnalysisResourceControls.js";
import { projectSemanticFunctionFingerprints } from "./JavaScriptSemanticGraphFingerprintProjection.js";
@@ -70,13 +69,18 @@ import {
} from "./JavaScriptSemanticGraphProjection.js";
import {
addSemanticFallbackRoot as addFallbackRoot,
addSemanticGraphUnknown,
recordSemanticUnknownOmission,
retainSemanticGraphNode as retainNode,
addSemanticGraphRelation as addRelation,
createSemanticGraphProjectionState as emptyState,
bindSemanticGraphApplicationNodes,
type SemanticGraphProjectionState as BuilderState,
} from "./JavaScriptSemanticGraphConstruction.js";
import type { JavaScriptSemanticFileProjection } from "../../domain/javascript/javascriptSemanticFileProjection.js";
import type {
JavaScriptSemanticFileProjection,
JavaScriptSemanticProjectionCapacity,
} from "../../domain/javascript/javascriptSemanticFileProjection.js";
import type { ElectronCallPath } from "../../domain/javascript/electronStaticAnalysisTypes.js";
import { projectElectronSemanticCallPaths } from "./JavaScriptSemanticGraphElectronProjection.js";
@@ -166,7 +170,7 @@ export const buildJavaScriptSemanticGraph = ({
/** File-local semantic projection that does not retain consumed source IR. */
export interface JavaScriptSemanticGraphProjection {
/** Node capacity offered to the next independently analyzed source file. */
readonly remainingFileNodeBudget: () => number;
readonly remainingFileCapacity: () => JavaScriptSemanticProjectionCapacity;
/** Adopt decoded file-local facts; final validation and proof remain in this realm. */
readonly adoptFileProjection: (
projection: JavaScriptSemanticFileProjection,
@@ -201,17 +205,33 @@ export interface JavaScriptSemanticGraphProjection {
/** Accumulate semantic nodes while each source IR is still file-local. */
export const createJavaScriptSemanticGraphProjection = (
limits: { readonly totalNodes?: number; readonly fileNodes?: number } = {},
limits: {
readonly totalNodes?: number;
readonly fileNodes?: number;
readonly totalUnknowns?: number;
readonly fileUnknowns?: number;
} = {},
): JavaScriptSemanticGraphProjection => {
const nodeCeiling = limits.totalNodes ?? SEMANTIC_GRAPH_NODE_CEILING;
const fileNodeCeiling = limits.fileNodes ?? SEMANTIC_GRAPH_FILE_NODE_CEILING;
for (const value of [nodeCeiling, fileNodeCeiling])
const unknownCeiling =
limits.totalUnknowns ??
JAVASCRIPT_ANALYSIS_RESOURCE_DEFAULTS.semanticUnknowns;
const fileUnknownCeiling =
limits.fileUnknowns ??
JAVASCRIPT_ANALYSIS_RESOURCE_DEFAULTS.semanticUnknownsPerFile;
for (const value of [
nodeCeiling,
fileNodeCeiling,
unknownCeiling,
fileUnknownCeiling,
])
if (
!Number.isSafeInteger(value) ||
value < 1 ||
value > JAVASCRIPT_SEMANTIC_NODE_MAXIMUM
value > JAVASCRIPT_SEMANTIC_ITEM_MAXIMUM
)
throw new RangeError("Semantic graph requires valid node capacities");
throw new RangeError("Semantic graph requires valid retained capacities");
const state = emptyState();
const fingerprints: JavaScriptSemanticFingerprint[] = [];
let truncatedFiles = 0;
@@ -233,12 +253,17 @@ export const createJavaScriptSemanticGraphProjection = (
const remainingTreeBudget = nodeCeiling - state.nodes.size;
state.fileNodeBudget = Math.min(fileNodeCeiling, remainingTreeBudget);
state.fileNodesDropped = false;
state.unknownCapacity = Math.min(
unknownCeiling,
state.unknowns.size + fileUnknownCeiling,
);
fingerprints.push(...(yield* projectFileSteps(file, ir, state, paths)));
// Nodes were dropped only if the budget actually blocked creation. A file
// that exactly fills its share ends with a zero budget without dropping
// anything, so the remaining budget alone cannot decide truncation.
if (state.fileNodesDropped) truncatedFiles += 1;
state.fileNodeBudget = null;
state.unknownCapacity = null;
};
const finishWith = <Value>(
factory: (input: unknown) => Value,
@@ -249,6 +274,8 @@ export const createJavaScriptSemanticGraphProjection = (
if (state.roots.size === 0) addFallbackRoot(rootArtifactSha256, state);
const relations = [...state.relations.values()];
const unknowns = [...state.unknowns.values()];
const unknownOmissions = [...state.unknownOmissions.values()];
const unknownsDropped = unknownOmissions.length > 0;
type Family = (typeof JAVASCRIPT_SEMANTIC_RELATION_FAMILIES)[number];
const retainedByFamily = new Map<Family, number>();
const unknownIdsByFamily = new Map<Family, string[]>();
@@ -275,13 +302,33 @@ export const createJavaScriptSemanticGraphProjection = (
unknowns,
coverage: {
status:
truncatedFiles > 0 || unavailableFiles > 0 ? "partial" : "unknown",
truncated: truncatedFiles > 0 || resourceTruncatedFiles > 0,
truncatedFiles > 0 || unavailableFiles > 0 || unknownsDropped
? "partial"
: "unknown",
truncated:
truncatedFiles > 0 || resourceTruncatedFiles > 0 || unknownsDropped,
...(unknownsDropped
? { omitted_unknown_occurrences: unknownOmissions }
: {}),
omitted_nodes: truncatedFiles > 0 || unavailableFiles > 0 ? null : 0,
omitted_relations:
truncatedFiles > 0 || unavailableFiles > 0 ? null : 0,
limits: [
...executionLimits.values(),
...(unknownsDropped
? [
{
name: "semantic_graph_unknown_ceiling",
value: unknownCeiling,
unit: "items" as const,
},
{
name: "semantic_graph_file_unknown_ceiling",
value: fileUnknownCeiling,
unit: "items" as const,
},
]
: []),
...(truncatedFiles > 0
? [
{
@@ -307,6 +354,11 @@ export const createJavaScriptSemanticGraphProjection = (
})),
},
limitations: [
...(unknownsDropped
? [
"Semantic unknown retention reached an effective capacity. Omission coverage counts examined emission occurrences by family and reason, including repeats; it does not count unexamined source frontiers or distinct discarded identities.",
]
: []),
"The semantic graph contains static syntax observations and conservative relationship candidates; it does not claim runtime execution.",
"Local data flow does not claim control-flow-sensitive reaching definitions or arbitrary dynamic property resolution.",
"Promise ownership covers explicit unshadowed Promise construction, static factories, aggregation, chaining, and await syntax only.",
@@ -323,14 +375,23 @@ export const createJavaScriptSemanticGraphProjection = (
state.nodes.clear();
state.relations.clear();
state.unknowns.clear();
state.unknownOmissions.clear();
state.roots.clear();
state.evidenceContexts.clear();
fingerprints.length = 0;
return graph;
};
return {
remainingFileNodeBudget: () =>
Math.min(fileNodeCeiling, Math.max(0, nodeCeiling - state.nodes.size)),
remainingFileCapacity: () => ({
nodes: Math.min(
fileNodeCeiling,
Math.max(0, nodeCeiling - state.nodes.size),
),
unknowns: Math.min(
fileUnknownCeiling,
Math.max(0, unknownCeiling - state.unknowns.size),
),
}),
adoptFileProjection: (projection) => {
const newNodes = projection.nodes.filter(
({ node_id }) => !state.nodes.has(node_id),
@@ -342,16 +403,28 @@ export const createJavaScriptSemanticGraphProjection = (
throw new RangeError(
"Transferred semantic nodes exceed the offered tree capacity",
);
const newUnknowns = projection.unknowns.filter(
({ unknown_id }) => !state.unknowns.has(unknown_id),
);
if (
newUnknowns.length >
Math.min(fileUnknownCeiling, unknownCeiling - state.unknowns.size)
)
throw new RangeError(
"Transferred semantic unknowns exceed the offered tree capacity",
);
for (const omission of projection.omittedUnknownOccurrences ?? [])
recordSemanticUnknownOmission(state, omission);
for (const context of projection.evidenceContexts)
state.evidenceContexts.adopt(context);
for (const node of newNodes) state.nodes.set(node.node_id, node);
for (const relation of projection.relations)
state.relations.set(relation.relation_id, relation);
for (const unknown of projection.unknowns)
for (const unknown of newUnknowns)
state.unknowns.set(unknown.unknown_id, unknown);
for (const root of projection.roots) state.roots.add(root);
fingerprints.push(...projection.fingerprints);
if (projection.truncated) truncatedFiles += 1;
if (projection.nodesTruncated) truncatedFiles += 1;
},
recordUnavailableFile: (file, reason, limits, unknownReason) => {
// Both full and incremental construction enter here. Metadata/native
@@ -385,24 +458,25 @@ export const createJavaScriptSemanticGraphProjection = (
coverage: partialApplicationCoverage(retainedLimits, null),
limitations: [reason],
};
state.unknownCapacity = Math.min(
unknownCeiling,
state.unknowns.size + fileUnknownCeiling,
);
for (const family of JAVASCRIPT_SEMANTIC_RELATION_FAMILIES) {
const unknown = createJavaScriptSemanticGraphUnknown(
{
node_id: module?.node_id ?? null,
family,
relation_kinds: JAVASCRIPT_SEMANTIC_RELATIONS.filter(
(relation) =>
JAVASCRIPT_SEMANTIC_RELATION_FAMILY[relation] === family,
),
reason: unknownReason,
detail: reason,
candidate_node_ids: [],
evidence,
},
state.evidenceContexts,
);
state.unknowns.set(unknown.unknown_id, unknown);
addSemanticGraphUnknown(state, {
node_id: module?.node_id ?? null,
family,
relation_kinds: JAVASCRIPT_SEMANTIC_RELATIONS.filter(
(relation) =>
JAVASCRIPT_SEMANTIC_RELATION_FAMILY[relation] === family,
),
reason: unknownReason,
detail: reason,
candidate_node_ids: [],
evidence,
});
}
state.unknownCapacity = null;
unavailableFiles += 1;
if (unknownReason === "resource-limit" && retainedLimits.length > 0)
resourceTruncatedFiles += 1;
@@ -427,23 +501,27 @@ export const createJavaScriptSemanticGraphProjection = (
};
};
/** Project one source with the parent's remaining node capacity, retaining no full IR. */
/** Project one source with the parent's remaining retained capacities, retaining no full IR. */
export function* projectJavaScriptSemanticFileSteps(
file: JavaScriptArtifactFile,
ir: JavaScriptSemanticIr,
nodeBudget: number,
capacity: JavaScriptSemanticProjectionCapacity,
paths: readonly ElectronCallPath[] = [],
): Generator<void, JavaScriptSemanticFileProjection> {
if (
!Number.isSafeInteger(nodeBudget) ||
nodeBudget < 0 ||
nodeBudget > JAVASCRIPT_SEMANTIC_NODE_MAXIMUM
[capacity.nodes, capacity.unknowns].some(
(value) =>
!Number.isSafeInteger(value) ||
value < 0 ||
value > JAVASCRIPT_SEMANTIC_ITEM_MAXIMUM,
)
)
throw new RangeError(
"File semantic projection requires a valid offered node capacity",
"File semantic projection requires valid offered retained capacities",
);
const state = emptyState();
state.fileNodeBudget = nodeBudget;
state.fileNodeBudget = capacity.nodes;
state.unknownCapacity = capacity.unknowns;
state.fileNodesDropped = false;
const fingerprints = yield* projectFileSteps(file, ir, state, paths);
return {
@@ -453,7 +531,10 @@ export function* projectJavaScriptSemanticFileSteps(
relations: [...state.relations.values()],
unknowns: [...state.unknowns.values()],
fingerprints,
truncated: Boolean(state.fileNodesDropped),
...(state.unknownOmissions.size > 0
? { omittedUnknownOccurrences: [...state.unknownOmissions.values()] }
: {}),
nodesTruncated: Boolean(state.fileNodesDropped),
};
}
@@ -569,26 +650,22 @@ const projectFileResourceLimits = (
// already project from binding or return-site values and must not be duplicated.
for (const limit of semanticCoverageResourceLimits(context.ir.coverage)) {
if (limit !== "getter-expansion") continue;
const unknown = createJavaScriptSemanticGraphUnknown(
{
node_id: context.moduleNode.node_id,
family: "data-flow",
relation_kinds: ["defines"],
reason: "resource-limit",
detail: semanticResourceLimitReason(limit),
candidate_node_ids: [],
evidence: {
...unknownSemanticEvidence(context.file, null),
coverage: partialApplicationCoverage(
semanticResourceLimitCoverage([limit]),
null,
),
limitations: [semanticResourceLimitReason(limit)],
},
addSemanticGraphUnknown(context.state, {
node_id: context.moduleNode.node_id,
family: "data-flow",
relation_kinds: ["defines"],
reason: "resource-limit",
detail: semanticResourceLimitReason(limit),
candidate_node_ids: [],
evidence: {
...unknownSemanticEvidence(context.file, null),
coverage: partialApplicationCoverage(
semanticResourceLimitCoverage([limit]),
null,
),
limitations: [semanticResourceLimitReason(limit)],
},
context.state.evidenceContexts,
);
context.state.unknowns.set(unknown.unknown_id, unknown);
});
}
};
@@ -1,4 +1,3 @@
import { createJavaScriptSemanticGraphUnknown } from "../../domain/javascript/javascriptSemanticGraph.js";
import type { JavaScriptSemanticGraphNode } from "../../domain/javascript/javascriptSemanticGraphSchemas.js";
import type {
JavaScriptSemanticChildProcessInteraction,
@@ -233,19 +232,13 @@ const addSignalNode = (
const addChildUnknown = (input: ChildUnknownInput): void => {
const { context, node, location, relationKinds, detail, candidateNodeIds } =
input;
addSemanticGraphUnknown(
context.state,
createJavaScriptSemanticGraphUnknown(
{
node_id: node?.node_id ?? null,
family: "child-process",
relation_kinds: relationKinds,
reason: input.reason ?? "ambiguous-target",
detail,
candidate_node_ids: candidateNodeIds,
evidence: unknownSemanticEvidence(context.file, location),
},
context.state.evidenceContexts,
),
);
addSemanticGraphUnknown(context.state, {
node_id: node?.node_id ?? null,
family: "child-process",
relation_kinds: relationKinds,
reason: input.reason ?? "ambiguous-target",
detail,
candidate_node_ids: candidateNodeIds,
evidence: unknownSemanticEvidence(context.file, location),
});
};
@@ -1,8 +1,10 @@
import type { z } from "zod";
import type { JavaScriptApplicationGraph } from "../../domain/javascript/javascriptApplicationGraph.js";
import type { JsonValue } from "../../domain/jsonValue.js";
import { javascriptDisplayText } from "../../domain/javascript/javascriptAstValues.js";
import {
createJavaScriptSemanticGraphNode,
createJavaScriptSemanticGraphUnknown,
createJavaScriptSemanticGraphRelation,
JavaScriptSemanticEvidenceContextRegistry,
javaScriptSemanticNodeId,
@@ -12,6 +14,8 @@ import type { ApplicationGraphEvidence } from "../../domain/javascript/javascrip
import type {
JavaScriptSemanticGraphRelation,
JavaScriptSemanticGraphUnknown,
JavaScriptSemanticUnknownOmission,
javaScriptSemanticUnknownInputSchema,
} from "../../domain/javascript/javascriptSemanticGraphSchemas.js";
import type { JavaScriptSourceRange } from "../../domain/javascript/javascriptStaticAnalysisTypes.js";
import type { JavaScriptArtifactFile } from "../../domain/javascript/javascriptArtifactFiles.js";
@@ -26,6 +30,9 @@ export interface SemanticGraphProjectionState {
readonly nodes: Map<string, JavaScriptSemanticGraphNode>;
readonly relations: Map<string, JavaScriptSemanticGraphRelation>;
readonly unknowns: Map<string, JavaScriptSemanticGraphUnknown>;
/** Absolute retained unknown count allowed during this file; null outside admission. */
unknownCapacity: number | null;
readonly unknownOmissions: Map<string, JavaScriptSemanticUnknownOmission>;
readonly evidenceContexts: JavaScriptSemanticEvidenceContextRegistry;
readonly roots: Set<string>;
/**
@@ -74,6 +81,8 @@ export const createSemanticGraphProjectionState =
nodes: new Map(),
relations: new Map(),
unknowns: new Map(),
unknownCapacity: null,
unknownOmissions: new Map(),
evidenceContexts: new JavaScriptSemanticEvidenceContextRegistry(),
roots: new Set(),
});
@@ -204,11 +213,45 @@ export const addSemanticGraphRelation = (
state.relations.set(value.relation_id, value);
};
/** Retain one unresolved frontier. */
/** Aggregate examined omissions without retaining unbounded discarded identities. */
export const recordSemanticUnknownOmission = (
state: SemanticGraphProjectionState,
omission: JavaScriptSemanticUnknownOmission,
): void => {
const key = `${omission.family}:${omission.reason}`;
const occurrences =
(state.unknownOmissions.get(key)?.occurrences ?? 0) + omission.occurrences;
if (!Number.isSafeInteger(occurrences))
throw new RangeError("Semantic unknown omission count overflow");
state.unknownOmissions.set(key, { ...omission, occurrences });
};
/** Admit unknown provenance only while its independent retained capacity allows it. */
export const addSemanticGraphUnknown = (
state: SemanticGraphProjectionState,
unknown: JavaScriptSemanticGraphUnknown,
input: Omit<
z.input<typeof javaScriptSemanticUnknownInputSchema>,
"relation_kinds" | "candidate_node_ids"
> & {
readonly relation_kinds: readonly JavaScriptSemanticGraphRelation["relation"][];
readonly candidate_node_ids: readonly string[];
},
): void => {
if (
state.unknownCapacity !== null &&
state.unknowns.size >= state.unknownCapacity
) {
recordSemanticUnknownOmission(state, {
family: input.family,
reason: input.reason,
occurrences: 1,
});
return;
}
const unknown = createJavaScriptSemanticGraphUnknown(
input,
state.evidenceContexts,
);
if (state.unknowns.has(unknown.unknown_id)) return;
state.unknowns.set(unknown.unknown_id, unknown);
};
@@ -1,4 +1,3 @@
import { createJavaScriptSemanticGraphUnknown } from "../../domain/javascript/javascriptSemanticGraph.js";
import type { JavaScriptSemanticGraphNode } from "../../domain/javascript/javascriptSemanticGraphSchemas.js";
import type {
JavaScriptSemanticBoundaryOperation,
@@ -325,19 +324,13 @@ const addDataUnknown = (input: DataUnknownInput): void => {
detail,
candidateNodeIds,
} = input;
addSemanticGraphUnknown(
context.state,
createJavaScriptSemanticGraphUnknown(
{
node_id: node.node_id,
family,
relation_kinds: relationKinds,
reason: input.reason ?? "ambiguous-target",
detail,
candidate_node_ids: candidateNodeIds,
evidence: unknownSemanticEvidence(context.file, location),
},
context.state.evidenceContexts,
),
);
addSemanticGraphUnknown(context.state, {
node_id: node.node_id,
family,
relation_kinds: relationKinds,
reason: input.reason ?? "ambiguous-target",
detail,
candidate_node_ids: candidateNodeIds,
evidence: unknownSemanticEvidence(context.file, location),
});
};
@@ -24,7 +24,7 @@ const project = (budget: number) => {
const steps = projectJavaScriptSemanticFileSteps(
file,
ir,
budget,
{ nodes: budget, unknowns: budget },
facts.electron.call_paths,
);
let result = steps.next();
@@ -35,7 +35,7 @@ const project = (budget: number) => {
it("retains Electron calls before library definitions consume the node budget", () => {
const result = project(20);
expect(result.nodes).toHaveLength(20);
expect(result.truncated).toBe(true);
expect(result.nodesTruncated).toBe(true);
const terminal = result.nodes.find(
(node) => node.label === "ipc-event.sender.copy",
);
@@ -59,7 +59,7 @@ it("retains Electron calls before library definitions consume the node budget",
it("reports truncation and keeps referential integrity even within a boundary path", () => {
const result = project(3);
expect(result.nodes).toHaveLength(3);
expect(result.truncated).toBe(true);
expect(result.nodesTruncated).toBe(true);
expect(
result.nodes.some((node) => node.label === "ipc-event.sender.copy"),
).toBe(false);
@@ -1,4 +1,3 @@
import { createJavaScriptSemanticGraphUnknown } from "../../domain/javascript/javascriptSemanticGraph.js";
import type { JavaScriptSemanticGraphNode } from "../../domain/javascript/javascriptSemanticGraphSchemas.js";
import type { JavaScriptSemanticIr } from "../../domain/javascript/javascriptSemanticIr.js";
import { sourceRangesEqual } from "../../domain/javascript/javascriptStaticAnalysisHelpers.js";
@@ -177,24 +176,18 @@ const addPromiseUnknown = (
promiseNodes: ReadonlyMap<string, JavaScriptSemanticGraphNode>,
): void => {
const relation = operation.kind === "aggregate" ? "aggregates" : "chains";
addSemanticGraphUnknown(
context.state,
createJavaScriptSemanticGraphUnknown(
{
node_id: promise.node_id,
family: "promise-ownership",
relation_kinds: [relation],
reason: "ambiguous-target",
detail: `Static ${operation.method} source resolution was ${operation.sourceResolution}.`,
candidate_node_ids: operation.sourcePromiseIds.flatMap((identifier) => {
const candidate = promiseNodes.get(identifier);
return candidate === undefined ? [] : [candidate.node_id];
}),
evidence: unknownSemanticEvidence(context.file, operation.location),
},
context.state.evidenceContexts,
),
);
addSemanticGraphUnknown(context.state, {
node_id: promise.node_id,
family: "promise-ownership",
relation_kinds: [relation],
reason: "ambiguous-target",
detail: `Static ${operation.method} source resolution was ${operation.sourceResolution}.`,
candidate_node_ids: operation.sourcePromiseIds.flatMap((identifier) => {
const candidate = promiseNodes.get(identifier);
return candidate === undefined ? [] : [candidate.node_id];
}),
evidence: unknownSemanticEvidence(context.file, operation.location),
});
};
/** Link direct return expressions to their retained semantic references. */
@@ -258,22 +251,18 @@ export function* projectSemanticFrontiers(
: frontier.kind === "dynamic-scope"
? (["reads", "writes"] as const)
: (["reads-property", "writes-property"] as const);
const unknown = createJavaScriptSemanticGraphUnknown(
{
node_id:
frontier.callableId === null
? context.moduleNode.node_id
: (context.callableNodes.get(frontier.callableId)?.node_id ??
context.moduleNode.node_id),
family,
relation_kinds: [...relationKinds],
reason: frontier.kind,
detail: frontier.reason,
candidate_node_ids: [],
evidence: unknownSemanticEvidence(context.file, frontier.location),
},
context.state.evidenceContexts,
);
addSemanticGraphUnknown(context.state, unknown);
addSemanticGraphUnknown(context.state, {
node_id:
frontier.callableId === null
? context.moduleNode.node_id
: (context.callableNodes.get(frontier.callableId)?.node_id ??
context.moduleNode.node_id),
family,
relation_kinds: [...relationKinds],
reason: frontier.kind,
detail: frontier.reason,
candidate_node_ids: [],
evidence: unknownSemanticEvidence(context.file, frontier.location),
});
}
}
@@ -7,7 +7,6 @@ import type {
JavaScriptSemanticSlotPresence,
JavaScriptSemanticValue,
} from "../../domain/javascript/javascriptSemanticValueTypes.js";
import { createJavaScriptSemanticGraphUnknown } from "../../domain/javascript/javascriptSemanticGraph.js";
import type { JavaScriptSemanticGraphNode } from "../../domain/javascript/javascriptSemanticGraphSchemas.js";
import type { JavaScriptSemanticObjectOperation } from "../../domain/javascript/javascriptSemanticIr.js";
import {
@@ -167,19 +166,13 @@ const addObjectUnknown = (
target: Pick<ObjectTarget, "presence" | "receiverResolved">,
): void => {
const relation = OBJECT_RELATIONS[operation.kind];
addSemanticGraphUnknown(
context.state,
createJavaScriptSemanticGraphUnknown(
{
node_id: node?.node_id ?? null,
family: "object-flow",
relation_kinds: [relation],
reason: "ambiguous-target",
detail: `Static ${operation.kind} property ${operation.propertyPath === null ? "path is unresolved" : semanticPropertyPointer(operation.propertyPath)} has ${target.presence} presence; its receiver is ${target.receiverResolved ? "a retained container" : "unresolved or not a retained container"}; object identity is ${operation.resolution}.`,
candidate_node_ids: [],
evidence: unknownSemanticEvidence(context.file, operation.location),
},
context.state.evidenceContexts,
),
);
addSemanticGraphUnknown(context.state, {
node_id: node?.node_id ?? null,
family: "object-flow",
relation_kinds: [relation],
reason: "ambiguous-target",
detail: `Static ${operation.kind} property ${operation.propertyPath === null ? "path is unresolved" : semanticPropertyPointer(operation.propertyPath)} has ${target.presence} presence; its receiver is ${target.receiverResolved ? "a retained container" : "unresolved or not a retained container"}; object identity is ${operation.resolution}.`,
candidate_node_ids: [],
evidence: unknownSemanticEvidence(context.file, operation.location),
});
};
@@ -1,4 +1,3 @@
import { createJavaScriptSemanticGraphUnknown } from "../../domain/javascript/javascriptSemanticGraph.js";
import type { JavaScriptSemanticGraphNode } from "../../domain/javascript/javascriptSemanticGraphSchemas.js";
import type { JavaScriptSemanticResourceOperation } from "../../domain/javascript/javascriptSemanticIr.js";
import {
@@ -67,21 +66,13 @@ const projectRelease = (
operation.resolution === "complete" ? "resolved" : "candidate",
});
if (operation.resolution === "complete") return;
addSemanticGraphUnknown(
context.state,
createJavaScriptSemanticGraphUnknown(
{
node_id: callSite?.node_id ?? null,
family: "resource-lifecycle",
relation_kinds: ["releases"],
reason: "ambiguous-target",
detail: `Static ${operation.method} resource ownership is ambiguous.`,
candidate_node_ids: resources.map(
({ node_id: identifier }) => identifier,
),
evidence: unknownSemanticEvidence(context.file, operation.location),
},
context.state.evidenceContexts,
),
);
addSemanticGraphUnknown(context.state, {
node_id: callSite?.node_id ?? null,
family: "resource-lifecycle",
relation_kinds: ["releases"],
reason: "ambiguous-target",
detail: `Static ${operation.method} resource ownership is ambiguous.`,
candidate_node_ids: resources.map(({ node_id: identifier }) => identifier),
evidence: unknownSemanticEvidence(context.file, operation.location),
});
};
@@ -8,7 +8,6 @@ import type {
JavaScriptSemanticProperty,
JavaScriptSemanticValue,
} from "../../domain/javascript/javascriptSemanticValueTypes.js";
import { createJavaScriptSemanticGraphUnknown } from "../../domain/javascript/javascriptSemanticGraph.js";
import {
retainSemanticGraphNode,
addSemanticGraphRelation,
@@ -95,26 +94,20 @@ const projectValue = (input: ValueProjectionInput): void => {
});
}
if (container.coverage.status === "partial")
addSemanticGraphUnknown(
context.state,
createJavaScriptSemanticGraphUnknown(
{
node_id: target.node_id,
family: "object-flow",
relation_kinds: ["writes-property"],
reason: "ambiguous-target",
detail: `Initializer container has unknown ${
value.status === "object" ? "properties" : "items"
}${container.coverage.omitted === null ? " with an unknown omitted count" : `; ${container.coverage.omitted} omitted`}.`,
candidate_node_ids: [target.node_id],
evidence: unknownSemanticEvidence(
context.file,
binding.definitions[0]?.location ?? null,
),
},
context.state.evidenceContexts,
addSemanticGraphUnknown(context.state, {
node_id: target.node_id,
family: "object-flow",
relation_kinds: ["writes-property"],
reason: "ambiguous-target",
detail: `Initializer container has unknown ${
value.status === "object" ? "properties" : "items"
}${container.coverage.omitted === null ? " with an unknown omitted count" : `; ${container.coverage.omitted} omitted`}.`,
candidate_node_ids: [target.node_id],
evidence: unknownSemanticEvidence(
context.file,
binding.definitions[0]?.location ?? null,
),
);
});
} else if (
value.status === "unknown" ||
value.status === "ambiguous" ||
@@ -122,30 +115,22 @@ const projectValue = (input: ValueProjectionInput): void => {
) {
const location = binding.definitions[0]?.location ?? null;
const evidence = observedSemanticEvidence(context.file, location);
addSemanticGraphUnknown(
context.state,
createJavaScriptSemanticGraphUnknown(
{
node_id: target.node_id,
family: "data-flow",
relation_kinds: ["defines"],
reason:
value.resourceLimit === undefined
? "unknown-value"
: "resource-limit",
detail: `${value.reason} Unknown value at ${role}.`,
candidate_node_ids: [target.node_id],
evidence: {
...evidence,
authority: "unknown",
state: "unknown",
confidence: "unknown",
limitations: [value.reason],
},
},
context.state.evidenceContexts,
),
);
addSemanticGraphUnknown(context.state, {
node_id: target.node_id,
family: "data-flow",
relation_kinds: ["defines"],
reason:
value.resourceLimit === undefined ? "unknown-value" : "resource-limit",
detail: `${value.reason} Unknown value at ${role}.`,
candidate_node_ids: [target.node_id],
evidence: {
...evidence,
authority: "unknown",
state: "unknown",
confidence: "unknown",
limitations: [value.reason],
},
});
}
};
@@ -0,0 +1,243 @@
import { createHash } from "node:crypto";
import { expect, it } from "vitest";
import type { JavaScriptArtifactFile } from "../../domain/javascript/javascriptArtifactFiles.js";
import { analyzeJavaScriptSemantics } from "../../domain/javascript/javascriptSemanticAnalysis.js";
import {
javaScriptSemanticGraphSchema,
createJavaScriptSemanticGraph,
} from "../../domain/javascript/javascriptSemanticGraph.js";
import { queryJavaScriptSemanticGraph } from "../../domain/javascript/javascriptSemanticQuery.js";
import {
createJavaScriptSemanticGraphProjection,
projectJavaScriptSemanticFileSteps,
} from "./JavaScriptSemanticGraphBuilder.js";
import {
createSemanticGraphProjectionState,
addSemanticGraphUnknown,
} from "./JavaScriptSemanticGraphConstruction.js";
import { unknownSemanticEvidence } from "./JavaScriptSemanticGraphEvidence.js";
const digest = "a".repeat(64);
const application = { graph_id: `jag_${"b".repeat(64)}`, nodes: [] };
const text = "globalThis.u.p(globalThis.v[k]);\n".repeat(4);
const source = (path = "app.js", value = text): JavaScriptArtifactFile => ({
path,
sha256: createHash("sha256").update(value).digest("hex"),
container_sha256: digest,
bytes: Buffer.byteLength(value),
inventory_artifact_id: `art_${digest}`,
kind: "javascript",
unpacked: false,
text: { included: true, value },
});
const fileProjection = (file = source(), nodes = 200, unknowns = 200) => {
if (!file.text.included) throw new Error("Expected inert source");
const steps = projectJavaScriptSemanticFileSteps(
file,
analyzeJavaScriptSemantics(file.text.value),
{ nodes, unknowns },
);
let step = steps.next();
while (!step.done) step = steps.next();
return step.value;
};
it("bounds unknowns independently of retained nodes and accounts for examined omissions", () => {
const complete = fileProjection();
const bounded = fileProjection(source(), 200, 2);
expect(complete.unknowns.length).toBeGreaterThan(2);
expect(bounded.nodes).toEqual(complete.nodes);
expect(bounded.relations).toEqual(complete.relations);
expect(bounded.unknowns).toHaveLength(2);
expect(
bounded.omittedUnknownOccurrences!.reduce(
(sum, row) => sum + row.occurrences,
0,
),
).toBe(complete.unknowns.length - 2);
const projection = createJavaScriptSemanticGraphProjection({
totalUnknowns: 2,
fileUnknowns: 2,
});
projection.adoptFileProjection(bounded);
const graph = projection.finish(digest, application);
expect(graph.coverage).toMatchObject({
truncated: true,
status: "partial",
omitted_nodes: 0,
});
expect(graph.coverage.limits).toEqual(
expect.arrayContaining([
{ name: "semantic_graph_unknown_ceiling", value: 2, unit: "items" },
{ name: "semantic_graph_file_unknown_ceiling", value: 2, unit: "items" },
]),
);
expect(javaScriptSemanticGraphSchema.parse(graph)).toEqual(graph);
expect(
queryJavaScriptSemanticGraph(graph, {
seed: { kind: "property", name: "missing" },
direction: "backward-provenance",
}),
).toMatchObject({ status: "partial", coverage: { status: "partial" } });
});
it("keeps unknowns bounded even after their semantic nodes are dropped", () => {
const projection = fileProjection(source(), 1, 2);
expect(projection.nodesTruncated).toBe(true);
expect(projection.nodes).toHaveLength(1);
expect(projection.unknowns).toHaveLength(2);
expect(projection.omittedUnknownOccurrences!.length).toBeGreaterThan(0);
});
it("enforces per-file and total unknown capacities across transferred files", () => {
const projection = createJavaScriptSemanticGraphProjection({
totalUnknowns: 3,
fileUnknowns: 2,
});
for (const path of ["a.js", "b.js", "c.js"]) {
const capacity = projection.remainingFileCapacity();
const file = source(path);
const steps = projectJavaScriptSemanticFileSteps(
file,
analyzeJavaScriptSemantics(text),
capacity,
);
let step = steps.next();
while (!step.done) step = steps.next();
projection.adoptFileProjection(step.value);
}
expect(projection.remainingFileCapacity().unknowns).toBe(0);
const graph = projection.finish(digest, application);
expect(graph.unknowns).toHaveLength(3);
expect(
graph.unknowns.filter(
(row) =>
row.evidence.location.available &&
row.evidence.location.value.kind === "source-range" &&
row.evidence.location.value.source === "a.js",
),
).toHaveLength(2);
expect(
graph.coverage.omitted_unknown_occurrences!.reduce(
(sum, row) => sum + row.occurrences,
0,
),
).toBe(fileProjection().unknowns.length * 3 - 3);
});
it("rejects excess transferred unknowns before adopting any file facts", () => {
const projection = createJavaScriptSemanticGraphProjection({
totalUnknowns: 1,
fileUnknowns: 1,
});
const before = projection.remainingFileCapacity();
expect(() => projection.adoptFileProjection(fileProjection())).toThrow(
"Transferred semantic unknowns exceed",
);
expect(projection.remainingFileCapacity()).toEqual(before);
});
it("bounds unavailable-file frontiers through the same owner", () => {
const projection = createJavaScriptSemanticGraphProjection({
totalUnknowns: 2,
fileUnknowns: 2,
});
projection.recordUnavailableFile(
source(),
"Source unavailable",
null,
"incomplete-module",
);
const graph = projection.finish(digest, application);
expect(graph.unknowns).toHaveLength(2);
expect(
graph.coverage.omitted_unknown_occurrences!.reduce(
(sum, row) => sum + row.occurrences,
0,
),
).toBe(10);
});
it("does not intern omitted provenance and counts repeated omitted emissions as occurrences", () => {
const state = createSemanticGraphProjectionState();
state.unknownCapacity = 1;
const file = source();
for (const detail of ["retained", "omitted", "omitted"])
addSemanticGraphUnknown(state, {
node_id: null,
family: "data-flow",
relation_kinds: ["defines"],
reason: "unknown-value",
detail,
candidate_node_ids: [],
evidence: {
...unknownSemanticEvidence(file, null),
limitations: [detail],
},
});
expect(state.unknowns.size).toBe(1);
expect(state.evidenceContexts.contexts).toHaveLength(1);
expect([...state.unknownOmissions.values()]).toEqual([
{ family: "data-flow", reason: "unknown-value", occurrences: 2 },
]);
});
it("preserves omission-free graph identity and refuses contradictory omission coverage", () => {
const projection = createJavaScriptSemanticGraphProjection();
projection.adoptFileProjection(fileProjection());
const graph = projection.finish(digest, application);
expect(Object.hasOwn(graph.coverage, "omitted_unknown_occurrences")).toBe(
false,
);
const { graph_id: _id, ...input } = graph;
expect(createJavaScriptSemanticGraph(input).graph_id).toBe(graph.graph_id);
const omission = {
family: "data-flow",
reason: "unknown-value",
occurrences: 1,
};
expect(() =>
createJavaScriptSemanticGraph({
...input,
coverage: { ...graph.coverage, omitted_unknown_occurrences: [omission] },
}),
).toThrow("Omitted unknown occurrences require");
expect(() =>
createJavaScriptSemanticGraph({
...input,
coverage: {
...graph.coverage,
status: "partial",
truncated: true,
limits: [{ name: "authored", value: 1, unit: "items" }],
omitted_unknown_occurrences: [omission, omission],
},
}),
).toThrow("Unknown omission coverage");
});
it("applies the same unknown admission in direct and transferred construction", () => {
const direct = createJavaScriptSemanticGraphProjection({
totalUnknowns: 3,
fileUnknowns: 2,
});
const transferred = createJavaScriptSemanticGraphProjection({
totalUnknowns: 3,
fileUnknowns: 2,
});
for (const name of ["a.js", "b.js"]) {
const file = source(name);
direct.projectFile(file, analyzeJavaScriptSemantics(text));
const steps = projectJavaScriptSemanticFileSteps(
file,
analyzeJavaScriptSemantics(text),
transferred.remainingFileCapacity(),
);
let step = steps.next();
while (!step.done) step = steps.next();
transferred.adoptFileProjection(step.value);
}
expect(direct.finish(digest, application)).toEqual(
transferred.finish(digest, application),
);
});