feat(javascript): expose bounded semantic tracing

This commit is contained in:
morluto committed 2026-07-22 23:22:55 +00:00
1 parent 8678b57b5e
commit 508a9ccde9
56 files changed
+2455 -307

No files matched your search

@@ -0,0 +1,127 @@
import { createJavaScriptSemanticGraphUnknown } from "../domain/javascriptSemanticGraph.js";
import type { JavaScriptSemanticGraphNode } from "../domain/javascriptSemanticGraph.js";
import type { JavaScriptSemanticIr } from "../domain/javascriptSemanticIr.js";
import type { JavaScriptArtifactAnalysis } from "./JavaScriptArtifactAnalysisTypes.js";
import type { JavaScriptArtifactFile } from "./JavaScriptArtifactFiles.js";
import {
addSemanticGraphRelation,
addSemanticGraphUnknown,
type SemanticGraphProjectionState,
} from "./JavaScriptSemanticGraphConstruction.js";
import {
inferredSemanticEvidenceAt,
unknownSemanticEvidence,
} from "./JavaScriptSemanticGraphEvidence.js";
import { semanticNodesWithinRange } from "./JavaScriptSemanticGraphProjection.js";
/** File-local graph state needed by return, capture, and frontier projection. */
export interface SemanticFlowProjectionContext {
readonly file: JavaScriptArtifactFile;
readonly ir: JavaScriptSemanticIr;
readonly state: SemanticGraphProjectionState;
readonly moduleNode: JavaScriptSemanticGraphNode;
readonly bindingNodes: ReadonlyMap<string, JavaScriptSemanticGraphNode>;
readonly callableNodes: ReadonlyMap<string, JavaScriptSemanticGraphNode>;
readonly returnSiteNodes: ReadonlyMap<string, JavaScriptSemanticGraphNode>;
readonly referenceNodes: readonly JavaScriptSemanticGraphNode[];
}
/** Link direct return expressions to their retained semantic references. */
export const projectSemanticReturnValues = (
context: SemanticFlowProjectionContext,
): void => {
for (const callable of context.ir.callables)
for (const site of callable.returnSites) {
const returnNode = context.returnSiteNodes.get(site.returnSiteId);
const references = semanticNodesWithinRange(
context.referenceNodes,
site.location,
);
for (const reference of references)
addSemanticGraphRelation(context.state, {
source: reference,
target: returnNode,
relation: "aliases",
resolution:
site.identityReferenceLocation !== null &&
rangesEqual(
reference.identity.source_range,
site.identityReferenceLocation,
)
? "resolved"
: "candidate",
});
}
};
const rangesEqual = (
left: JavaScriptSemanticGraphNode["identity"]["source_range"],
right: JavaScriptSemanticGraphNode["identity"]["source_range"],
): boolean =>
left !== null &&
right !== null &&
left.start.line === right.start.line &&
left.start.column === right.start.column &&
left.end.line === right.end.line &&
left.end.column === right.end.column;
/** Link each captured binding to the callable at the exact reference site. */
export const projectSemanticClosureCaptures = (
context: SemanticFlowProjectionContext,
): void => {
for (const capture of context.ir.closureCaptures)
addSemanticGraphRelation(context.state, {
source: context.bindingNodes.get(capture.bindingId),
target: context.callableNodes.get(capture.callableId),
relation: "captures",
resolution: "resolved",
evidence: inferredSemanticEvidenceAt(
context.file,
capture.referenceLocation,
),
});
};
/** Retain bounded unresolved dynamic-call and dynamic-property frontiers. */
export const projectSemanticFrontiers = (
context: SemanticFlowProjectionContext,
): void => {
for (const frontier of context.ir.frontiers) {
const unknown = createJavaScriptSemanticGraphUnknown({
node_id:
frontier.callableId === null
? context.moduleNode.node_id
: (context.callableNodes.get(frontier.callableId)?.node_id ??
context.moduleNode.node_id),
family: frontier.kind === "dynamic-call" ? "call-flow" : "object-flow",
relation_kinds:
frontier.kind === "dynamic-call"
? ["calls"]
: ["reads-property", "writes-property"],
reason: frontier.kind,
detail: frontier.reason,
candidate_node_ids: [],
evidence: unknownSemanticEvidence(context.file, frontier.location),
});
addSemanticGraphUnknown(context.state, unknown);
}
};
/** Publish exact semantic analyzer limits reached by any admitted source file. */
export const semanticRecoveryLimits = (
analysis: JavaScriptArtifactAnalysis,
): {
readonly name: string;
readonly value: number;
readonly unit: "items";
}[] => {
const limits = new Map<string, number>();
for (const { semantic } of analysis.files) {
if (semantic === null) continue;
for (const name of semantic.ir.coverage.limitsReached)
limits.set(`semantic.${name}`, semantic.limits[name]);
}
return [...limits]
.sort(([left], [right]) => left.localeCompare(right))
.map(([name, value]) => ({ name, value, unit: "items" }));
};