fix(javascript): bound semantic unknown retention at projection

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

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

Fixes #4797
This commit is contained in:
morluto committed 2026-10-17 21:57:31 +08:00
1 parent 926e18b3fc
commit f4322b15ed
34 files changed
+892 -293

No files matched your search

@@ -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);
});
}
};