mirror of
https://github.com/morluto/rea.git
synced 2026-10-26 17:35:11 +00:00
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:
1 parent
9c4a68bc19
commit
e3bbb9ac45
25 files changed
+1109
-464
No files matched your search
@@ -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: [],
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
}),
|
||||
}),
|
||||
]),
|
||||
|
||||
Reference in new issue
Block a user