refactor(javascript)!: share semantic evidence at fact construction

Intern shared provenance in a graph-scoped context registry while retaining exact locations on every fact. Semantic traces carry their referenced context subset, and portable graph validation rejects stale or dangling commitments.

BREAKING CHANGE: Semantic graph facts now store evidence context IDs and locations. Graph and semantic trace results require an inline evidence_contexts table.
This commit is contained in:
morluto committed 2026-10-10 03:52:13 +08:00
1 parent 9c4a68bc19
commit e3bbb9ac45
25 files changed
+1109 -464

No files matched your search

+9
View File
@@ -23,6 +23,15 @@ artifact digest and structural graph ID. Semantic tracing requires the semantic
relation graph and reports when it is unavailable rather than treating missing
data as an empty graph.
The semantic graph wire format stores shared non-location provenance once in
its required `evidence_contexts` table. Each node, relation, fingerprint, and
unknown retains its exact `evidence.location` and names the owning context with
`evidence.context_id`. This is a breaking representation change: consumers
reading fields such as `evidence.authority` must look up the context by ID and
combine its fields with the fact's location. Semantic trace results include the
canonical context subset referenced by their returned facts, so those facts
remain self-contained in the response.
## Feature tracing
Select one literal seed kind: node ID, route, string, API, IPC channel, module,
@@ -12,6 +12,7 @@ import type {
ReconstructionObligation,
ReviewedReconstructionObligation,
} from "../domain/reconstructionObligationLedgerSchemas.js";
import { resolveJavaScriptSemanticEvidence } from "../domain/javascript/javascriptSemanticGraph.js";
import { parseApplicationGraphEvidence } from "./javascript/JavaScriptApplicationEvidenceGraph.js";
import {
applicationObligationPolicy,
@@ -111,6 +112,10 @@ const deriveApplicationCandidates = (
for (const node of source.semanticGraph.nodes) {
const candidatePolicy = semanticObligationPolicy(node.kind);
if (candidatePolicy === undefined) continue;
const evidenceContext = resolveJavaScriptSemanticEvidence(
source.semanticGraph,
node.evidence,
);
addCandidate(
candidates,
generatedCandidate({
@@ -125,7 +130,7 @@ const deriveApplicationCandidates = (
graphReference(
evidence,
records,
node.evidence,
evidenceContext,
`${source.semanticGraph.graph_id}/node/${node.node_id}`,
),
],
@@ -120,18 +120,21 @@ const addEventUnknown = (
): 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),
}),
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,
),
);
};
@@ -210,14 +213,17 @@ const projectTimerCancellation = (
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),
}),
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,
),
);
};
@@ -3,7 +3,10 @@ import {
createImmutableJavaScriptSemanticGraphSteps,
type JavaScriptSemanticGraph,
} from "../../domain/javascript/javascriptSemanticGraph.js";
import type { JavaScriptSemanticGraphNode } from "../../domain/javascript/javascriptSemanticGraphSchemas.js";
import type {
JavaScriptSemanticFingerprint,
JavaScriptSemanticGraphNode,
} from "../../domain/javascript/javascriptSemanticGraphSchemas.js";
import {
JAVASCRIPT_SEMANTIC_RELATION_FAMILIES,
JAVASCRIPT_SEMANTIC_RELATION_FAMILY,
@@ -141,7 +144,7 @@ export interface JavaScriptSemanticGraphProjection {
export const createJavaScriptSemanticGraphProjection =
(): JavaScriptSemanticGraphProjection => {
const state = emptyState({ nodes: [] });
const fingerprints: JavaScriptSemanticGraph["fingerprints"][number][] = [];
const fingerprints: JavaScriptSemanticFingerprint[] = [];
let truncatedFiles = 0;
const projectSource = (
file: JavaScriptArtifactFile,
@@ -178,6 +181,7 @@ export const createJavaScriptSemanticGraphProjection =
root_artifact_sha256: rootArtifactSha256,
application_graph_id: applicationGraph.graph_id,
root_node_ids: [...state.roots],
evidence_contexts: state.evidenceContexts.contexts,
nodes: [...state.nodes.values()],
relations: [...state.relations.values()],
fingerprints,
@@ -229,6 +233,7 @@ export const createJavaScriptSemanticGraphProjection =
state.relations.clear();
state.unknowns.clear();
state.roots.clear();
state.evidenceContexts.clear();
fingerprints.length = 0;
return graph;
};
@@ -253,7 +258,7 @@ const projectFile = (
file: JavaScriptArtifactFile,
ir: JavaScriptSemanticIr,
state: BuilderState,
): JavaScriptSemanticGraph["fingerprints"][number][] => {
): JavaScriptSemanticFingerprint[] => {
const moduleNode = retainNode(state, file, {
kind: "module",
roleKey: "module",
@@ -335,7 +340,12 @@ const projectFile = (
projectSemanticResources(context);
projectSemanticClosureCaptures(context);
projectSemanticFrontiers(context);
return projectSemanticFunctionFingerprints(file, ir, callableNodes);
return projectSemanticFunctionFingerprints(
file,
ir,
callableNodes,
state.evidenceContexts,
);
};
const createReturnSiteNodes = (
@@ -223,14 +223,17 @@ const addChildUnknown = (input: ChildUnknownInput): void => {
input;
addSemanticGraphUnknown(
context.state,
createJavaScriptSemanticGraphUnknown({
node_id: node?.node_id ?? null,
family: "child-process",
relation_kinds: relationKinds,
reason: "ambiguous-target",
detail,
candidate_node_ids: candidateNodeIds,
evidence: unknownSemanticEvidence(context.file, location),
}),
createJavaScriptSemanticGraphUnknown(
{
node_id: node?.node_id ?? null,
family: "child-process",
relation_kinds: relationKinds,
reason: "ambiguous-target",
detail,
candidate_node_ids: candidateNodeIds,
evidence: unknownSemanticEvidence(context.file, location),
},
context.state.evidenceContexts,
),
);
};
@@ -4,6 +4,7 @@ import { javascriptDisplayText } from "../../domain/javascript/javascriptAstValu
import {
createJavaScriptSemanticGraphNode,
createJavaScriptSemanticGraphRelation,
JavaScriptSemanticEvidenceContextRegistry,
javaScriptSemanticNodeId,
} from "../../domain/javascript/javascriptSemanticGraph.js";
import type { JavaScriptSemanticGraphNode } from "../../domain/javascript/javascriptSemanticGraphSchemas.js";
@@ -25,6 +26,7 @@ export interface SemanticGraphProjectionState {
readonly nodes: Map<string, JavaScriptSemanticGraphNode>;
readonly relations: Map<string, JavaScriptSemanticGraphRelation>;
readonly unknowns: Map<string, JavaScriptSemanticGraphUnknown>;
readonly evidenceContexts: JavaScriptSemanticEvidenceContextRegistry;
readonly roots: Set<string>;
readonly applicationNodeIdsByLocation: ReadonlyMap<string, readonly string[]>;
/**
@@ -76,6 +78,7 @@ export const createSemanticGraphProjectionState = (
nodes: new Map(),
relations: new Map(),
unknowns: new Map(),
evidenceContexts: new JavaScriptSemanticEvidenceContextRegistry(),
roots: new Set(),
applicationNodeIdsByLocation: index.identifiers,
};
@@ -109,20 +112,23 @@ export const constructSemanticGraphNode = (
input: SemanticNodeConstructionInput,
state: SemanticGraphProjectionState,
): JavaScriptSemanticGraphNode =>
createJavaScriptSemanticGraphNode({
kind: input.kind,
identity: {
artifact_sha256: file.sha256,
module_path: file.path,
source_range: input.location,
role_key: input.roleKey,
createJavaScriptSemanticGraphNode(
{
kind: input.kind,
identity: {
artifact_sha256: file.sha256,
module_path: file.path,
source_range: input.location,
role_key: input.roleKey,
},
function_node_id: input.functionNodeId,
application_node_ids: matchingApplicationNodeIds(file, input, state),
label: input.label === null ? null : javascriptDisplayText(input.label),
properties: input.properties ?? {},
evidence: observedSemanticEvidence(file, input.location),
},
function_node_id: input.functionNodeId,
application_node_ids: matchingApplicationNodeIds(file, input, state),
label: input.label === null ? null : javascriptDisplayText(input.label),
properties: input.properties ?? {},
evidence: observedSemanticEvidence(file, input.location),
});
state.evidenceContexts,
);
/** Reuse retained identities and reject exhausted budgets before allocation. */
export const retainSemanticGraphNode = (
@@ -183,14 +189,21 @@ export const addSemanticGraphRelation = (
input.source.node_id === input.target.node_id
)
return;
const value = createJavaScriptSemanticGraphRelation({
source_node_id: input.source.node_id,
target_node_id: input.target.node_id,
relation: input.relation,
resolution: input.resolution ?? "candidate",
properties: input.properties ?? {},
evidence: input.evidence ?? inferredSemanticEvidence(input.source),
});
const value = createJavaScriptSemanticGraphRelation(
{
source_node_id: input.source.node_id,
target_node_id: input.target.node_id,
relation: input.relation,
resolution: input.resolution ?? "candidate",
properties: input.properties ?? {},
evidence:
input.evidence ??
inferredSemanticEvidence(
state.evidenceContexts.resolve(input.source.evidence),
),
},
state.evidenceContexts,
);
if (state.relations.has(value.relation_id)) return;
state.relations.set(value.relation_id, value);
};
@@ -211,20 +224,23 @@ export const addSemanticFallbackRoot = (
): void => {
const node = addSemanticGraphNode(
state,
createJavaScriptSemanticGraphNode({
kind: "module",
identity: {
artifact_sha256: rootArtifactSha256,
module_path: "unknown-semantic-root",
source_range: null,
role_key: "artifact-root",
createJavaScriptSemanticGraphNode(
{
kind: "module",
identity: {
artifact_sha256: rootArtifactSha256,
module_path: "unknown-semantic-root",
source_range: null,
role_key: "artifact-root",
},
function_node_id: null,
application_node_ids: [],
label: "unavailable semantic root",
properties: {},
evidence: unavailableSemanticRootEvidence(rootArtifactSha256),
},
function_node_id: null,
application_node_ids: [],
label: "unavailable semantic root",
properties: {},
evidence: unavailableSemanticRootEvidence(rootArtifactSha256),
}),
state.evidenceContexts,
),
);
if (node !== null) state.roots.add(node.node_id);
};
@@ -307,14 +307,17 @@ const addDataUnknown = (input: DataUnknownInput): void => {
} = input;
addSemanticGraphUnknown(
context.state,
createJavaScriptSemanticGraphUnknown({
node_id: node.node_id,
family,
relation_kinds: relationKinds,
reason: "ambiguous-target",
detail,
candidate_node_ids: candidateNodeIds,
evidence: unknownSemanticEvidence(context.file, location),
}),
createJavaScriptSemanticGraphUnknown(
{
node_id: node.node_id,
family,
relation_kinds: relationKinds,
reason: "ambiguous-target",
detail,
candidate_node_ids: candidateNodeIds,
evidence: unknownSemanticEvidence(context.file, location),
},
context.state.evidenceContexts,
),
);
};
@@ -1,5 +1,4 @@
import type { ApplicationGraphEvidence } from "../../domain/javascript/javascriptApplicationEvidenceSchemas.js";
import type { JavaScriptSemanticGraphNode } from "../../domain/javascript/javascriptSemanticGraphSchemas.js";
import type { JavaScriptSourceRange } from "../../domain/javascript/javascriptStaticAnalysisTypes.js";
import type { JavaScriptArtifactFile } from "../../domain/javascript/javascriptArtifactFiles.js";
@@ -44,9 +43,9 @@ export const observedSemanticEvidence = (
/** Conservative static relationship evidence derived from one syntax node. */
export const inferredSemanticEvidence = (
source: JavaScriptSemanticGraphNode,
source: ApplicationGraphEvidence,
): ApplicationGraphEvidence => ({
...source.evidence,
...source,
authority: "static-relationship-inference",
state: "inferred",
confidence: "high",
@@ -1,8 +1,11 @@
import {
createJavaScriptSemanticFingerprint,
type JavaScriptSemanticGraph,
type JavaScriptSemanticEvidenceContextRegistry,
} from "../../domain/javascript/javascriptSemanticGraph.js";
import type { JavaScriptSemanticGraphNode } from "../../domain/javascript/javascriptSemanticGraphSchemas.js";
import type {
JavaScriptSemanticFingerprint,
JavaScriptSemanticGraphNode,
} from "../../domain/javascript/javascriptSemanticGraphSchemas.js";
import type { JavaScriptSemanticIr } from "../../domain/javascript/javascriptSemanticIr.js";
import type { JavaScriptArtifactFile } from "../../domain/javascript/javascriptArtifactFiles.js";
import { observedSemanticEvidence } from "./JavaScriptSemanticGraphEvidence.js";
@@ -12,28 +15,32 @@ export const projectSemanticFunctionFingerprints = (
file: JavaScriptArtifactFile,
ir: JavaScriptSemanticIr,
callableNodes: ReadonlyMap<string, JavaScriptSemanticGraphNode>,
): JavaScriptSemanticGraph["fingerprints"][number][] =>
evidenceContexts: JavaScriptSemanticEvidenceContextRegistry,
): JavaScriptSemanticFingerprint[] =>
ir.functionFingerprints.flatMap((fingerprint) => {
const callable = callableNodes.get(fingerprint.callableId);
if (callable === undefined) return [];
return [
createJavaScriptSemanticFingerprint({
function_node_id: callable.node_id,
algorithm: "rea.javascript-semantic-function",
status: fingerprint.status,
components: {
parameter_arity: fingerprint.components.parameterArity,
normalized_ast_sha256: fingerprint.components.normalizedAstSha256,
control_flow_sha256: fingerprint.components.controlFlowSha256,
relation_shape_sha256: fingerprint.components.relationShapeSha256,
literal_set_sha256: fingerprint.components.literalSetSha256,
effects: fingerprint.components.effects,
createJavaScriptSemanticFingerprint(
{
function_node_id: callable.node_id,
algorithm: "rea.javascript-semantic-function",
status: fingerprint.status,
components: {
parameter_arity: fingerprint.components.parameterArity,
normalized_ast_sha256: fingerprint.components.normalizedAstSha256,
control_flow_sha256: fingerprint.components.controlFlowSha256,
relation_shape_sha256: fingerprint.components.relationShapeSha256,
literal_set_sha256: fingerprint.components.literalSetSha256,
effects: fingerprint.components.effects,
},
limitations: fingerprint.limitations,
evidence: observedSemanticEvidence(
file,
callable.identity.source_range,
),
},
limitations: fingerprint.limitations,
evidence: observedSemanticEvidence(
file,
callable.identity.source_range,
),
}),
evidenceContexts,
),
];
});
@@ -179,18 +179,21 @@ const addPromiseUnknown = (
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),
}),
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,
),
);
};
@@ -254,19 +257,22 @@ export const 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),
});
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);
}
};
@@ -168,14 +168,17 @@ const addObjectUnknown = (
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),
}),
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,
),
);
};
@@ -69,16 +69,19 @@ const projectRelease = (
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),
}),
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,
),
);
};
@@ -4,6 +4,7 @@ import { buildJavaScriptSemanticGraph } from "./JavaScriptSemanticGraphBuilder.j
import type { JavaScriptArtifactAnalysis } from "./JavaScriptArtifactAnalysisTypes.js";
import type { JavaScriptArtifactFile } from "../../domain/javascript/javascriptArtifactFiles.js";
import { queryJavaScriptSemanticGraph } from "../../domain/javascript/javascriptSemanticQuery.js";
import { resolveJavaScriptSemanticEvidence } from "../../domain/javascript/javascriptSemanticGraph.js";
import { analyzeJavaScriptSemantics } from "../../domain/javascript/javascriptSemanticAnalysis.js";
const SHA256 = "a".repeat(64);
@@ -335,11 +336,15 @@ it("uses unavailable evidence when no source produced semantic IR", () => {
applicationGraph: { graph_id: GRAPH_ID, nodes: [] },
analysis: emptyAnalysis(),
});
expect(graph.nodes[0]?.evidence).toMatchObject({
authority: "unknown",
state: "unavailable",
location: { available: false, reason: "not-observed" },
});
const root = graph.nodes[0];
if (root === undefined) throw new Error("Expected unavailable semantic root");
expect(resolveJavaScriptSemanticEvidence(graph, root.evidence)).toMatchObject(
{
authority: "unknown",
state: "unavailable",
location: { available: false, reason: "not-observed" },
},
);
});
const emptyAnalysis = (): JavaScriptArtifactAnalysis => ({
@@ -96,20 +96,23 @@ 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,
),
}),
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,
),
);
} else if (
value.status === "unknown" ||
@@ -120,24 +123,27 @@ const projectValue = (input: ValueProjectionInput): void => {
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],
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,
),
);
}
};
@@ -13,6 +13,7 @@ import {
import {
createJavaScriptSemanticGraph,
createJavaScriptSemanticGraphNode,
JavaScriptSemanticEvidenceContextRegistry,
} from "../../domain/javascript/javascriptSemanticGraph.js";
import { JAVASCRIPT_SEMANTIC_RELATION_FAMILIES } from "../../domain/javascript/javascriptSemanticGraphSchemas.js";
import { javascriptApplicationAnalysisResultSchema } from "../../domain/javascript/javascriptApplicationAnalysis.js";
@@ -184,30 +185,35 @@ const exampleEvidence = (property: "count" | "total") => {
coverage: partialApplicationCoverage([], null),
limitations,
});
const module = createJavaScriptSemanticGraphNode({
kind: "module",
identity: {
artifact_sha256: artifact.sha256,
module_path: modulePath,
source_range: null,
role_key: "example-module",
const evidenceContexts = new JavaScriptSemanticEvidenceContextRegistry();
const module = createJavaScriptSemanticGraphNode(
{
kind: "module",
identity: {
artifact_sha256: artifact.sha256,
module_path: modulePath,
source_range: null,
role_key: "example-module",
},
function_node_id: null,
application_node_ids: [exported.node_id],
label: modulePath,
properties: {},
evidence: {
...evidence,
authority: "ast-static-analysis",
state: "observed",
confidence: "exact",
},
},
function_node_id: null,
application_node_ids: [exported.node_id],
label: modulePath,
properties: {},
evidence: {
...evidence,
authority: "ast-static-analysis",
state: "observed",
confidence: "exact",
},
});
evidenceContexts,
);
const semanticGraph = createJavaScriptSemanticGraph({
schema: "JavaScriptSemanticRelationGraph",
root_artifact_sha256: metadata.root_artifact_sha256,
application_graph_id: graph.graph_id,
root_node_ids: [module.node_id],
evidence_contexts: evidenceContexts.contexts,
nodes: [module],
relations: [],
fingerprints: [],
@@ -7,6 +7,7 @@ import { createWebTextArtifact } from "../../domain/webContentArtifact.js";
import {
createJavaScriptSemanticGraph,
createJavaScriptSemanticGraphNode,
JavaScriptSemanticEvidenceContextRegistry,
} from "../../domain/javascript/javascriptSemanticGraph.js";
import { JAVASCRIPT_SEMANTIC_RELATION_FAMILIES } from "../../domain/javascript/javascriptSemanticGraphSchemas.js";
@@ -70,25 +71,30 @@ const graph = createJavaScriptApplicationGraph({
coverage: completeCoverage,
limitations: [],
});
const semanticNode = createJavaScriptSemanticGraphNode({
kind: "module",
identity: {
artifact_sha256: applicationSha256,
module_path: "renderer.js",
source_range: null,
role_key: "example-module",
const evidenceContexts = new JavaScriptSemanticEvidenceContextRegistry();
const semanticNode = createJavaScriptSemanticGraphNode(
{
kind: "module",
identity: {
artifact_sha256: applicationSha256,
module_path: "renderer.js",
source_range: null,
role_key: "example-module",
},
function_node_id: null,
application_node_ids: [asset.node_id],
label: "renderer.js",
properties: {},
evidence: graphEvidence,
},
function_node_id: null,
application_node_ids: [asset.node_id],
label: "renderer.js",
properties: {},
evidence: graphEvidence,
});
evidenceContexts,
);
const semanticGraph = createJavaScriptSemanticGraph({
schema: "JavaScriptSemanticRelationGraph",
root_artifact_sha256: applicationSha256,
application_graph_id: graph.graph_id,
root_node_ids: [semanticNode.node_id],
evidence_contexts: evidenceContexts.contexts,
nodes: [semanticNode],
relations: [],
fingerprints: [],
-1
View File
@@ -5,7 +5,6 @@ import type {
} from "./providerOperationHealth.js";
import {
hopperStartupFailure,
type HopperStartupDiagnostic,
type HopperStartupFailureCode,
type HopperStartupFailureDiagnostic,
} from "./hopperStartupFailure.js";
@@ -269,6 +269,14 @@ const applicationGraphEvidenceBaseSchema = z.strictObject({
evidence_ids: z.array(prefixedDigestSchema("ev")),
});
/** Shared provenance fields apart from the fact-specific evidence location. */
export const applicationGraphEvidenceContextSchema =
applicationGraphEvidenceBaseSchema.omit({ location: true });
/** Exact coordinates retained on each graph fact. */
export const applicationGraphEvidenceLocationSchema =
applicationLocationReferenceSchema;
type EvidenceValue = z.infer<typeof applicationGraphEvidenceBaseSchema>;
const checkAuthorityState = (
@@ -476,6 +484,11 @@ export type ApplicationGraphEvidence = z.infer<
typeof applicationGraphEvidenceSchema
>;
/** Provenance shared by facts with the same non-location evidence. */
export type ApplicationGraphEvidenceContext = z.infer<
typeof applicationGraphEvidenceContextSchema
>;
/** Explicit stable identity strategy for one application entity. */
export type ApplicationNodeIdentity = z.infer<
typeof applicationNodeIdentitySchema
+176 -11
View File
@@ -6,7 +6,11 @@ import { uniqueSorted } from "../canonicalOrdering.js";
import { compareUnicodeCodePoints } from "../unicodeCodePointOrder.js";
import { canonicalJsonDigestSteps } from "../canonicalJsonDigestSteps.js";
import { freezeOwnedJsonSnapshotSteps } from "../immutableJson.js";
import type { ApplicationGraphEvidence } from "./javascriptApplicationEvidenceSchemas.js";
import type {
ApplicationGraphEvidence,
ApplicationGraphEvidenceContext,
} from "./javascriptApplicationEvidenceSchemas.js";
import { applicationGraphEvidenceSchema } from "./javascriptApplicationEvidenceSchemas.js";
import {
JAVASCRIPT_SEMANTIC_RELATION_FAMILIES,
JAVASCRIPT_SEMANTIC_RELATION_FAMILY,
@@ -23,6 +27,10 @@ import {
type JavaScriptSemanticGraphInput,
type JavaScriptSemanticGraphNode,
type JavaScriptSemanticGraphRelation,
type JavaScriptSemanticGraphUnknown,
type JavaScriptSemanticFingerprint,
type JavaScriptSemanticEvidenceContext,
type JavaScriptSemanticEvidenceReference,
} from "./javascriptSemanticGraphSchemas.js";
const normalizeEvidence = (
@@ -42,6 +50,60 @@ const normalizeEvidence = (
evidence_ids: uniqueSorted(evidence.evidence_ids),
});
const evidenceContextId = (context: ApplicationGraphEvidenceContext): string =>
`jsrg_evidence_${digestCanonicalValue(context, "JavaScript semantic evidence context")}`;
/** Graph-scoped owner for canonical provenance shared by semantic facts. */
export class JavaScriptSemanticEvidenceContextRegistry {
readonly #contexts = new Map<string, JavaScriptSemanticEvidenceContext>();
/** Intern normalized non-location provenance at fact construction. */
intern(
evidence: ApplicationGraphEvidence,
): JavaScriptSemanticEvidenceReference {
const normalized = normalizeEvidence(evidence);
const { location, ...context } = normalized;
const contextId = evidenceContextId(context);
const row = { ...context, context_id: contextId };
const existing = this.#contexts.get(contextId);
if (
existing !== undefined &&
canonicalJson(existing, "JavaScript semantic graph") !==
canonicalJson(row, "JavaScript semantic graph")
)
throw new TypeError(
"JavaScript semantic evidence context digest collision",
);
this.#contexts.set(contextId, row);
return { context_id: contextId, location };
}
/** Resolve one already interned fact reference for derived evidence. */
resolve(
reference: JavaScriptSemanticEvidenceReference,
): ApplicationGraphEvidence {
const context = this.#contexts.get(reference.context_id);
if (context === undefined)
throw new TypeError(
"Semantic evidence reference names an absent context",
);
const { context_id: _contextId, ...evidenceContext } = context;
return { ...evidenceContext, location: reference.location };
}
/** Return the deterministic context table owned by this graph projection. */
get contexts(): JavaScriptSemanticEvidenceContext[] {
return [...this.#contexts.values()].sort((left, right) =>
compareUnicodeCodePoints(left.context_id, right.context_id),
);
}
/** Release interned contexts after the owning graph is finalized. */
clear(): void {
this.#contexts.clear();
}
}
/** Derive a semantic node ID from its kind and exact artifact identity. */
export const javaScriptSemanticNodeId = (
node: Pick<JavaScriptSemanticGraphNode, "kind" | "identity">,
@@ -51,12 +113,13 @@ export const javaScriptSemanticNodeId = (
/** Normalize one semantic entity and derive its artifact-version identifier. */
export const createJavaScriptSemanticGraphNode = (
input: unknown,
evidenceContexts: JavaScriptSemanticEvidenceContextRegistry,
): JavaScriptSemanticGraphNode => {
const parsed = javaScriptSemanticNodeInputSchema.parse(input);
const semantic = {
...parsed,
application_node_ids: uniqueSorted(parsed.application_node_ids),
evidence: normalizeEvidence(parsed.evidence),
evidence: evidenceContexts.intern(parsed.evidence),
identifier_strategy: {
strategy: "semantic-content-sha256" as const,
stability: "artifact-version" as const,
@@ -71,11 +134,12 @@ export const createJavaScriptSemanticGraphNode = (
/** Normalize one semantic relationship and derive its exact identifier. */
export const createJavaScriptSemanticGraphRelation = (
input: unknown,
evidenceContexts: JavaScriptSemanticEvidenceContextRegistry,
): JavaScriptSemanticGraphRelation => {
const parsed = javaScriptSemanticRelationInputSchema.parse(input);
const semantic = {
...parsed,
evidence: normalizeEvidence(parsed.evidence),
evidence: evidenceContexts.intern(parsed.evidence),
identifier_strategy: {
strategy: "semantic-content-sha256" as const,
stability: "relationship-exact" as const,
@@ -88,13 +152,16 @@ export const createJavaScriptSemanticGraphRelation = (
};
/** Normalize one unresolved semantic frontier and derive its identifier. */
export const createJavaScriptSemanticGraphUnknown = (input: unknown) => {
export const createJavaScriptSemanticGraphUnknown = (
input: unknown,
evidenceContexts: JavaScriptSemanticEvidenceContextRegistry,
): JavaScriptSemanticGraphUnknown => {
const parsed = javaScriptSemanticUnknownInputSchema.parse(input);
const semantic = {
...parsed,
relation_kinds: uniqueSorted(parsed.relation_kinds),
candidate_node_ids: uniqueSorted(parsed.candidate_node_ids),
evidence: normalizeEvidence(parsed.evidence),
evidence: evidenceContexts.intern(parsed.evidence),
};
return javaScriptSemanticUnknownSchema.parse({
...semantic,
@@ -103,7 +170,10 @@ export const createJavaScriptSemanticGraphUnknown = (input: unknown) => {
};
/** Normalize one function fingerprint and derive its component commitment. */
export const createJavaScriptSemanticFingerprint = (input: unknown) => {
export const createJavaScriptSemanticFingerprint = (
input: unknown,
evidenceContexts: JavaScriptSemanticEvidenceContextRegistry,
): JavaScriptSemanticFingerprint => {
const parsed = javaScriptSemanticFingerprintInputSchema.parse(input);
const semantic = {
...parsed,
@@ -112,7 +182,7 @@ export const createJavaScriptSemanticFingerprint = (input: unknown) => {
effects: uniqueSorted(parsed.components.effects),
},
limitations: uniqueSorted(parsed.limitations),
evidence: normalizeEvidence(parsed.evidence),
evidence: evidenceContexts.intern(parsed.evidence),
};
const fingerprintSha256 = digestCanonicalValue(
semantic.components,
@@ -252,6 +322,12 @@ const checkCanonicalOrder = (
"Root nodes",
context,
);
sortedUniqueIssue(
graph.evidence_contexts.map(({ context_id }) => context_id),
["evidence_contexts"],
"Evidence contexts",
context,
);
sortedUniqueIssue(
graph.nodes.map(({ node_id }) => node_id),
["nodes"],
@@ -279,6 +355,66 @@ const checkCanonicalOrder = (
sortedUniqueIssue(graph.limitations, ["limitations"], "Limitations", context);
};
function* checkEvidenceContextReferencesSteps(
graph: GraphRecord,
context: GraphIssueReporter,
): Generator<void> {
const contexts = new Map(
graph.evidence_contexts.map((value) => [value.context_id, value]),
);
const referenced = new Set<string>();
const validateReferences = function* (
entities: readonly { evidence: JavaScriptSemanticEvidenceReference }[],
entityKind: "nodes" | "relations" | "unknowns" | "fingerprints",
): Generator<void> {
for (const [index, { evidence }] of entities.entries()) {
if (index % 256 === 0) yield;
referenced.add(evidence.context_id);
const evidenceContext = contexts.get(evidence.context_id);
if (evidenceContext === undefined) {
context.addIssue({
code: "custom",
path: [entityKind, index, "evidence", "context_id"],
message: "Evidence reference names an absent context",
});
continue;
}
const { context_id: _contextId, ...evidenceValue } = evidenceContext;
if (
!applicationGraphEvidenceSchema.safeParse({
...evidenceValue,
location: evidence.location,
}).success
)
context.addIssue({
code: "custom",
path: [entityKind, index, "evidence"],
message: "Evidence context is incompatible with its fact location",
});
}
};
yield* validateReferences(graph.nodes, "nodes");
yield* validateReferences(graph.relations, "relations");
yield* validateReferences(graph.unknowns, "unknowns");
yield* validateReferences(graph.fingerprints, "fingerprints");
for (const [index, evidenceContext] of graph.evidence_contexts.entries()) {
if (index % 256 === 0) yield;
const { context_id: identifier, ...value } = evidenceContext;
if (identifier !== evidenceContextId(value))
context.addIssue({
code: "custom",
path: ["evidence_contexts", index, "context_id"],
message: "Evidence context identifier is stale",
});
if (!referenced.has(identifier))
context.addIssue({
code: "custom",
path: ["evidence_contexts", index],
message: "Evidence context is not referenced by a graph fact",
});
}
}
function* checkNodesSteps(
graph: GraphRecord,
nodes: ReadonlyMap<string, JavaScriptSemanticGraphNode>,
@@ -323,6 +459,7 @@ function* checkRootsSteps(
function* checkRelationsSteps(
graph: GraphRecord,
nodes: ReadonlyMap<string, JavaScriptSemanticGraphNode>,
evidenceContexts: ReadonlyMap<string, JavaScriptSemanticEvidenceContext>,
context: GraphIssueReporter,
): Generator<void> {
for (const [index, relation] of graph.relations.entries()) {
@@ -355,7 +492,7 @@ function* checkRelationsSteps(
});
if (
relation.resolution === "candidate" &&
relation.evidence.state === "observed"
evidenceContexts.get(relation.evidence.context_id)?.state === "observed"
)
context.addIssue({
code: "custom",
@@ -368,6 +505,7 @@ function* checkRelationsSteps(
function* checkUnknownsSteps(
graph: GraphRecord,
nodes: ReadonlyMap<string, JavaScriptSemanticGraphNode>,
evidenceContexts: ReadonlyMap<string, JavaScriptSemanticEvidenceContext>,
context: GraphIssueReporter,
): Generator<void> {
for (const [index, unknown] of graph.unknowns.entries()) {
@@ -405,7 +543,10 @@ function* checkUnknownsSteps(
path: ["unknowns", index, "relation_kinds"],
message: "Unknown relation kinds must belong to their declared family",
});
if (!["unknown", "unavailable"].includes(unknown.evidence.state))
const evidenceState = evidenceContexts.get(
unknown.evidence.context_id,
)?.state;
if (evidenceState !== "unknown" && evidenceState !== "unavailable")
context.addIssue({
code: "custom",
path: ["unknowns", index, "evidence", "state"],
@@ -485,6 +626,13 @@ function* checkGraphContentSteps(
context: GraphIssueReporter,
): Generator<void> {
checkCanonicalOrder(graph, context);
yield* checkEvidenceContextReferencesSteps(graph, context);
const evidenceContexts = new Map(
graph.evidence_contexts.map((evidenceContext) => [
evidenceContext.context_id,
evidenceContext,
]),
);
const nodes = new Map<string, JavaScriptSemanticGraphNode>();
for (const [index, node] of graph.nodes.entries()) {
if (index % 256 === 0) yield;
@@ -492,8 +640,8 @@ function* checkGraphContentSteps(
}
yield* checkNodesSteps(graph, nodes, context);
yield* checkRootsSteps(graph, nodes, context);
yield* checkRelationsSteps(graph, nodes, context);
yield* checkUnknownsSteps(graph, nodes, context);
yield* checkRelationsSteps(graph, nodes, evidenceContexts, context);
yield* checkUnknownsSteps(graph, nodes, evidenceContexts, context);
checkUnknownReferences(graph, context);
yield* checkFingerprintsSteps(graph, nodes, context);
yield* checkCoverageSteps(graph, context);
@@ -538,11 +686,28 @@ export type JavaScriptSemanticGraph = z.infer<
typeof javaScriptSemanticGraphSchema
>;
/** Resolve one fact's compact provenance without changing its stored shape. */
export const resolveJavaScriptSemanticEvidence = (
graph: Pick<JavaScriptSemanticGraph, "evidence_contexts">,
reference: JavaScriptSemanticEvidenceReference,
): ApplicationGraphEvidence => {
const context = graph.evidence_contexts.find(
({ context_id }) => context_id === reference.context_id,
);
if (context === undefined)
throw new TypeError("Semantic evidence reference names an absent context");
const { context_id: _contextId, ...evidenceContext } = context;
return { ...evidenceContext, location: reference.location };
};
const normalizeGraphInput = (
parsed: JavaScriptSemanticGraphInput,
): JavaScriptSemanticGraphInput => ({
...parsed,
root_node_ids: uniqueSorted(parsed.root_node_ids),
evidence_contexts: [...parsed.evidence_contexts].sort((left, right) =>
compareUnicodeCodePoints(left.context_id, right.context_id),
),
nodes: [...parsed.nodes].sort((left, right) =>
compareUnicodeCodePoints(left.node_id, right.node_id),
),
@@ -16,6 +16,9 @@ import {
createJavaScriptSemanticGraphUnknown,
createImmutableJavaScriptSemanticGraphSteps,
isValidatedImmutableJavaScriptSemanticGraph,
JavaScriptSemanticEvidenceContextRegistry,
javaScriptSemanticGraphSchema,
resolveJavaScriptSemanticEvidence,
type JavaScriptSemanticGraph,
} from "./javascriptSemanticGraph.js";
import type { JavaScriptSemanticGraphNode } from "./javascriptSemanticGraphSchemas.js";
@@ -100,80 +103,111 @@ const node = (
kind: (typeof JAVASCRIPT_SEMANTIC_NODE_KINDS)[number],
role: string,
properties: Record<string, string | number | boolean | null> = {},
evidenceContexts: JavaScriptSemanticEvidenceContextRegistry,
options: {
readonly functionNodeId?: string | null;
readonly modulePath?: string;
} = {},
): JavaScriptSemanticGraphNode =>
createJavaScriptSemanticGraphNode({
kind,
identity: {
artifact_sha256: SHA,
module_path: options.modulePath ?? "bundle.js",
source_range: {
start: { line: 1, column: role.length },
end: { line: 1, column: role.length + 1 },
createJavaScriptSemanticGraphNode(
{
kind,
identity: {
artifact_sha256: SHA,
module_path: options.modulePath ?? "bundle.js",
source_range: {
start: { line: 1, column: role.length },
end: { line: 1, column: role.length + 1 },
},
role_key: role,
},
role_key: role,
function_node_id: options.functionNodeId ?? null,
application_node_ids: [],
label: role,
properties,
evidence: evidence(),
},
function_node_id: options.functionNodeId ?? null,
application_node_ids: [],
label: role,
properties,
evidence: evidence(),
});
evidenceContexts,
);
it("accepts semantic module paths longer than the former schema ceiling", () => {
const modulePath = `${"segment/".repeat(600)}entry.js`;
const evidenceContexts = new JavaScriptSemanticEvidenceContextRegistry();
expect(
node("module", "long-path", {}, { modulePath }).identity.module_path,
node("module", "long-path", {}, evidenceContexts, { modulePath }).identity
.module_path,
).toBe(modulePath);
});
const fixtureGraph = (withUnknown = false): JavaScriptSemanticGraph => {
const module = node("module", "module");
const literal = node("literal", "literal", { value: "TOKEN" });
const binding = node("binding", "binding");
const callable = node("function", "function");
const request = node("request", "request", {
endpoint: "https://example.invalid/v1",
});
const fixtureInput = (withUnknown = false): JavaScriptSemanticGraphInput => {
const evidenceContexts = new JavaScriptSemanticEvidenceContextRegistry();
const module = node("module", "module", {}, evidenceContexts);
const literal = node(
"literal",
"literal",
{ value: "TOKEN" },
evidenceContexts,
);
const binding = node("binding", "binding", {}, evidenceContexts);
const callable = node("function", "function", {}, evidenceContexts);
const request = node(
"request",
"request",
{
endpoint: "https://example.invalid/v1",
},
evidenceContexts,
);
const relations = [
createJavaScriptSemanticGraphRelation({
source_node_id: literal.node_id,
target_node_id: binding.node_id,
relation: "defines",
resolution: "resolved",
properties: {},
evidence: evidence("inferred"),
}),
createJavaScriptSemanticGraphRelation({
source_node_id: binding.node_id,
target_node_id: callable.node_id,
relation: "captures",
resolution: "resolved",
properties: {},
evidence: evidence("inferred"),
}),
createJavaScriptSemanticGraphRelation({
source_node_id: callable.node_id,
target_node_id: request.node_id,
relation: "constructs-request",
resolution: "resolved",
properties: {},
evidence: evidence("inferred"),
}),
createJavaScriptSemanticGraphRelation(
{
source_node_id: literal.node_id,
target_node_id: binding.node_id,
relation: "defines",
resolution: "resolved",
properties: {},
evidence: evidence("inferred"),
},
evidenceContexts,
),
createJavaScriptSemanticGraphRelation(
{
source_node_id: binding.node_id,
target_node_id: callable.node_id,
relation: "captures",
resolution: "resolved",
properties: {},
evidence: evidence("inferred"),
},
evidenceContexts,
),
createJavaScriptSemanticGraphRelation(
{
source_node_id: callable.node_id,
target_node_id: request.node_id,
relation: "constructs-request",
resolution: "resolved",
properties: {},
evidence: evidence("inferred"),
},
evidenceContexts,
),
];
const dynamic = createJavaScriptSemanticGraphUnknown({
node_id: callable.node_id,
family: "call-flow",
relation_kinds: ["calls"],
reason: "dynamic-call",
detail: "Computed callee is unresolved.",
candidate_node_ids: [],
evidence: unknownEvidence(),
});
const unknowns = withUnknown ? [dynamic] : [];
const dynamic = withUnknown
? createJavaScriptSemanticGraphUnknown(
{
node_id: callable.node_id,
family: "call-flow",
relation_kinds: ["calls"],
reason: "dynamic-call",
detail: "Computed callee is unresolved.",
candidate_node_ids: [],
evidence: unknownEvidence(),
},
evidenceContexts,
)
: null;
const unknowns = dynamic === null ? [] : [dynamic];
const coverageFamilies = JAVASCRIPT_SEMANTIC_RELATION_FAMILIES.map(
(family) => ({
family,
@@ -190,29 +224,33 @@ const fixtureGraph = (withUnknown = false): JavaScriptSemanticGraph => {
).length,
omitted_relations: withUnknown && family === "call-flow" ? null : 0,
unknown_ids:
withUnknown && family === "call-flow" ? [dynamic.unknown_id] : [],
dynamic !== null && family === "call-flow" ? [dynamic.unknown_id] : [],
}),
);
const fingerprint = createJavaScriptSemanticFingerprint({
function_node_id: callable.node_id,
algorithm: "rea.javascript-semantic-function",
status: "complete",
components: {
parameter_arity: 0,
normalized_ast_sha256: "1".repeat(64),
control_flow_sha256: "2".repeat(64),
relation_shape_sha256: "3".repeat(64),
literal_set_sha256: "4".repeat(64),
effects: ["network"],
const fingerprint = createJavaScriptSemanticFingerprint(
{
function_node_id: callable.node_id,
algorithm: "rea.javascript-semantic-function",
status: "complete",
components: {
parameter_arity: 0,
normalized_ast_sha256: "1".repeat(64),
control_flow_sha256: "2".repeat(64),
relation_shape_sha256: "3".repeat(64),
literal_set_sha256: "4".repeat(64),
effects: ["network"],
},
limitations: [],
evidence: evidence("inferred"),
},
limitations: [],
evidence: evidence("inferred"),
});
return createJavaScriptSemanticGraph({
evidenceContexts,
);
return {
schema: "JavaScriptSemanticRelationGraph",
root_artifact_sha256: SHA,
application_graph_id: JAG_ID,
root_node_ids: [module.node_id],
evidence_contexts: evidenceContexts.contexts,
nodes: [request, callable, binding, literal, module],
relations,
fingerprints: [fingerprint],
@@ -226,7 +264,24 @@ const fixtureGraph = (withUnknown = false): JavaScriptSemanticGraph => {
families: coverageFamilies,
},
limitations: withUnknown ? ["Dynamic call target remains unknown."] : [],
});
};
};
const fixtureGraph = (withUnknown = false): JavaScriptSemanticGraph =>
createJavaScriptSemanticGraph(fixtureInput(withUnknown));
const registryForGraph = (
graph: JavaScriptSemanticGraph,
): JavaScriptSemanticEvidenceContextRegistry => {
const registry = new JavaScriptSemanticEvidenceContextRegistry();
for (const { evidence: reference } of [
...graph.nodes,
...graph.relations,
...graph.unknowns,
...graph.fingerprints,
])
registry.intern(resolveJavaScriptSemanticEvidence(graph, reference));
return registry;
};
it("defines every required relation family and canonicalizes records", () => {
@@ -247,7 +302,6 @@ it("defines every required relation family and canonicalizes records", () => {
it("rejects stale identities, dangling endpoints, and incomplete coverage claims", () => {
const graph = fixtureGraph();
const { graph_id: _graphId, ...graphInput } = graph;
expect(() =>
parseJavaScriptSemanticGraph({
...graph,
@@ -256,10 +310,10 @@ it("rejects stale identities, dangling endpoints, and incomplete coverage claims
).toThrow();
expect(() =>
createJavaScriptSemanticGraph({
...graphInput,
...fixtureInput(),
relations: [
{
...graph.relations[0],
...fixtureInput().relations[0],
target_node_id: `jsrg_node_${"f".repeat(64)}`,
},
],
@@ -267,27 +321,174 @@ it("rejects stale identities, dangling endpoints, and incomplete coverage claims
).toThrow();
expect(() =>
createJavaScriptSemanticGraph({
...graphInput,
coverage: { ...graph.coverage, omitted_nodes: 1 },
...fixtureInput(),
coverage: { ...fixtureInput().coverage, omitted_nodes: 1 },
}),
).toThrow(/Complete graph coverage/u);
const graphWithUnknown = fixtureGraph(true);
const { graph_id: _unknownGraphId, ...unknownGraphInput } = graphWithUnknown;
const originalUnknown = graphWithUnknown.unknowns[0];
const unknownGraphInput = fixtureInput(true);
const originalUnknown = unknownGraphInput.unknowns[0];
if (originalUnknown === undefined)
throw new Error("Expected unknown frontier");
const { unknown_id: _unknownId, ...unknownInput } = originalUnknown;
expect(() =>
createJavaScriptSemanticGraph({
...unknownGraphInput,
unknowns: [
createJavaScriptSemanticGraphUnknown({
...unknownInput,
candidate_node_ids: [`jsrg_node_${"f".repeat(64)}`],
}),
const validatedUnknownGraph =
createJavaScriptSemanticGraph(unknownGraphInput);
const evidenceContexts = registryForGraph(validatedUnknownGraph);
unknownGraphInput.unknowns = [
createJavaScriptSemanticGraphUnknown(
{
...unknownInput,
candidate_node_ids: [`jsrg_node_${"f".repeat(64)}`],
evidence: resolveJavaScriptSemanticEvidence(
validatedUnknownGraph,
originalUnknown.evidence,
),
},
evidenceContexts,
),
];
unknownGraphInput.evidence_contexts = evidenceContexts.contexts;
expect(() => createJavaScriptSemanticGraph(unknownGraphInput)).toThrow(
/candidate node is absent/u,
);
});
it("validates evidence context ownership and references", () => {
const graph = fixtureGraph();
const alteredContext = {
...graph,
evidence_contexts: graph.evidence_contexts.map((context, index) =>
index === 0
? { ...context, context_id: `jsrg_evidence_${"f".repeat(64)}` }
: context,
),
};
const alteredIssues = javaScriptSemanticGraphSchema.safeParse(alteredContext);
expect(alteredIssues.success).toBe(false);
if (alteredIssues.success) throw new Error("Expected invalid context digest");
expect(alteredIssues.error.issues.map(({ message }) => message)).toContain(
"Evidence context identifier is stale",
);
expect(alteredIssues.error.issues.map(({ message }) => message)).toContain(
"Evidence reference names an absent context",
);
const changedPayload = javaScriptSemanticGraphSchema.safeParse({
...graph,
evidence_contexts: graph.evidence_contexts.map((context, index) =>
index === 0 ? { ...context, authority: "user-assertion" } : context,
),
});
expect(changedPayload.success).toBe(false);
if (changedPayload.success)
throw new Error("Expected changed context payload rejection");
expect(changedPayload.error.issues.map(({ message }) => message)).toContain(
"Evidence context identifier is stale",
);
const withUnknown = fixtureGraph(true);
const extraContext = withUnknown.evidence_contexts.find(
({ context_id }) =>
!graph.evidence_contexts.some(
(existing) => existing.context_id === context_id,
),
);
if (extraContext === undefined)
throw new Error("Expected the unknown fact to own a distinct context");
const orphanIssues = javaScriptSemanticGraphSchema.safeParse({
...graph,
evidence_contexts: [...graph.evidence_contexts, extraContext].toSorted(
(left, right) => left.context_id.localeCompare(right.context_id),
),
});
expect(orphanIssues.success).toBe(false);
if (orphanIssues.success)
throw new Error("Expected orphan context rejection");
expect(orphanIssues.error.issues.map(({ message }) => message)).toContain(
"Evidence context is not referenced by a graph fact",
);
for (const fact of [
"nodes",
"relations",
"unknowns",
"fingerprints",
] as const) {
const facts = withUnknown[fact];
const first = facts[0];
if (first === undefined) throw new Error(`Expected a ${fact} fact`);
const dangling = javaScriptSemanticGraphSchema.safeParse({
...withUnknown,
[fact]: [
{
...first,
evidence: {
...first.evidence,
context_id: `jsrg_evidence_${"e".repeat(64)}`,
},
},
...facts.slice(1),
],
}),
).toThrow(/candidate node is absent/u);
});
expect(dangling.success, fact).toBe(false);
if (dangling.success)
throw new Error(`Expected dangling ${fact} reference`);
expect(
dangling.error.issues.map(({ message }) => message),
fact,
).toContain("Evidence reference names an absent context");
}
const observedNode = graph.nodes[0];
if (observedNode === undefined) throw new Error("Expected observed node");
const impossibleEvidence = javaScriptSemanticGraphSchema.safeParse({
...graph,
nodes: [
{
...observedNode,
evidence: {
...observedNode.evidence,
location: {
available: false,
reason: "not-observed",
detail: "Location removed from an observed fact.",
},
},
},
...graph.nodes.slice(1),
],
});
expect(impossibleEvidence.success).toBe(false);
if (impossibleEvidence.success)
throw new Error("Expected incompatible evidence context and location");
expect(
impossibleEvidence.error.issues.map(({ message }) => message),
).toContain("Evidence context is incompatible with its fact location");
});
it("shares provenance contexts while preserving fact-specific locations and unknown IDs", () => {
const graph = fixtureGraph(true);
const first = graph.unknowns[0];
if (first === undefined) throw new Error("Expected an unknown frontier");
const evidenceContexts = registryForGraph(graph);
const { unknown_id: _unknownId, ...unknown } = first;
const second = createJavaScriptSemanticGraphUnknown(
{
...unknown,
evidence: {
...resolveJavaScriptSemanticEvidence(graph, first.evidence),
location: {
available: false,
reason: "unresolved",
detail: "Another source location remains unresolved.",
},
},
},
evidenceContexts,
);
expect(second.evidence.context_id).toBe(first.evidence.context_id);
expect(second.evidence.location).not.toEqual(first.evidence.location);
expect(second.unknown_id).not.toBe(first.unknown_id);
expect(evidenceContexts.contexts).toEqual(graph.evidence_contexts);
});
const complete = <Value>(steps: Iterator<void, Value>): Value => {
@@ -298,7 +499,7 @@ const complete = <Value>(steps: Iterator<void, Value>): Value => {
};
it("captures nested input before yielding and preserves the synchronous graph commitment", () => {
const { graph_id: _graphId, ...input } = fixtureGraph(true);
const input = fixtureInput(true);
const node = input.nodes[0];
if (node === undefined) throw new Error("Expected fixture node");
const nested = { text: 'Unicode: \u{1f600} \ud800 " \\ ', rows: [1, 2, 3] };
@@ -346,17 +547,16 @@ const invalidGraphCases: {
message: "Node identifier is stale",
},
{
label: "relation identity, endpoints and authority",
label: "relation endpoints and authority",
change: (input) => {
input.relations = input.relations.map((relation) => ({
...relation,
source_node_id: `jsrg_node_${"f".repeat(64)}`,
target_node_id: `jsrg_node_${"f".repeat(64)}`,
resolution: "candidate",
evidence: evidence(),
}));
},
message: "Relation identifier is stale",
message: "Relation endpoints must name semantic nodes",
},
{
label: "unknown identity and candidates",
@@ -364,7 +564,6 @@ const invalidGraphCases: {
input.unknowns = input.unknowns.map((unknown) => ({
...unknown,
candidate_node_ids: [`jsrg_node_${"f".repeat(64)}`],
evidence: evidence(),
}));
},
message: "Unknown frontier candidate node is absent",
@@ -398,7 +597,7 @@ const invalidGraphCases: {
it.each(invalidGraphCases)(
"retains full validation diagnostics for $label in owned steps",
({ change, message }) => {
const { graph_id: _graphId, ...input } = fixtureGraph(true);
const input = fixtureInput(true);
change(input);
const issues = (action: () => unknown) => {
try {
@@ -1,6 +1,10 @@
import { z } from "zod";
import { applicationGraphEvidenceSchema } from "./javascriptApplicationEvidenceSchemas.js";
import {
applicationGraphEvidenceContextSchema,
applicationGraphEvidenceLocationSchema,
applicationGraphEvidenceSchema,
} from "./javascriptApplicationEvidenceSchemas.js";
import { jsonValueSchema } from "../jsonValue.js";
import { digestSchema } from "../digests.js";
import { prefixedDigestSchema } from "../digests.js";
@@ -13,6 +17,21 @@ const semanticRelationIdSchema = z
const semanticUnknownIdSchema = z
.string()
.regex(/^jsrg_unknown_[a-f0-9]{64}$/u);
const semanticEvidenceContextIdSchema = z
.string()
.regex(/^jsrg_evidence_[a-f0-9]{64}$/u);
/** One canonical owner for provenance shared by semantic graph facts. */
export const javaScriptSemanticEvidenceContextSchema =
applicationGraphEvidenceContextSchema.extend({
context_id: semanticEvidenceContextIdSchema,
});
/** Location-specific reference to one exact graph-level evidence context. */
export const javaScriptSemanticEvidenceReferenceSchema = z.strictObject({
context_id: semanticEvidenceContextIdSchema,
location: applicationGraphEvidenceLocationSchema,
});
/** Semantic entity kinds admitted by JavaScript Semantic Relation Graph. */
export const JAVASCRIPT_SEMANTIC_NODE_KINDS = [
@@ -213,6 +232,7 @@ export const javaScriptSemanticNodeInputSchema = z.strictObject({
/** One exact, artifact-version semantic entity. */
export const javaScriptSemanticNodeSchema =
javaScriptSemanticNodeInputSchema.extend({
evidence: javaScriptSemanticEvidenceReferenceSchema,
node_id: semanticNodeIdSchema,
identifier_strategy: z.strictObject({
strategy: z.literal("semantic-content-sha256"),
@@ -233,6 +253,7 @@ export const javaScriptSemanticRelationInputSchema = z.strictObject({
/** One canonical directed semantic relationship. */
export const javaScriptSemanticRelationSchema =
javaScriptSemanticRelationInputSchema.extend({
evidence: javaScriptSemanticEvidenceReferenceSchema,
relation_id: semanticRelationIdSchema,
identifier_strategy: z.strictObject({
strategy: z.literal("semantic-content-sha256"),
@@ -269,6 +290,7 @@ export const javaScriptSemanticUnknownInputSchema = z.strictObject({
/** One canonical unresolved semantic frontier. */
export const javaScriptSemanticUnknownSchema =
javaScriptSemanticUnknownInputSchema.extend({
evidence: javaScriptSemanticEvidenceReferenceSchema,
unknown_id: semanticUnknownIdSchema,
});
@@ -304,6 +326,7 @@ export const javaScriptSemanticFingerprintInputSchema = z.strictObject({
/** One canonical function fingerprint record. */
export const javaScriptSemanticFingerprintSchema =
javaScriptSemanticFingerprintInputSchema.extend({
evidence: javaScriptSemanticEvidenceReferenceSchema,
fingerprint_id: prefixedDigestSchema("jsrg_fingerprint"),
fingerprint_sha256: digestSchema,
});
@@ -337,6 +360,7 @@ export const javaScriptSemanticGraphInputSchema = z.strictObject({
root_artifact_sha256: digestSchema,
application_graph_id: prefixedDigestSchema("jag"),
root_node_ids: z.array(semanticNodeIdSchema).min(1),
evidence_contexts: z.array(javaScriptSemanticEvidenceContextSchema),
nodes: z.array(javaScriptSemanticNodeSchema).min(1),
relations: z.array(javaScriptSemanticRelationSchema),
fingerprints: z.array(javaScriptSemanticFingerprintSchema),
@@ -355,6 +379,14 @@ export const javaScriptSemanticGraphRecordSchema =
export type JavaScriptSemanticGraphNode = z.infer<
typeof javaScriptSemanticNodeSchema
>;
/** Compact provenance on one graph fact. */
export type JavaScriptSemanticEvidenceReference = z.infer<
typeof javaScriptSemanticEvidenceReferenceSchema
>;
/** Content-addressed graph-level provenance context. */
export type JavaScriptSemanticEvidenceContext = z.infer<
typeof javaScriptSemanticEvidenceContextSchema
>;
/** Canonical semantic relation. */
export type JavaScriptSemanticGraphRelation = z.infer<
typeof javaScriptSemanticRelationSchema
@@ -363,6 +395,10 @@ export type JavaScriptSemanticGraphRelation = z.infer<
export type JavaScriptSemanticGraphUnknown = z.infer<
typeof javaScriptSemanticUnknownSchema
>;
/** Canonical function fingerprint with a compact evidence reference. */
export type JavaScriptSemanticFingerprint = z.infer<
typeof javaScriptSemanticFingerprintSchema
>;
/** Canonical graph content before graph ID derivation. */
export type JavaScriptSemanticGraphInput = z.infer<
typeof javaScriptSemanticGraphInputSchema
@@ -5,6 +5,7 @@ import {
JAVASCRIPT_SEMANTIC_NODE_KINDS,
JAVASCRIPT_SEMANTIC_RELATION_FAMILIES,
JAVASCRIPT_SEMANTIC_RELATION_FAMILY,
type JavaScriptSemanticGraphInput,
} from "./javascriptSemanticGraphSchemas.js";
import {
createJavaScriptSemanticFingerprint,
@@ -12,6 +13,8 @@ import {
createJavaScriptSemanticGraphNode,
createJavaScriptSemanticGraphRelation,
createJavaScriptSemanticGraphUnknown,
JavaScriptSemanticEvidenceContextRegistry,
resolveJavaScriptSemanticEvidence,
type JavaScriptSemanticGraph,
} from "./javascriptSemanticGraph.js";
import type {
@@ -96,71 +99,92 @@ const unknownEvidence = (): ApplicationGraphEvidence => ({
const node = (
kind: (typeof JAVASCRIPT_SEMANTIC_NODE_KINDS)[number],
role: string,
evidenceContexts: JavaScriptSemanticEvidenceContextRegistry,
properties: Record<string, JsonValue> = {},
functionNodeId: string | null = null,
): JavaScriptSemanticGraphNode =>
createJavaScriptSemanticGraphNode({
kind,
identity: {
artifact_sha256: SHA,
module_path: "bundle.js",
source_range: {
start: { line: 1, column: role.length },
end: { line: 1, column: role.length + 1 },
createJavaScriptSemanticGraphNode(
{
kind,
identity: {
artifact_sha256: SHA,
module_path: "bundle.js",
source_range: {
start: { line: 1, column: role.length },
end: { line: 1, column: role.length + 1 },
},
role_key: role,
},
role_key: role,
function_node_id: functionNodeId,
application_node_ids: [],
label: role,
properties,
evidence: evidence(),
},
function_node_id: functionNodeId,
application_node_ids: [],
label: role,
properties,
evidence: evidence(),
});
evidenceContexts,
);
const fixtureGraph = (withUnknown = false): JavaScriptSemanticGraph => {
const module = node("module", "module");
const literal = node("literal", "literal", { value: "TOKEN" });
const binding = node("binding", "binding");
const callable = node("function", "function");
const request = node("request", "request", {
const evidenceContexts = new JavaScriptSemanticEvidenceContextRegistry();
const module = node("module", "module", evidenceContexts);
const literal = node("literal", "literal", evidenceContexts, {
value: "TOKEN",
});
const binding = node("binding", "binding", evidenceContexts);
const callable = node("function", "function", evidenceContexts);
const request = node("request", "request", evidenceContexts, {
endpoint: "https://example.invalid/v1",
});
const relations = [
createJavaScriptSemanticGraphRelation({
source_node_id: literal.node_id,
target_node_id: binding.node_id,
relation: "defines",
resolution: "resolved",
properties: {},
evidence: evidence("inferred"),
}),
createJavaScriptSemanticGraphRelation({
source_node_id: binding.node_id,
target_node_id: callable.node_id,
relation: "captures",
resolution: "resolved",
properties: {},
evidence: evidence("inferred"),
}),
createJavaScriptSemanticGraphRelation({
source_node_id: callable.node_id,
target_node_id: request.node_id,
relation: "constructs-request",
resolution: "resolved",
properties: {},
evidence: evidence("inferred"),
}),
createJavaScriptSemanticGraphRelation(
{
source_node_id: literal.node_id,
target_node_id: binding.node_id,
relation: "defines",
resolution: "resolved",
properties: {},
evidence: evidence("inferred"),
},
evidenceContexts,
),
createJavaScriptSemanticGraphRelation(
{
source_node_id: binding.node_id,
target_node_id: callable.node_id,
relation: "captures",
resolution: "resolved",
properties: {},
evidence: evidence("inferred"),
},
evidenceContexts,
),
createJavaScriptSemanticGraphRelation(
{
source_node_id: callable.node_id,
target_node_id: request.node_id,
relation: "constructs-request",
resolution: "resolved",
properties: {},
evidence: evidence("inferred"),
},
evidenceContexts,
),
];
const dynamic = createJavaScriptSemanticGraphUnknown({
node_id: callable.node_id,
family: "call-flow",
relation_kinds: ["calls"],
reason: "dynamic-call",
detail: "Computed callee is unresolved.",
candidate_node_ids: [],
evidence: unknownEvidence(),
});
const unknowns = withUnknown ? [dynamic] : [];
const dynamic = withUnknown
? createJavaScriptSemanticGraphUnknown(
{
node_id: callable.node_id,
family: "call-flow",
relation_kinds: ["calls"],
reason: "dynamic-call",
detail: "Computed callee is unresolved.",
candidate_node_ids: [],
evidence: unknownEvidence(),
},
evidenceContexts,
)
: null;
const unknowns = dynamic === null ? [] : [dynamic];
const coverageFamilies = JAVASCRIPT_SEMANTIC_RELATION_FAMILIES.map(
(family) => ({
family,
@@ -177,29 +201,33 @@ const fixtureGraph = (withUnknown = false): JavaScriptSemanticGraph => {
).length,
omitted_relations: withUnknown && family === "call-flow" ? null : 0,
unknown_ids:
withUnknown && family === "call-flow" ? [dynamic.unknown_id] : [],
dynamic !== null && family === "call-flow" ? [dynamic.unknown_id] : [],
}),
);
const fingerprint = createJavaScriptSemanticFingerprint({
function_node_id: callable.node_id,
algorithm: "rea.javascript-semantic-function",
status: "complete",
components: {
parameter_arity: 0,
normalized_ast_sha256: "1".repeat(64),
control_flow_sha256: "2".repeat(64),
relation_shape_sha256: "3".repeat(64),
literal_set_sha256: "4".repeat(64),
effects: ["network"],
const fingerprint = createJavaScriptSemanticFingerprint(
{
function_node_id: callable.node_id,
algorithm: "rea.javascript-semantic-function",
status: "complete",
components: {
parameter_arity: 0,
normalized_ast_sha256: "1".repeat(64),
control_flow_sha256: "2".repeat(64),
relation_shape_sha256: "3".repeat(64),
literal_set_sha256: "4".repeat(64),
effects: ["network"],
},
limitations: [],
evidence: evidence("inferred"),
},
limitations: [],
evidence: evidence("inferred"),
});
evidenceContexts,
);
return createJavaScriptSemanticGraph({
schema: "JavaScriptSemanticRelationGraph",
root_artifact_sha256: SHA,
application_graph_id: JAG_ID,
root_node_ids: [module.node_id],
evidence_contexts: evidenceContexts.contexts,
nodes: [request, callable, binding, literal, module],
relations,
fingerprints: [fingerprint],
@@ -218,22 +246,27 @@ const fixtureGraph = (withUnknown = false): JavaScriptSemanticGraph => {
const graphWithCandidateEdge = (): JavaScriptSemanticGraph => {
const graph = fixtureGraph();
const input = constructionInput(graph);
const evidenceContexts = registryForGraph(graph);
const literal = graph.nodes.find(({ kind }) => kind === "literal");
const request = graph.nodes.find(({ kind }) => kind === "request");
if (literal === undefined || request === undefined)
throw new TypeError("Semantic fixture nodes are missing");
const candidate = createJavaScriptSemanticGraphRelation({
source_node_id: literal.node_id,
target_node_id: request.node_id,
relation: "supplies-request-field",
resolution: "candidate",
properties: { field: "authorization" },
evidence: evidence("inferred"),
});
const { graph_id: _graphId, ...input } = graph;
const candidate = createJavaScriptSemanticGraphRelation(
{
source_node_id: literal.node_id,
target_node_id: request.node_id,
relation: "supplies-request-field",
resolution: "candidate",
properties: { field: "authorization" },
evidence: evidence("inferred"),
},
evidenceContexts,
);
return createJavaScriptSemanticGraph({
...input,
relations: [...graph.relations, candidate],
evidence_contexts: evidenceContexts.contexts,
relations: [...input.relations, candidate],
coverage: {
...graph.coverage,
families: graph.coverage.families.map((family) =>
@@ -248,6 +281,27 @@ const graphWithCandidateEdge = (): JavaScriptSemanticGraph => {
});
};
const constructionInput = (
graph: JavaScriptSemanticGraph,
): JavaScriptSemanticGraphInput => {
const { graph_id: _graphId, ...input } = graph;
return input;
};
const registryForGraph = (
graph: JavaScriptSemanticGraph,
): JavaScriptSemanticEvidenceContextRegistry => {
const registry = new JavaScriptSemanticEvidenceContextRegistry();
for (const { evidence: reference } of [
...graph.nodes,
...graph.relations,
...graph.unknowns,
...graph.fingerprints,
])
registry.intern(resolveJavaScriptSemanticEvidence(graph, reference));
return registry;
};
it("returns every relevant unknown inline", () => {
const graph = fixtureGraph(true);
const callable = graph.nodes.find(({ kind }) => kind === "function");
@@ -279,28 +333,55 @@ it("returns the complete deterministic forward influence result inline", () => {
expect(first.relations).toHaveLength(3);
});
it("returns only the referenced evidence contexts with resolvable trace facts", () => {
const graph = fixtureGraph();
const result = queryJavaScriptSemanticGraph(graph, {
seed: { kind: "literal", value: "TOKEN" },
direction: "forward-influence",
});
const expectedContextIds = [
...new Set([
...result.nodes.map(({ evidence }) => evidence.context_id),
...result.relations.map(({ evidence }) => evidence.context_id),
...result.unknowns.map(({ evidence }) => evidence.context_id),
]),
].sort();
expect(result.evidence_contexts.map(({ context_id }) => context_id)).toEqual(
expectedContextIds,
);
for (const node of result.nodes)
expect(resolveJavaScriptSemanticEvidence(result, node.evidence)).toEqual(
resolveJavaScriptSemanticEvidence(graph, node.evidence),
);
});
it("traverses paths beyond the former fixed depth ceiling", () => {
const original = fixtureGraph();
const input = constructionInput(original);
const evidenceContexts = registryForGraph(original);
const request = original.nodes.find(({ kind }) => kind === "request");
if (request === undefined) throw new TypeError("Missing request node");
const chain = Array.from({ length: 70 }, (_, index) =>
node("binding", `long-chain-${index}`),
node("binding", `long-chain-${index}`, evidenceContexts),
);
const chainRelations = chain.map((target, index) =>
createJavaScriptSemanticGraphRelation({
source_node_id: chain[index - 1]?.node_id ?? request.node_id,
target_node_id: target.node_id,
relation: "reads",
resolution: "resolved",
properties: {},
evidence: evidence("inferred"),
}),
createJavaScriptSemanticGraphRelation(
{
source_node_id: chain[index - 1]?.node_id ?? request.node_id,
target_node_id: target.node_id,
relation: "reads",
resolution: "resolved",
properties: {},
evidence: evidence("inferred"),
},
evidenceContexts,
),
);
const { graph_id: _graphId, ...input } = original;
const graph = createJavaScriptSemanticGraph({
...input,
nodes: [...original.nodes, ...chain],
relations: [...original.relations, ...chainRelations],
evidence_contexts: evidenceContexts.contexts,
nodes: [...input.nodes, ...chain],
relations: [...input.relations, ...chainRelations],
coverage: {
...original.coverage,
families: original.coverage.families.map((family) =>
@@ -329,7 +410,12 @@ it("accepts deeply nested semantic properties", () => {
for (let index = 0; index < 12; index += 1)
nested = { [`level-${String(index)}`]: nested };
const result = node("binding", "deep-properties", { nested });
const result = node(
"binding",
"deep-properties",
new JavaScriptSemanticEvidenceContextRegistry(),
{ nested },
);
expect(result.properties).toEqual({ nested });
});
@@ -369,41 +455,50 @@ it("excludes candidate edges unless the caller explicitly opts in", () => {
});
it("retains every structural application node linked to one semantic node", () => {
const original = node("module", "module");
const linked = createJavaScriptSemanticGraphNode({
kind: original.kind,
identity: original.identity,
function_node_id: original.function_node_id,
application_node_ids: Array.from(
{ length: 65 },
(_, index) => `jag_node_${index.toString(16).padStart(64, "0")}`,
),
label: original.label,
properties: original.properties,
evidence: original.evidence,
});
const evidenceContexts = new JavaScriptSemanticEvidenceContextRegistry();
const original = node("module", "module", evidenceContexts);
const linked = createJavaScriptSemanticGraphNode(
{
kind: original.kind,
identity: original.identity,
function_node_id: original.function_node_id,
application_node_ids: Array.from(
{ length: 65 },
(_, index) => `jag_node_${index.toString(16).padStart(64, "0")}`,
),
label: original.label,
properties: original.properties,
evidence: evidenceContexts.resolve(original.evidence),
},
evidenceContexts,
);
expect(linked.application_node_ids).toHaveLength(65);
});
it("retains all explicit unresolved candidate node identifiers", () => {
const unknown = createJavaScriptSemanticGraphUnknown({
node_id: null,
family: "call-flow",
relation_kinds: ["calls"],
reason: "ambiguous-target",
detail: "All candidate targets are retained.",
candidate_node_ids: Array.from(
{ length: 1_001 },
(_, index) => `jsrg_node_${index.toString(16).padStart(64, "0")}`,
),
evidence: unknownEvidence(),
});
const evidenceContexts = new JavaScriptSemanticEvidenceContextRegistry();
const unknown = createJavaScriptSemanticGraphUnknown(
{
node_id: null,
family: "call-flow",
relation_kinds: ["calls"],
reason: "ambiguous-target",
detail: "All candidate targets are retained.",
candidate_node_ids: Array.from(
{ length: 1_001 },
(_, index) => `jsrg_node_${index.toString(16).padStart(64, "0")}`,
),
evidence: unknownEvidence(),
},
evidenceContexts,
);
expect(unknown.candidate_node_ids).toHaveLength(1_001);
});
const directionalGraph = (
nodes: readonly JavaScriptSemanticGraphNode[],
relations: readonly JavaScriptSemanticGraphRelation[],
evidenceContexts: JavaScriptSemanticEvidenceContextRegistry,
partial = false,
): JavaScriptSemanticGraph =>
createJavaScriptSemanticGraph({
@@ -411,6 +506,7 @@ const directionalGraph = (
root_artifact_sha256: SHA,
application_graph_id: JAG_ID,
root_node_ids: nodes.slice(0, 1).map(({ node_id }) => node_id),
evidence_contexts: evidenceContexts.contexts,
nodes: [...nodes],
relations: [...relations],
fingerprints: [],
@@ -439,16 +535,20 @@ const relationBetween = (
source: JavaScriptSemanticGraphNode,
target: JavaScriptSemanticGraphNode,
relation: JavaScriptSemanticGraphRelation["relation"],
evidenceContexts: JavaScriptSemanticEvidenceContextRegistry,
resolution: JavaScriptSemanticGraphRelation["resolution"] = "resolved",
) =>
createJavaScriptSemanticGraphRelation({
source_node_id: source.node_id,
target_node_id: target.node_id,
relation,
resolution,
properties: {},
evidence: evidence("inferred"),
});
createJavaScriptSemanticGraphRelation(
{
source_node_id: source.node_id,
target_node_id: target.node_id,
relation,
resolution,
properties: {},
evidence: evidence("inferred"),
},
evidenceContexts,
);
it.each([
{
@@ -489,25 +589,28 @@ it.each([
] as const)(
"limits $direction ambiguity to traversable $relation candidates (incoming=$incoming)",
({ direction, relation, incoming, relevant }) => {
const seed = node("function", "directional-seed");
const reached = node("function", "directional-reached");
const other = node("function", "directional-other");
const evidenceContexts = new JavaScriptSemanticEvidenceContextRegistry();
const seed = node("function", "directional-seed", evidenceContexts);
const reached = node("function", "directional-reached", evidenceContexts);
const other = node("function", "directional-other", evidenceContexts);
const reverse =
direction === "callers" || direction === "backward-provenance";
const resolved = relationBetween(
reverse ? reached : seed,
reverse ? seed : reached,
direction === "ownership" ? "owns" : "calls",
evidenceContexts,
);
const candidate = relationBetween(
incoming ? other : reached,
incoming ? reached : other,
relation,
evidenceContexts,
"candidate",
);
const nodes = [seed, reached, other];
const control = queryJavaScriptSemanticGraph(
directionalGraph(nodes, [resolved]),
directionalGraph(nodes, [resolved], evidenceContexts),
{
seed: { kind: "semantic-node", node_id: seed.node_id },
direction,
@@ -519,7 +622,7 @@ it.each([
});
for (const include of [false, true]) {
const result = queryJavaScriptSemanticGraph(
directionalGraph(nodes, [resolved, candidate]),
directionalGraph(nodes, [resolved, candidate], evidenceContexts),
{
seed: { kind: "semantic-node", node_id: seed.node_id },
direction,
@@ -535,19 +638,33 @@ it.each([
expect(result.relations).toEqual(control.relations);
} else {
expect(result.nodes).toHaveLength(3);
expect(result.relations).toContainEqual(candidate);
expect(result.relations).toContainEqual(
expect.objectContaining({
source_node_id: candidate.source_node_id,
target_node_id: candidate.target_node_id,
relation: candidate.relation,
resolution: candidate.resolution,
}),
);
}
}
},
);
it("excludes explicitly filtered candidates without hiding partial graph coverage", () => {
const seed = node("function", "filtered-seed");
const other = node("function", "filtered-other");
const candidate = relationBetween(seed, other, "reads", "candidate");
const evidenceContexts = new JavaScriptSemanticEvidenceContextRegistry();
const seed = node("function", "filtered-seed", evidenceContexts);
const other = node("function", "filtered-other", evidenceContexts);
const candidate = relationBetween(
seed,
other,
"reads",
evidenceContexts,
"candidate",
);
for (const partial of [false, true]) {
const result = queryJavaScriptSemanticGraph(
directionalGraph([seed, other], [candidate], partial),
directionalGraph([seed, other], [candidate], evidenceContexts, partial),
{
seed: { kind: "semantic-node", node_id: seed.node_id },
direction: "forward-influence",
@@ -563,8 +680,9 @@ it("excludes explicitly filtered candidates without hiding partial graph coverag
});
it("keeps candidate uncertainty at the end of a long reachable path", () => {
const evidenceContexts = new JavaScriptSemanticEvidenceContextRegistry();
const chain = Array.from({ length: 72 }, (_, index) =>
node("binding", `candidate-chain-${index}`),
node("binding", `candidate-chain-${index}`, evidenceContexts),
);
const relations = chain.slice(1).map((target, index) => {
const source = chain[index];
@@ -573,6 +691,7 @@ it("keeps candidate uncertainty at the end of a long reachable path", () => {
source,
target,
"reads",
evidenceContexts,
index === 70 ? "candidate" : "resolved",
);
});
@@ -580,7 +699,7 @@ it("keeps candidate uncertainty at the end of a long reachable path", () => {
if (seed === undefined) throw new TypeError("Missing chain seed");
for (const include of [false, true]) {
const result = queryJavaScriptSemanticGraph(
directionalGraph(chain, relations),
directionalGraph(chain, relations, evidenceContexts),
{
seed: { kind: "semantic-node", node_id: seed.node_id },
direction: "forward-influence",
@@ -68,6 +68,11 @@ export const queryJavaScriptSemanticGraph = (
result.retainedRelations,
input,
);
const retainedContextIds = new Set([
...result.retainedNodes.map(({ evidence }) => evidence.context_id),
...result.retainedRelations.map(({ evidence }) => evidence.context_id),
...relevantUnknowns.map(({ evidence }) => evidence.context_id),
]);
const assessment = assessJavaScriptSemanticQuery({
graph,
totalSeeds: seeds.length,
@@ -83,6 +88,9 @@ export const queryJavaScriptSemanticGraph = (
direction: input.direction,
status: assessment.status,
seed_node_ids: retainedSeeds,
evidence_contexts: graph.evidence_contexts.filter(({ context_id }) =>
retainedContextIds.has(context_id),
),
nodes: result.retainedNodes,
relations: result.retainedRelations,
unknowns: relevantUnknowns,
@@ -5,6 +5,7 @@ import {
JAVASCRIPT_SEMANTIC_RELATIONS,
javaScriptSemanticNodeSchema,
javaScriptSemanticRelationSchema,
javaScriptSemanticEvidenceContextSchema,
javaScriptSemanticUnknownSchema,
} from "./javascriptSemanticGraphSchemas.js";
import { jsonValueSchema } from "../jsonValue.js";
@@ -93,6 +94,7 @@ export const javaScriptSemanticQueryResultSchema = z.strictObject({
direction: javaScriptSemanticQueryInputSchema.shape.direction,
status: z.enum(["found", "no-match", "ambiguous", "partial", "unsupported"]),
seed_node_ids: z.array(semanticNodeIdSchema),
evidence_contexts: z.array(javaScriptSemanticEvidenceContextSchema),
nodes: z.array(javaScriptSemanticNodeSchema),
relations: z.array(javaScriptSemanticRelationSchema),
unknowns: z.array(javaScriptSemanticUnknownSchema),
@@ -88,7 +88,10 @@ it("traces example semantic modules with their parsed artifact digests", async (
const module = analysis.semantic_graph.nodes.find(
({ kind }) => kind === "module",
);
if (module === undefined || !module.evidence.artifact.available)
const evidenceContext = analysis.semantic_graph.evidence_contexts.find(
({ context_id }) => context_id === module?.evidence.context_id,
);
if (module === undefined || !evidenceContext?.artifact.available)
throw new Error("Example must retain an artifact-backed semantic module");
const response = await client.callTool({
name: "trace_javascript_semantics",
@@ -103,11 +106,19 @@ it("traces example semantic modules with their parsed artifact digests", async (
expect(response.isError).not.toBe(true);
expect(response.structuredContent).toMatchObject({
normalized_result: {
evidence_contexts: expect.arrayContaining([
expect.objectContaining({
context_id: module.evidence.context_id,
artifact: expect.objectContaining({
sha256: evidenceContext.artifact.sha256,
}),
}),
]),
nodes: expect.arrayContaining([
expect.objectContaining({
node_id: module.node_id,
identity: expect.objectContaining({
artifact_sha256: module.evidence.artifact.sha256,
artifact_sha256: evidenceContext.artifact.sha256,
}),
}),
]),