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 committed 2026-10-17 21:57:31 +08:00
1 parent 926e18b3fc
commit f4322b15ed
34 files changed
+892 -293

No files matched your search

@@ -586,6 +586,21 @@ not bypass worker heap, execution deadline or parent result admission. Truncated
coverage reports both effective node ceilings; omitted relationships remain
unknown.
Unknown records have independent defaults of 100000 across the application and
20000 per source. Select `max_semantic_unknowns` and
`max_semantic_unknowns_per_file` (CLI: `--max-semantic-unknowns` and
`--max-semantic-unknowns-per-file`), each from 1 to 1000000. The parent offers the
remaining capacity to the worker, and rejects excess records before adoption.
Unknown admission happens before canonical identity and provenance construction.
When capacity is exhausted, coverage is partial and truncated and names both
effective unknown ceilings. `coverage.omitted_unknown_occurrences` counts examined
emission occurrences by family and reason, including repeated emissions. It does
not claim distinct discarded identities or count frontiers in unexamined source.
Omitted unknown provenance and IDs are not retained. Existing source, node,
execution and memory limits continue to determine examined coverage. Raising only
a node capacity does not raise the unknown capacity. Graphs without unknown
omissions omit this optional coverage field, preserving their existing identities.
While sources remain, the sequential worker reserves one next compact static
checkpoint at its 8 MiB bound with the same 6x retention expansion used at
admission. Earlier checkpoints are already charged to retained bytes; the
@@ -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),
);
});
+9
View File
@@ -295,6 +295,15 @@ const registerJavaScriptApplicationCommand = (
max_semantic_nodes_per_file:
options.maxSemanticNodesPerFile,
}),
...(options.maxSemanticUnknowns === undefined
? {}
: { max_semantic_unknowns: options.maxSemanticUnknowns }),
...(options.maxSemanticUnknownsPerFile === undefined
? {}
: {
max_semantic_unknowns_per_file:
options.maxSemanticUnknownsPerFile,
}),
...(options.sourcePath === undefined
? {}
: { source_paths: options.sourcePath }),
+5
View File
@@ -65,6 +65,11 @@ export const javascriptApplicationOptions = z.object({
javaScriptAnalysisResourceControlsSchema.shape.max_semantic_nodes,
maxSemanticNodesPerFile:
javaScriptAnalysisResourceControlsSchema.shape.max_semantic_nodes_per_file,
maxSemanticUnknowns:
javaScriptAnalysisResourceControlsSchema.shape.max_semantic_unknowns,
maxSemanticUnknownsPerFile:
javaScriptAnalysisResourceControlsSchema.shape
.max_semantic_unknowns_per_file,
sourcePath: javaScriptSourcePathsSchema
.optional()
.describe(
@@ -18,7 +18,7 @@ import {
const analyze: JavaScriptSourceWorkerAnalyzer = async (
file,
nodeBudget,
capacity,
callbacks,
) => {
if (!file.text.included)
@@ -70,7 +70,7 @@ const analyze: JavaScriptSourceWorkerAnalyzer = async (
projectJavaScriptSemanticFileSteps(
file,
ir,
nodeBudget,
capacity,
facts.electron.call_paths,
),
);
@@ -7,13 +7,22 @@ it("keeps omitted operational choices absent and validates explicit semantic bud
javaScriptAnalysisResourceControlsSchema.parse({
max_semantic_nodes: 750_000,
max_semantic_nodes_per_file: 700_000,
max_semantic_unknowns: 500_000,
max_semantic_unknowns_per_file: 400_000,
}),
).toEqual({
max_semantic_nodes: 750_000,
max_semantic_nodes_per_file: 700_000,
max_semantic_unknowns: 500_000,
max_semantic_unknowns_per_file: 400_000,
});
for (const value of [0, -1, 1.5, 1_000_001, Infinity, NaN]) {
for (const key of ["max_semantic_nodes", "max_semantic_nodes_per_file"])
for (const key of [
"max_semantic_nodes",
"max_semantic_nodes_per_file",
"max_semantic_unknowns",
"max_semantic_unknowns_per_file",
])
expect(
javaScriptAnalysisResourceControlsSchema.safeParse({ [key]: value })
.success,
@@ -8,10 +8,12 @@ export const JAVASCRIPT_ANALYSIS_RESOURCE_DEFAULTS = {
timeoutMs: 300_000,
semanticNodes: 100_000,
semanticNodesPerFile: 20_000,
semanticUnknowns: 100_000,
semanticUnknownsPerFile: 20_000,
} as const;
/** Maximum caller-selected node capacity; heap and transfer admission still apply. */
export const JAVASCRIPT_SEMANTIC_NODE_MAXIMUM = 1_000_000;
/** Maximum caller-selected retained record capacity; heap and transfer admission still apply. */
export const JAVASCRIPT_SEMANTIC_ITEM_MAXIMUM = 1_000_000;
/** Project observed execution limits without inventing unmeasured values. */
export const javaScriptAnalysisCoverageLimits = (
@@ -46,7 +48,7 @@ export const javaScriptAnalysisResourceControlsSchema = z.strictObject({
.number()
.int()
.min(1)
.max(JAVASCRIPT_SEMANTIC_NODE_MAXIMUM)
.max(JAVASCRIPT_SEMANTIC_ITEM_MAXIMUM)
.optional()
.describe(
"Maximum retained semantic nodes across the application; defaults to 100000. Worker heap and parent result admission remain enforced.",
@@ -55,11 +57,29 @@ export const javaScriptAnalysisResourceControlsSchema = z.strictObject({
.number()
.int()
.min(1)
.max(JAVASCRIPT_SEMANTIC_NODE_MAXIMUM)
.max(JAVASCRIPT_SEMANTIC_ITEM_MAXIMUM)
.optional()
.describe(
"Maximum retained semantic nodes per source file; defaults to 20000, additionally bounded by max_semantic_nodes and memory/transfer limits.",
),
max_semantic_unknowns: z
.number()
.int()
.min(1)
.max(JAVASCRIPT_SEMANTIC_ITEM_MAXIMUM)
.optional()
.describe(
"Maximum retained semantic unknown records across the application; defaults to 100000. Omitted emission occurrences are counted by family and reason.",
),
max_semantic_unknowns_per_file: z
.number()
.int()
.min(1)
.max(JAVASCRIPT_SEMANTIC_ITEM_MAXIMUM)
.optional()
.describe(
"Maximum retained semantic unknown records per source file; defaults to 20000, additionally bounded by max_semantic_unknowns and memory/transfer limits.",
),
max_heap_mb: z
.number()
.int()
@@ -4,6 +4,7 @@ import type {
JavaScriptSemanticGraphNode,
JavaScriptSemanticGraphRelation,
JavaScriptSemanticGraphUnknown,
JavaScriptSemanticUnknownOmission,
} from "./javascriptSemanticGraphSchemas.js";
/** File-local facts transported before binding them to the containing application graph. */
@@ -13,6 +14,13 @@ export interface JavaScriptSemanticFileProjection {
readonly nodes: readonly JavaScriptSemanticGraphNode[];
readonly relations: readonly JavaScriptSemanticGraphRelation[];
readonly unknowns: readonly JavaScriptSemanticGraphUnknown[];
readonly omittedUnknownOccurrences?: readonly JavaScriptSemanticUnknownOmission[];
readonly fingerprints: readonly JavaScriptSemanticFingerprint[];
readonly truncated: boolean;
readonly nodesTruncated: boolean;
}
/** Independent retained capacities offered to one source projection. */
export interface JavaScriptSemanticProjectionCapacity {
readonly nodes: number;
readonly unknowns: number;
}
@@ -265,6 +265,22 @@ function* checkCoverageSteps(
graph: GraphRecord,
context: GraphIssueReporter,
): Generator<void> {
const omittedUnknowns = graph.coverage.omitted_unknown_occurrences ?? [];
sortedUniqueIssue(
omittedUnknowns.map(({ family, reason }) => `${family}:${reason}`),
["coverage", "omitted_unknown_occurrences"],
"Unknown omission coverage",
context,
);
if (
omittedUnknowns.length > 0 &&
(!graph.coverage.truncated || graph.coverage.status !== "partial")
)
context.addIssue({
code: "custom",
path: ["coverage"],
message: "Omitted unknown occurrences require truncated partial coverage",
});
const families = graph.coverage.families.map(({ family }) => family);
if (
canonicalJson(families, "JavaScript semantic graph") !==
@@ -333,7 +349,8 @@ function* checkCoverageSteps(
family.status === "complete" &&
(family.omitted_relations !== 0 ||
family.unknown_ids.length > 0 ||
unknownFamilies.has(family.family))
unknownFamilies.has(family.family) ||
omittedUnknowns.some((omitted) => omitted.family === family.family))
)
context.addIssue({
code: "custom",
@@ -759,6 +776,18 @@ const normalizeGraphInput = (
canonicalJson(right, "JavaScript semantic graph"),
),
),
...(parsed.coverage.omitted_unknown_occurrences === undefined
? {}
: {
omitted_unknown_occurrences: [
...parsed.coverage.omitted_unknown_occurrences,
].sort((left, right) =>
compareUnicodeCodePoints(
`${left.family}:${left.reason}`,
`${right.family}:${right.reason}`,
),
),
}),
families: [...parsed.coverage.families]
.map((family) => ({
...family,
@@ -271,6 +271,24 @@ export const JAVASCRIPT_SEMANTIC_UNKNOWN_REASONS = [
"unsupported-syntax",
] as const;
/** Counts examined emission occurrences, including repeats, without retaining discarded IDs. */
export const javaScriptSemanticUnknownOmissionSchema = z.strictObject({
family: z.enum(JAVASCRIPT_SEMANTIC_RELATION_FAMILIES),
reason: z.enum(JAVASCRIPT_SEMANTIC_UNKNOWN_REASONS),
occurrences: z.number().int().positive(),
});
/** At most one summary row for each finite family/reason pair. */
export const javaScriptSemanticUnknownOmissionsSchema = z
.array(javaScriptSemanticUnknownOmissionSchema)
.min(1)
.max(
JAVASCRIPT_SEMANTIC_RELATION_FAMILIES.length *
JAVASCRIPT_SEMANTIC_UNKNOWN_REASONS.length,
);
export type JavaScriptSemanticUnknownOmission = z.infer<
typeof javaScriptSemanticUnknownOmissionSchema
>;
/** One unresolved semantic frontier before its ID is derived. */
export const javaScriptSemanticUnknownInputSchema = z.strictObject({
node_id: semanticNodeIdSchema.nullable(),
@@ -337,6 +355,8 @@ const familyCoverageSchema = z.strictObject({
const graphCoverageSchema = z.strictObject({
status: z.enum(["complete", "partial", "unknown", "unavailable"]),
truncated: z.boolean(),
omitted_unknown_occurrences:
javaScriptSemanticUnknownOmissionsSchema.exactOptional(),
omitted_nodes: z.number().int().min(0).nullable(),
omitted_relations: z.number().int().min(0).nullable(),
limits: z.array(
@@ -1,3 +1,4 @@
import type { JavaScriptSemanticProjectionCapacity } from "./javascriptSemanticFileProjection.js";
import type { AnalysisError } from "../analysisErrorBase.js";
import type { AnalysisResourceConstraintError } from "../analysisErrorCore.js";
import type { Result } from "../result.js";
@@ -33,7 +34,7 @@ export interface JavaScriptSourceAnalysisLimits {
export interface JavaScriptSourceAnalysisPort {
analyze(
file: JavaScriptArtifactFile,
nodeBudget: number,
capacity: JavaScriptSemanticProjectionCapacity,
signal?: AbortSignal,
): Promise<Result<JavaScriptAnalyzedSource, JavaScriptSourceAnalysisFailure>>;
close(): Promise<void>;
@@ -6,7 +6,11 @@ import { AnalysisResourceConstraintError } from "../../domain/analysisErrorCore.
import type { JavaScriptArtifactFile } from "../../domain/javascript/javascriptArtifactFiles.js";
import type { JavaScriptModuleSemanticIr } from "../../domain/javascript/javascriptModuleSemanticIr.js";
import type { JavaScriptSemanticFileProjection } from "../../domain/javascript/javascriptSemanticFileProjection.js";
import { JAVASCRIPT_SEMANTIC_ITEM_MAXIMUM } from "../../domain/javascript/javascriptAnalysisResourceControls.js";
import type {
JavaScriptSemanticFileProjection,
JavaScriptSemanticProjectionCapacity,
} from "../../domain/javascript/javascriptSemanticFileProjection.js";
import { digestSchema } from "../../domain/digests.js";
import {
javaScriptWorkerGraphRecordSchema,
@@ -183,7 +187,15 @@ export const writeJavaScriptAnalysisTransfer = async (
try {
await append({
kind: "projection-header",
value: { roots: projection.roots, truncated: projection.truncated },
value: {
roots: projection.roots,
nodesTruncated: projection.nodesTruncated,
...(projection.omittedUnknownOccurrences === undefined
? {}
: {
omittedUnknownOccurrences: projection.omittedUnknownOccurrences,
}),
},
});
for (const record of moduleTransferRecords(module)) await append(record);
for (const value of projection.evidenceContexts)
@@ -226,7 +238,7 @@ export function readJavaScriptAnalysisTransfer(
descriptor: Descriptor,
source: Pick<JavaScriptArtifactFile, "path" | "sha256">,
signal: AbortSignal | undefined,
retainGraph: boolean,
capacity: JavaScriptSemanticProjectionCapacity | null,
): Promise<{
readonly module: JavaScriptModuleSemanticIr;
readonly projection: JavaScriptSemanticFileProjection | null;
@@ -236,11 +248,26 @@ export async function readJavaScriptAnalysisTransfer(
descriptor: Descriptor,
source: Pick<JavaScriptArtifactFile, "path" | "sha256">,
signal?: AbortSignal,
retainGraph = true,
capacity: JavaScriptSemanticProjectionCapacity | null = {
nodes: JAVASCRIPT_SEMANTIC_ITEM_MAXIMUM,
unknowns: JAVASCRIPT_SEMANTIC_ITEM_MAXIMUM,
},
): Promise<{
readonly module: JavaScriptModuleSemanticIr;
readonly projection: JavaScriptSemanticFileProjection | null;
}> {
if (
capacity !== null &&
[capacity.nodes, capacity.unknowns].some(
(value) =>
!Number.isSafeInteger(value) ||
value < 0 ||
value > JAVASCRIPT_SEMANTIC_ITEM_MAXIMUM,
)
)
throw new RangeError(
"JavaScript transfer requires valid retained capacities",
);
const expected = javaScriptAnalysisTransferDescriptorSchema.parse(descriptor);
if ((await stat(path)).size !== expected.bytes)
throw new TypeError(
@@ -340,7 +367,7 @@ export async function readJavaScriptAnalysisTransfer(
"JavaScript transfer facts precede their headers",
);
if (
!retainGraph &&
capacity === null &&
[
"evidence-context",
"node",
@@ -389,6 +416,10 @@ export async function readJavaScriptAnalysisTransfer(
throw new TypeError(
"JavaScript transfer node belongs to a different artifact source",
);
if (capacity !== null && nodes.length >= capacity.nodes)
throw new TypeError(
"JavaScript transfer nodes exceed the offered capacity",
);
nodes.push(record.value);
break;
}
@@ -398,6 +429,10 @@ export async function readJavaScriptAnalysisTransfer(
break;
case "unknown":
retainIdentity(record.kind, record.value.unknown_id);
if (capacity !== null && unknowns.length >= capacity.unknowns)
throw new TypeError(
"JavaScript transfer unknowns exceed the offered capacity",
);
unknowns.push(record.value);
break;
case "fingerprint":
@@ -433,15 +468,16 @@ export async function readJavaScriptAnalysisTransfer(
throw new TypeError("JavaScript analysis transfer is missing its headers");
return {
module: { ...moduleHeader, scopes, bindings, callables, moduleLinks },
projection: retainGraph
? {
...projectionHeader,
evidenceContexts,
nodes,
relations,
unknowns,
fingerprints,
}
: null,
projection:
capacity !== null
? {
...projectionHeader,
evidenceContexts,
nodes,
relations,
unknowns,
fingerprints,
}
: null,
};
}
@@ -1,3 +1,4 @@
import type { JavaScriptSemanticProjectionCapacity } from "../../domain/javascript/javascriptSemanticFileProjection.js";
import { createHash, randomUUID } from "node:crypto";
import { readFile, rm, stat, writeFile } from "node:fs/promises";
import { join } from "node:path";
@@ -154,7 +155,7 @@ export class JavaScriptAnalysisWorker {
/** Analyze inert source; hard worker failure does not terminate this process. */
async analyze(
file: JavaScriptArtifactFile,
nodeBudget: number,
capacity: JavaScriptSemanticProjectionCapacity,
signal?: AbortSignal,
): Promise<
Result<JavaScriptAnalyzedSource, JavaScriptSourceAnalysisFailure>
@@ -203,7 +204,8 @@ export class JavaScriptAnalysisWorker {
let offeredResultBudget = 0;
const request = javaScriptWorkerRequestSchema.parse({
id,
node_budget: nodeBudget,
node_budget: capacity.nodes,
unknown_budget: capacity.unknowns,
text_sha256: createHash("sha256").update(file.text.value).digest("hex"),
file: metadata,
});
@@ -344,7 +346,7 @@ export class JavaScriptAnalysisWorker {
descriptor,
file,
deadline.signal,
retainGraph,
retainGraph ? capacity : null,
);
module = decoded.module;
projection = decoded.projection;
@@ -4,7 +4,7 @@ import { jsonValueSchema } from "../../domain/jsonValue.js";
import { digestSchema } from "../../domain/digests.js";
import {
JAVASCRIPT_ANALYSIS_RESOURCE_DEFAULTS,
JAVASCRIPT_SEMANTIC_NODE_MAXIMUM,
JAVASCRIPT_SEMANTIC_ITEM_MAXIMUM,
} from "../../domain/javascript/javascriptAnalysisResourceControls.js";
import { javaScriptAnalysisTransferDescriptorSchema } from "./JavaScriptAnalysisTransfer.js";
@@ -18,7 +18,8 @@ export const JAVASCRIPT_ANALYSIS_WORKER_DEFAULTS = {
/** Caller-selected artifact identity, separate from the decoded source-text digest. */
export const javaScriptWorkerRequestSchema = z.strictObject({
id: z.uuid(),
node_budget: z.number().int().min(0).max(JAVASCRIPT_SEMANTIC_NODE_MAXIMUM),
unknown_budget: z.number().int().min(0).max(JAVASCRIPT_SEMANTIC_ITEM_MAXIMUM),
node_budget: z.number().int().min(0).max(JAVASCRIPT_SEMANTIC_ITEM_MAXIMUM),
text_sha256: digestSchema,
file: z.strictObject({
path: z.string(),
@@ -1,3 +1,4 @@
import { javaScriptSemanticUnknownOmissionsSchema } from "../../domain/javascript/javascriptSemanticGraphSchemas.js";
import { z } from "zod";
import { digestSchema } from "../../domain/digests.js";
import type { JavaScriptStaticAnalysis } from "../../domain/javascript/javascriptStaticAnalysisTypes.js";
@@ -653,8 +654,10 @@ export const javaScriptWorkerGraphRecordSchema = z.discriminatedUnion("kind", [
/** Portable per-file header, before any graph binding or immutable proof. */
export const javaScriptWorkerProjectionHeaderSchema = z.strictObject({
omittedUnknownOccurrences:
javaScriptSemanticUnknownOmissionsSchema.exactOptional(),
roots: z.array(javaScriptSemanticNodeSchema.shape.node_id),
truncated: z.boolean(),
nodesTruncated: z.boolean(),
});
/** Module facts are transferred individually to avoid a whole-IR JSON string. */
@@ -1,3 +1,4 @@
import type { JavaScriptSemanticProjectionCapacity } from "../../domain/javascript/javascriptSemanticFileProjection.js";
import { createHash } from "node:crypto";
import { readFile, writeFile } from "node:fs/promises";
import { join } from "node:path";
@@ -26,7 +27,7 @@ import {
export interface JavaScriptSourceWorkerAnalyzer {
(
file: JavaScriptArtifactFile,
nodeBudget: number,
capacity: JavaScriptSemanticProjectionCapacity,
callbacks: {
readonly phase: (
phase: "parse" | "static" | "semantic" | "projection",
@@ -111,7 +112,7 @@ const analyzeJob = async (
...request.file,
text: { included: true, value: source },
},
request.node_budget,
{ nodes: request.node_budget, unknowns: request.unknown_budget },
{
phase: async (phase) => send({ kind: "phase", id: request.id, phase }),
checkpoint: async (facts) => {
@@ -0,0 +1,92 @@
import { Client } from "@modelcontextprotocol/client";
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
import { writeFile } from "node:fs/promises";
import { join, resolve } from "node:path";
import { expect } from "vitest";
import { toolContract } from "../../../src/contracts/toolContracts.js";
import { javascriptApplicationAnalysisResultSchema } from "../../../src/domain/javascript/javascriptApplicationAnalysis.js";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import { cliTest } from "../../support/cli/cliFixture.js";
cliTest(
"preserves caller unknown capacities and omission coverage through real worker, CLI and stdio MCP",
async ({ cli }) => {
const root = await createTestTempDirectory("rea-js-unknown-capacity-");
for (const name of ["first.js", "second.js"])
await writeFile(
join(root, name),
"globalThis.u.p(globalThis.v[k]);\n".repeat(4),
);
const options = {
max_semantic_nodes: 100,
max_semantic_nodes_per_file: 100,
max_semantic_unknowns: 3,
max_semantic_unknowns_per_file: 2,
};
const contract = toolContract("analyze_javascript_application");
const output = await cli.run({
arguments: [
"analyze-javascript-application",
root,
"--format",
"json",
"--max-semantic-nodes",
"100",
"--max-semantic-nodes-per-file",
"100",
"--max-semantic-unknowns",
"3",
"--max-semantic-unknowns-per-file",
"2",
],
});
expect(output.exitCode).toBe(0);
const evidence = contract.outputSchema.parse(output.json);
expect(evidence.parameters).toMatchObject(options);
const result = javascriptApplicationAnalysisResultSchema.parse(
evidence.normalized_result,
);
expect(result.semantic_graph.unknowns).toHaveLength(3);
expect(result.semantic_graph.coverage).toMatchObject({
status: "partial",
truncated: true,
omitted_nodes: 0,
});
expect(
result.semantic_graph.coverage.omitted_unknown_occurrences!.reduce(
(sum, row) => sum + row.occurrences,
0,
),
).toBe(29);
const transport = new StdioClientTransport({
command: process.execPath,
args: [resolve("scripts/rea.mjs"), "mcp"],
cwd: process.cwd(),
env: { PATH: process.env.PATH ?? "", REA_LOG_LEVEL: "silent" },
stderr: "pipe",
});
const client = new Client({
name: "unknown-capacity-regression",
version: "1",
});
try {
await client.connect(transport);
const response = await client.callTool({
name: contract.name,
arguments: { input_path: root, detail: "complete", ...options },
});
expect(response.isError, JSON.stringify(response)).not.toBe(true);
const mcp = contract.outputSchema.parse(response.structuredContent);
expect(mcp.parameters).toEqual(evidence.parameters);
expect(mcp.evidence_id).toBe(evidence.evidence_id);
expect(mcp.normalized_result).toEqual(evidence.normalized_result);
} finally {
try {
await client.close();
} finally {
await transport.close();
}
}
},
30_000,
);
@@ -211,7 +211,7 @@ it("preserves graph commitments and export shapes when consuming file-local IR",
projectJavaScriptSemanticFileSteps(
analyzed.file,
analyzed.semantic.ir,
transferred.remainingFileNodeBudget(),
transferred.remainingFileCapacity(),
),
);
const module = projectJavaScriptModuleSemantics(analyzed.semantic.ir);
@@ -38,7 +38,7 @@ it("enforces the offered complete-file byte budget while preserving authenticate
const ir = analyzeJavaScriptSemantics(text);
const module = projectJavaScriptModuleSemantics(ir);
const projection = await completeJavaScriptAnalysisSteps(
projectJavaScriptSemanticFileSteps(file, ir, 100),
projectJavaScriptSemanticFileSteps(file, ir, { nodes: 100, unknowns: 100 }),
);
const observed = await writeJavaScriptAnalysisTransfer(
join(root, "observed"),
@@ -98,3 +98,41 @@ it("enforces the offered complete-file byte budget while preserving authenticate
),
).rejects.toBeInstanceOf(RangeError);
});
it("rejects authenticated worker rows beyond the offered node or unknown capacity", async () => {
const root = await createTestTempDirectory("rea-js-transfer-capacity-");
const text = "globalThis.u.p(globalThis.v[k]);";
const file: JavaScriptArtifactFile = {
path: "app.js",
sha256: createHash("sha256").update(text).digest("hex"),
container_sha256: "a".repeat(64),
bytes: Buffer.byteLength(text),
inventory_artifact_id: "fixture-source",
kind: "javascript",
unpacked: false,
text: { included: true, value: text },
};
const ir = analyzeJavaScriptSemantics(text);
const projection = await completeJavaScriptAnalysisSteps(
projectJavaScriptSemanticFileSteps(file, ir, { nodes: 100, unknowns: 100 }),
);
expect(projection.unknowns.length).toBeGreaterThan(1);
const path = join(root, "records");
const descriptor = await writeJavaScriptAnalysisTransfer(
path,
projectJavaScriptModuleSemantics(ir),
projection,
);
await expect(
readJavaScriptAnalysisTransfer(path, descriptor, file, undefined, {
nodes: 100,
unknowns: 1,
}),
).rejects.toThrow("unknowns exceed the offered capacity");
await expect(
readJavaScriptAnalysisTransfer(path, descriptor, file, undefined, {
nodes: 1,
unknowns: 100,
}),
).rejects.toThrow("nodes exceed the offered capacity");
});
@@ -51,7 +51,7 @@ it("reuses one real owned analyzer while retaining distinct source identities an
"export function next() { return { observed: 8 }; }",
);
for (const [index, file] of [first, second].entries()) {
const result = await worker.analyze(file, 100);
const result = await worker.analyze(file, { nodes: 100, unknowns: 100 });
if (!result.ok) throw result.error.error;
expect(result.value.javascript.parse_status).toBe("complete");
expect(
@@ -83,7 +83,7 @@ it("reports an actual startup deadline and closes its owned resources before sub
const file = source("app.mjs", "export const observed = 7;");
const expired = new JavaScriptAnalysisWorker({ heapMb: 128, timeoutMs: 1 });
try {
const result = await expired.analyze(file, 100);
const result = await expired.analyze(file, { nodes: 100, unknowns: 100 });
if (result.ok)
throw new Error(
"A one-millisecond fresh process deadline unexpectedly completed",
@@ -97,7 +97,7 @@ it("reports an actual startup deadline and closes its owned resources before sub
}
const next = new JavaScriptAnalysisWorker({ heapMb: 128, timeoutMs: 30_000 });
try {
const result = await next.analyze(file, 100);
const result = await next.analyze(file, { nodes: 100, unknowns: 100 });
if (!result.ok) throw result.error.error;
expect(result.value.javascript.parse_status).toBe("complete");
} finally {
@@ -131,14 +131,18 @@ it("preserves a failed owned cleanup retry when the next caller cancels before a
const next = new JavaScriptAnalysisWorker({ heapMb: 128, timeoutMs: 30_000 });
try {
const file = source("app.mjs", "export const observed = 7;");
const first = await worker.analyze(file, 100);
const first = await worker.analyze(file, { nodes: 100, unknowns: 100 });
if (!first.ok) throw first.error.error;
const closing = worker.close();
await expect(closing).rejects.toBeInstanceOf(ProviderCleanupError);
await expect(closing).rejects.toMatchObject({
cause: { message: "Fixture owned cleanup is unavailable" },
});
const interrupted = await next.analyze(file, 100, controller.signal);
const interrupted = await next.analyze(
file,
{ nodes: 100, unknowns: 100 },
controller.signal,
);
if (interrupted.ok)
throw new Error("Unreleased ownership must block subsequent analysis");
expect(controller.signal.aborted).toBe(true);
@@ -147,7 +151,7 @@ it("preserves a failed owned cleanup retry when the next caller cancels before a
});
releaseAllowed = true;
await worker.close();
const recovered = await next.analyze(file, 100);
const recovered = await next.analyze(file, { nodes: 100, unknowns: 100 });
if (!recovered.ok) throw recovered.error.error;
expect(recovered.value.javascript.parse_status).toBe("complete");
} finally {
@@ -195,7 +199,7 @@ it("ends worker stdin and unreferences process handles when cleanup reports insp
);
try {
const file = source("app.mjs", "export const observed = 7;");
const result = await worker.analyze(file, 100);
const result = await worker.analyze(file, { nodes: 100, unknowns: 100 });
if (!result.ok) throw result.error.error;
const closing = worker.close();
await expect(closing).rejects.toBeInstanceOf(ProviderCleanupError);
@@ -249,7 +253,7 @@ const worker = new JavaScriptAnalysisWorker({ heapMb: 128, timeoutMs: 10000 }, a
await writeFile(${JSON.stringify(ownershipPath)}, JSON.stringify(ownership));
return { ...launched, cleanup: async () => ({ cleaned: false, reason: "fixture inspection unavailable" }) };
});
const analyzed = await worker.analyze(${JSON.stringify(source("app.mjs", "export const value = 1;"))}, 100);
const analyzed = await worker.analyze(${JSON.stringify(source("app.mjs", "export const value = 1;"))}, { nodes: 100, unknowns: 100 });
if (!analyzed.ok) throw analyzed.error.error;
try { await worker.close(); throw new Error("cleanup unexpectedly completed"); }
catch (error) {
@@ -308,7 +312,7 @@ it("formats a real worker's declined semantic budget once", async () => {
try {
const result = await worker.analyze(
source("main.js", "export const observed = 7;"),
100,
{ nodes: 100, unknowns: 100 },
);
if (result.ok) throw new Error("expected a declined semantic budget");
const { error } = result.error;
@@ -350,7 +354,7 @@ it("reports the worker heap limit only when the runtime enforces V8 heap flags",
try {
const result = await worker.analyze(
source("app.mjs", "export const observed = 7;"),
100,
{ nodes: 100, unknowns: 100 },
);
if (result.ok) throw new Error("expected a reported resource failure");
expect(result.error.error).toMatchObject({
@@ -382,7 +386,7 @@ it("rejects a worker heap report that contradicts the launching runtime", async
try {
const result = await worker.analyze(
source("app.mjs", "export const observed = 7;"),
100,
{ nodes: 100, unknowns: 100 },
);
if (result.ok) throw new Error("expected a handshake failure");
expect(result.error.error._tag).toBe("AnalysisOutputError");
@@ -426,7 +430,7 @@ require("node:readline").createInterface({ input: process.stdin }).once("line",
try {
const result = await worker.analyze(
source("app.mjs", "export const observed = 7;"),
100,
{ nodes: 100, unknowns: 100 },
);
if (result.ok) throw new Error("expected the worker to fail");
expect(result.error.error).toMatchObject(