mirror of
https://github.com/morluto/rea.git
synced 2026-10-18 20:10:13 -05:00
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:
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),
|
||||
);
|
||||
});
|
||||
@@ -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 }),
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
+1
-1
@@ -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(
|
||||
|
||||
Reference in new issue
Block a user