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
@@ -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 {
|
||||
|
||||
Reference in new issue
Block a user