mirror of
https://github.com/morluto/rea
synced 2026-10-28 06:46:47 -04: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:
@@ -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),
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user