perf(javascript): reuse validated immutable graphs (#1112)

* perf(javascript): reuse validated immutable graphs

* fix(ci): follow the moved process CLI boundary test
This commit is contained in:
Ysopwnable
2026-10-09 02:40:21 +08:00
committed by GitHub
parent 1f5be750bc
commit ec6db0f063
11 changed files with 582 additions and 117 deletions
+1 -1
View File
@@ -334,7 +334,7 @@ jobs:
- name: Process filesystem snapshot descriptor regressions
run: npm run test:focused -- src/process/capture/FilesystemSnapshot.test.ts
- name: Native Darwin ownership and process cleanup regressions
run: npm run test:focused -- tests/boundary/process/darwinProcessRunTokenReader.test.ts tests/boundary/process/processCaptureLifecycle.test.ts tests/boundary/cli/processCli.test.ts tests/boundary/mcp/sessionMcp.test.ts
run: npm run test:focused -- tests/boundary/process/darwinProcessRunTokenReader.test.ts tests/boundary/process/processCaptureLifecycle.test.ts tests/boundary/process/processCli.test.ts tests/boundary/mcp/sessionMcp.test.ts
reconstruction-readiness:
name: Reconstruction readiness
+6 -3
View File
@@ -86,11 +86,14 @@ Derived comparisons and reconstruction verification yield before computation
and before publication, so cancellation cannot race with successful Evidence.
`analyze_javascript_application` also yields between reconstruction phases and
during complete-result sealing, Evidence JSON validation, and canonical hashing.
during graph/result sealing, cross-graph binding checks, Evidence JSON validation,
and canonical hashing. Its final result validation reuses exact graphs whose
owned constructors validated and completely sealed them; imported graphs still
receive full schema and commitment checks.
Cancellation observed before completion returns `cancelled` and prevents the
provisional result from entering the session ledger; prior Evidence stays usable.
Single-file parsing, graph completion, and full application-result schema
validation still run synchronously, so control messages can wait for those
Single-file parsing, graph construction, and validation of imported graphs
still run synchronously, so control messages can wait for those
phases to release the event loop. A rejected client promise alone does not
establish that the server has stopped its work.
@@ -10,7 +10,9 @@ describe("JavaScript analysis cancellation before publication", () => {
it.each([
"parse_javascript_source",
"build_javascript_application_graph",
"seal_javascript_application_graph",
"build_javascript_semantic_graph",
"seal_javascript_semantic_graph",
"validate_javascript_application_result",
"create_javascript_application_evidence",
"seal_javascript_application_result",
@@ -3,7 +3,7 @@ import { z } from "zod";
import { ArtifactReaderFailure } from "../../artifacts/ArtifactReader.js";
import {
analyzeJavaScriptApplicationInputSchema,
javascriptApplicationAnalysisResultSchema,
parseOwnedJavaScriptApplicationAnalysisSteps,
} from "../../domain/javascript/javascriptApplicationAnalysis.js";
import { AnalysisOutputError } from "../../domain/analysisErrorCore.js";
import { ArtifactOperationError } from "../../domain/artifactOperationError.js";
@@ -17,6 +17,7 @@ import { createOwnedJavaScriptApplicationEvidenceCooperatively } from "./JavaScr
import {
assertJavaScriptAnalysisActive,
checkpointJavaScriptAnalysis,
completeJavaScriptAnalysisSteps,
} from "./JavaScriptAnalysisControl.js";
import { reconstructJavaScriptArtifact } from "./JavaScriptArtifactReconstruction.js";
import { JAVASCRIPT_APPLICATION_PROVIDER } from "../InvestigationProviders.js";
@@ -63,14 +64,17 @@ export const analyzeJavaScriptApplicationValidated = async (
phase: "validate_javascript_application_result",
completed: 0,
total: 1,
message: "Validating the application analysis result",
message: "Validating result metadata and cross-graph bindings",
});
await checkpointJavaScriptAnalysis(options.signal);
const result = javascriptApplicationAnalysisResultSchema.parse({
...application,
summary,
limitations: reconstructed.graph.limitations,
});
const result = await completeJavaScriptAnalysisSteps(
parseOwnedJavaScriptApplicationAnalysisSteps({
...application,
summary,
limitations: reconstructed.graph.limitations,
}),
options.signal,
);
await options.progress?.report({
phase: "create_javascript_application_evidence",
completed: 0,
@@ -1,6 +1,7 @@
import type { ArtifactInventorySnapshot } from "../../artifacts/inventory/ArtifactInventory.js";
import {
createJavaScriptApplicationGraph,
createImmutableJavaScriptApplicationGraphSteps,
type JavaScriptApplicationGraph,
} from "../../domain/javascript/javascriptApplicationGraph.js";
import type { JavaScriptModuleArtifactAnalysis } from "./JavaScriptArtifactAnalysisTypes.js";
@@ -46,7 +47,26 @@ export const buildJavaScriptArtifactGraph = (
snapshot: ArtifactInventorySnapshot,
fileSet: JavaScriptArtifactFileSet,
analysis: JavaScriptModuleArtifactAnalysis,
): JavaScriptApplicationGraph => {
): JavaScriptApplicationGraph =>
createJavaScriptApplicationGraph(
buildJavaScriptArtifactGraphInput(snapshot, fileSet, analysis),
);
/** Build a validated application graph with cooperative immutable ownership. */
export const buildImmutableJavaScriptArtifactGraphSteps = (
snapshot: ArtifactInventorySnapshot,
fileSet: JavaScriptArtifactFileSet,
analysis: JavaScriptModuleArtifactAnalysis,
): Generator<void, JavaScriptApplicationGraph> =>
createImmutableJavaScriptApplicationGraphSteps(
buildJavaScriptArtifactGraphInput(snapshot, fileSet, analysis),
);
const buildJavaScriptArtifactGraphInput = (
snapshot: ArtifactInventorySnapshot,
fileSet: JavaScriptArtifactFileSet,
analysis: JavaScriptModuleArtifactAnalysis,
): unknown => {
const accumulator = new JavaScriptArtifactGraphAccumulator();
const root = createJavaScriptArtifactRootNode(accumulator, snapshot);
const context: JavaScriptArtifactGraphContext = {
@@ -74,7 +94,7 @@ export const buildJavaScriptArtifactGraph = (
addJavaScriptHtmlRoles(context);
addJavaScriptSourceMapOriginals(context);
const coverage = graphCoverage(context);
return createJavaScriptApplicationGraph({
return {
schema: "JavaScriptApplicationGraph",
root_node_ids:
packageRoots.length === 0
@@ -84,7 +104,7 @@ export const buildJavaScriptArtifactGraph = (
edges: accumulator.edges(),
coverage,
limitations: graphLimitations(context, coverage.status),
});
};
};
const graphCoverage = (context: JavaScriptArtifactGraphContext) => {
@@ -7,7 +7,7 @@ import type { JavaScriptSemanticGraph } from "../../domain/javascript/javascript
import type { ElectronBoundarySummary } from "../../domain/javascript/javascriptApplicationAnalysis.js";
import { analyzeAndProjectJavaScriptArtifactFiles } from "./JavaScriptArtifactAnalysis.js";
import { readJavaScriptArtifactFiles } from "../../artifacts/javascript/JavaScriptArtifactFiles.js";
import { buildJavaScriptArtifactGraph } from "./JavaScriptArtifactGraphBuilder.js";
import { buildImmutableJavaScriptArtifactGraphSteps } from "./JavaScriptArtifactGraphBuilder.js";
import {
javascriptArtifactReconstructionInputSchema,
type JavaScriptArtifactReconstructionInput,
@@ -16,7 +16,10 @@ import { scanCanonicalArtifactInventory } from "../../artifacts/inventory/Artifa
import { summarizeElectronBoundaries } from "./ElectronBoundaryAnalysis.js";
import { createJavaScriptSemanticGraphProjection } from "./JavaScriptSemanticGraphBuilder.js";
import type { ProgressReporter } from "../ProgressReporter.js";
import { checkpointJavaScriptAnalysis } from "./JavaScriptAnalysisControl.js";
import {
checkpointJavaScriptAnalysis,
completeJavaScriptAnalysisSteps,
} from "./JavaScriptAnalysisControl.js";
/** Application-layer result retaining local diagnostics outside the canonical graph. */
export interface JavaScriptArtifactReconstructionResult {
@@ -95,16 +98,36 @@ export const reconstructJavaScriptArtifact = async (
"build_javascript_application_graph",
"Constructing application and Electron boundary relationships",
);
const graph = buildJavaScriptArtifactGraph(snapshot, files, analysis);
const applicationGraphSteps = buildImmutableJavaScriptArtifactGraphSteps(
snapshot,
files,
analysis,
);
await reportPhase(
"seal_javascript_application_graph",
"Sealing the validated application graph",
);
const graph = await completeJavaScriptAnalysisSteps(
applicationGraphSteps,
signal,
);
await reportPhase(
"build_javascript_semantic_graph",
"Binding and validating static semantic relationships",
);
const semanticGraph = semanticProjection.finish(
const semanticGraphSteps = semanticProjection.finishImmutableSteps(
snapshot.manifest.root_sha256,
graph,
analysis,
);
await reportPhase(
"seal_javascript_semantic_graph",
"Sealing the validated semantic graph",
);
const semanticGraph = await completeJavaScriptAnalysisSteps(
semanticGraphSteps,
signal,
);
await checkpointJavaScriptAnalysis(signal);
return {
input_path: path,
@@ -1,5 +1,6 @@
import {
createJavaScriptSemanticGraph,
createImmutableJavaScriptSemanticGraphSteps,
type JavaScriptSemanticGraph,
type JavaScriptSemanticGraphNode,
} from "../../domain/javascript/javascriptSemanticGraph.js";
@@ -131,6 +132,11 @@ export interface JavaScriptSemanticGraphProjection {
applicationGraph: BuilderInput["applicationGraph"],
analysis: Pick<JavaScriptArtifactAnalysis, "truncated_scopes">,
) => JavaScriptSemanticGraph;
readonly finishImmutableSteps: (
rootArtifactSha256: string,
applicationGraph: BuilderInput["applicationGraph"],
analysis: Pick<JavaScriptArtifactAnalysis, "truncated_scopes">,
) => Generator<void, JavaScriptSemanticGraph>;
}
/** Accumulate semantic nodes while each source IR is still file-local. */
@@ -161,71 +167,90 @@ export const createJavaScriptSemanticGraphProjection =
if (state.fileNodesDropped) truncatedFiles += 1;
state.fileNodeBudget = null;
};
const finishWith = <Value>(
factory: (input: unknown) => Value,
rootArtifactSha256: string,
applicationGraph: BuilderInput["applicationGraph"],
analysis: Pick<JavaScriptArtifactAnalysis, "truncated_scopes">,
): Value => {
bindSemanticGraphApplicationNodes(state, applicationGraph);
if (state.roots.size === 0) addFallbackRoot(rootArtifactSha256, state);
const unknowns = [...state.unknowns.values()];
const graph = factory({
schema: "JavaScriptSemanticRelationGraph",
root_artifact_sha256: rootArtifactSha256,
application_graph_id: applicationGraph.graph_id,
root_node_ids: [...state.roots],
nodes: [...state.nodes.values()],
relations: [...state.relations.values()],
fingerprints,
unknowns,
coverage: {
status: truncatedFiles > 0 ? "partial" : "unknown",
truncated: truncatedFiles > 0,
omitted_nodes: truncatedFiles > 0 ? null : 0,
omitted_relations: truncatedFiles > 0 ? null : 0,
limits:
truncatedFiles > 0
? [
{
name: "semantic_graph_node_ceiling",
value: SEMANTIC_GRAPH_NODE_CEILING,
unit: "items" as const,
},
]
: [],
families: JAVASCRIPT_SEMANTIC_RELATION_FAMILIES.map((family) => ({
family,
status: semanticFamilyStatus(family, analysis),
retained_relations: [...state.relations.values()].filter(
(relation) =>
JAVASCRIPT_SEMANTIC_RELATION_FAMILY[relation.relation] ===
family,
).length,
omitted_relations: truncatedFiles > 0 ? null : 0,
unknown_ids: unknowns
.filter((unknown) => unknown.family === family)
.map(({ unknown_id: identifier }) => identifier),
})),
},
limitations: [
"The semantic graph contains static syntax observations and conservative relationship candidates; it does not claim runtime execution.",
"Local data flow does not claim control-flow-sensitive reaching definitions or arbitrary dynamic property resolution.",
"Promise ownership covers explicit unshadowed Promise construction, static factories, aggregation, chaining, and await syntax only.",
"Function fingerprints are static candidates; equal digests can remain ambiguous and do not prove behavioral equivalence.",
"Event extraction covers EventEmitter-style literal registrations, removals, and dispatch candidates; dynamic names remain unknown.",
"Timer extraction covers global or node:timers scheduling and exact local-handle cancellation.",
"Child-process extraction covers asynchronous node:child_process creation, literal argv/env/stdio options, exit/error listeners, and kill signals.",
"Configuration extraction covers process.env, process.argv, node:fs reads, and direct logical defaults.",
"Request extraction covers fetch, WebSocket, node:http/node:https construction, direct option fields, and exact local response consumers.",
"Boundary extraction covers unshadowed JSON/global coercions plus parse and validation method candidates.",
"Resource extraction covers built-in filesystem/network acquisition and exact local close/destroy/end handles.",
],
});
state.nodes.clear();
state.relations.clear();
state.unknowns.clear();
state.roots.clear();
fingerprints.length = 0;
return graph;
};
return {
projectFile: projectSource,
finish: (rootArtifactSha256, applicationGraph, analysis) => {
bindSemanticGraphApplicationNodes(state, applicationGraph);
if (state.roots.size === 0) addFallbackRoot(rootArtifactSha256, state);
const unknowns = [...state.unknowns.values()];
const graph = createJavaScriptSemanticGraph({
schema: "JavaScriptSemanticRelationGraph",
root_artifact_sha256: rootArtifactSha256,
application_graph_id: applicationGraph.graph_id,
root_node_ids: [...state.roots],
nodes: [...state.nodes.values()],
relations: [...state.relations.values()],
fingerprints,
unknowns,
coverage: {
status: truncatedFiles > 0 ? "partial" : "unknown",
truncated: truncatedFiles > 0,
omitted_nodes: truncatedFiles > 0 ? null : 0,
omitted_relations: truncatedFiles > 0 ? null : 0,
limits:
truncatedFiles > 0
? [
{
name: "semantic_graph_node_ceiling",
value: SEMANTIC_GRAPH_NODE_CEILING,
unit: "items" as const,
},
]
: [],
families: JAVASCRIPT_SEMANTIC_RELATION_FAMILIES.map((family) => ({
family,
status: semanticFamilyStatus(family, analysis),
retained_relations: [...state.relations.values()].filter(
(relation) =>
JAVASCRIPT_SEMANTIC_RELATION_FAMILY[relation.relation] ===
family,
).length,
omitted_relations: truncatedFiles > 0 ? null : 0,
unknown_ids: unknowns
.filter((unknown) => unknown.family === family)
.map(({ unknown_id: identifier }) => identifier),
})),
},
limitations: [
"The semantic graph contains static syntax observations and conservative relationship candidates; it does not claim runtime execution.",
"Local data flow does not claim control-flow-sensitive reaching definitions or arbitrary dynamic property resolution.",
"Promise ownership covers explicit unshadowed Promise construction, static factories, aggregation, chaining, and await syntax only.",
"Function fingerprints are static candidates; equal digests can remain ambiguous and do not prove behavioral equivalence.",
"Event extraction covers EventEmitter-style literal registrations, removals, and dispatch candidates; dynamic names remain unknown.",
"Timer extraction covers global or node:timers scheduling and exact local-handle cancellation.",
"Child-process extraction covers asynchronous node:child_process creation, literal argv/env/stdio options, exit/error listeners, and kill signals.",
"Configuration extraction covers process.env, process.argv, node:fs reads, and direct logical defaults.",
"Request extraction covers fetch, WebSocket, node:http/node:https construction, direct option fields, and exact local response consumers.",
"Boundary extraction covers unshadowed JSON/global coercions plus parse and validation method candidates.",
"Resource extraction covers built-in filesystem/network acquisition and exact local close/destroy/end handles.",
],
});
state.nodes.clear();
state.relations.clear();
state.unknowns.clear();
state.roots.clear();
fingerprints.length = 0;
return graph;
},
finish: (rootArtifactSha256, applicationGraph, analysis) =>
finishWith(
createJavaScriptSemanticGraph,
rootArtifactSha256,
applicationGraph,
analysis,
),
finishImmutableSteps: (rootArtifactSha256, applicationGraph, analysis) =>
finishWith(
createImmutableJavaScriptSemanticGraphSteps,
rootArtifactSha256,
applicationGraph,
analysis,
),
};
};
@@ -1,7 +1,15 @@
import { z } from "zod";
import { javascriptApplicationGraphSchema } from "./javascriptApplicationGraph.js";
import { javaScriptSemanticGraphSchema } from "./javascriptSemanticGraph.js";
import {
javascriptApplicationGraphSchema,
isValidatedImmutableJavaScriptApplicationGraph,
type JavaScriptApplicationGraph,
} from "./javascriptApplicationGraph.js";
import {
javaScriptSemanticGraphSchema,
isValidatedImmutableJavaScriptSemanticGraph,
type JavaScriptSemanticGraph,
} from "./javascriptSemanticGraph.js";
import { digestSchema } from "../digests.js";
import { prefixedDigestSchema } from "../digests.js";
@@ -51,43 +59,62 @@ const reconstructionStatisticsSchema = z.strictObject({
truncated_scopes: countSchema,
});
/** JavaScript application analysis with an authenticated semantic companion. */
export const javascriptApplicationAnalysisResultSchema = z
.strictObject({
input_path: z.string().min(1),
format: z.enum(["asar", "directory"]),
root_artifact_sha256: digestSchema,
inventory_manifest_id: prefixedDigestSchema("agm"),
inventory_graph_sha256: digestSchema,
graph: javascriptApplicationGraphSchema,
summary: electronBoundarySummarySchema,
statistics: reconstructionStatisticsSchema,
limitations: z.array(z.string().min(1)),
semantic_graph: javaScriptSemanticGraphSchema,
})
.superRefine((result, context) => {
if (result.semantic_graph.application_graph_id !== result.graph.graph_id)
context.addIssue({
code: "custom",
path: ["semantic_graph", "application_graph_id"],
message: "Semantic graph must commit the containing application graph",
});
if (
result.semantic_graph.root_artifact_sha256 !== result.root_artifact_sha256
)
context.addIssue({
code: "custom",
path: ["semantic_graph", "root_artifact_sha256"],
message: "Semantic graph must commit the containing root artifact",
});
const applicationNodeIds = new Set(
result.graph.nodes.map(({ node_id }) => node_id),
);
for (const [nodeIndex, node] of result.semantic_graph.nodes.entries())
const applicationAnalysisResultShape = z.strictObject({
input_path: z.string().min(1),
format: z.enum(["asar", "directory"]),
root_artifact_sha256: digestSchema,
inventory_manifest_id: prefixedDigestSchema("agm"),
inventory_graph_sha256: digestSchema,
graph: javascriptApplicationGraphSchema,
summary: electronBoundarySummarySchema,
statistics: reconstructionStatisticsSchema,
limitations: z.array(z.string().min(1)),
semantic_graph: javaScriptSemanticGraphSchema,
});
type BindingIssue = {
readonly code: "custom";
readonly path: PropertyKey[];
readonly message: string;
};
function* checkApplicationBindingsSteps(
result: z.infer<typeof applicationAnalysisResultShape>,
context: { readonly addIssue: (issue: BindingIssue) => void },
): Generator<void> {
if (result.semantic_graph.application_graph_id !== result.graph.graph_id)
context.addIssue({
code: "custom",
path: ["semantic_graph", "application_graph_id"],
message: "Semantic graph must commit the containing application graph",
});
if (
result.semantic_graph.root_artifact_sha256 !== result.root_artifact_sha256
)
context.addIssue({
code: "custom",
path: ["semantic_graph", "root_artifact_sha256"],
message: "Semantic graph must commit the containing root artifact",
});
const applicationNodeIds = new Set<string>();
let examined = 0;
try {
for (const node of result.graph.nodes) {
applicationNodeIds.add(node.node_id);
if (++examined === 4096) {
examined = 0;
yield;
}
}
for (const [nodeIndex, node] of result.semantic_graph.nodes.entries()) {
if (++examined === 4096) {
examined = 0;
yield;
}
for (const [
identifierIndex,
identifier,
] of node.application_node_ids.entries())
] of node.application_node_ids.entries()) {
if (!applicationNodeIds.has(identifier))
context.addIssue({
code: "custom",
@@ -100,8 +127,52 @@ export const javascriptApplicationAnalysisResultSchema = z
],
message: "Semantic node references an absent application node",
});
if (++examined === 4096) {
examined = 0;
yield;
}
}
}
} finally {
applicationNodeIds.clear();
}
}
/** JavaScript application analysis with an authenticated semantic companion. */
export const javascriptApplicationAnalysisResultSchema =
applicationAnalysisResultShape.superRefine((result, context) => {
const steps = checkApplicationBindingsSteps(result, context);
while (!steps.next().done) {
// Public/imported results still receive complete synchronous validation.
}
});
const ownedAnalysisResultShape = applicationAnalysisResultShape.extend({
graph: z.custom<JavaScriptApplicationGraph>(
isValidatedImmutableJavaScriptApplicationGraph,
),
semantic_graph: z.custom<JavaScriptSemanticGraph>(
isValidatedImmutableJavaScriptSemanticGraph,
),
});
/** Reuse exact owned graphs while validating metadata and cross-graph bindings. */
export function* parseOwnedJavaScriptApplicationAnalysisSteps(
input: unknown,
): Generator<void, JavaScriptApplicationAnalysisResult> {
const prepared = ownedAnalysisResultShape.safeParse(input);
if (!prepared.success)
return javascriptApplicationAnalysisResultSchema.parse(input);
const issues: BindingIssue[] = [];
yield* checkApplicationBindingsSteps(prepared.data, {
addIssue: (issue) => {
issues.push(issue);
},
});
if (issues.length > 0) throw new z.ZodError(issues);
return prepared.data;
}
/** Parsed public JavaScript application analysis request. */
export type AnalyzeJavaScriptApplicationInput = z.infer<
typeof analyzeJavaScriptApplicationInputSchema
@@ -0,0 +1,267 @@
import { describe, expect, it } from "vitest";
import { z } from "zod";
import { JAVASCRIPT_APPLICATION_EVIDENCE_EXAMPLE } from "../../contracts/javascript/javascriptRuntimeReconciliationExample.js";
import { freezeJsonSnapshot } from "../immutableJson.js";
import {
javascriptApplicationAnalysisResultSchema,
parseOwnedJavaScriptApplicationAnalysisSteps,
} from "./javascriptApplicationAnalysis.js";
import {
createJavaScriptApplicationGraph,
createImmutableJavaScriptApplicationGraphSteps,
isValidatedImmutableJavaScriptApplicationGraph,
} from "./javascriptApplicationGraph.js";
import {
createJavaScriptSemanticGraph,
createImmutableJavaScriptSemanticGraphSteps,
isValidatedImmutableJavaScriptSemanticGraph,
} from "./javascriptSemanticGraph.js";
const complete = <Value>(steps: Iterator<void, Value>): Value => {
let next = steps.next();
while (!next.done) next = steps.next();
return next.value;
};
const example = () =>
javascriptApplicationAnalysisResultSchema.parse(
JAVASCRIPT_APPLICATION_EVIDENCE_EXAMPLE.normalized_result,
);
const ownedExample = () => {
const result = example();
const { graph_id: _applicationId, ...application } = result.graph;
const { graph_id: _semanticId, ...semantic } = result.semantic_graph;
return {
...result,
graph: complete(
createImmutableJavaScriptApplicationGraphSteps(application),
),
semantic_graph: complete(
createImmutableJavaScriptSemanticGraphSteps(semantic),
),
};
};
const issuesFrom = (action: () => unknown) => {
try {
action();
} catch (error: unknown) {
if (error instanceof z.ZodError) return error.issues;
throw error;
}
throw new Error("Expected complete result validation to reject the input");
};
describe("owned JavaScript graph validation", () => {
it("reuses exact sealed graphs and preserves all public result fields", () => {
const input = ownedExample();
const result = complete(
parseOwnedJavaScriptApplicationAnalysisSteps(input),
);
expect(result).toEqual(
javascriptApplicationAnalysisResultSchema.parse(input),
);
expect(result.graph).toBe(input.graph);
expect(result.semantic_graph).toBe(input.semantic_graph);
expect(result.summary).not.toBe(input.summary);
expect(result.statistics).not.toBe(input.statistics);
expect(Object.isFrozen(result.graph.nodes[0]?.observations)).toBe(true);
expect(Object.isFrozen(result.semantic_graph.nodes[0]?.properties)).toBe(
true,
);
expect(isValidatedImmutableJavaScriptApplicationGraph(result.graph)).toBe(
true,
);
expect(
isValidatedImmutableJavaScriptSemanticGraph(result.semantic_graph),
).toBe(true);
expect(
isValidatedImmutableJavaScriptApplicationGraph(result.graph.nodes),
).toBe(false);
});
it("keeps ordinary constructors mutable and clones inputs before sealing", () => {
const input = example();
const { graph_id: _applicationId, ...application } = input.graph;
const { graph_id: _semanticId, ...semantic } = input.semantic_graph;
const ordinaryApplication = createJavaScriptApplicationGraph(application);
const ordinarySemantic = createJavaScriptSemanticGraph(semantic);
expect(Object.isFrozen(ordinaryApplication)).toBe(false);
expect(Object.isFrozen(ordinarySemantic)).toBe(false);
const applicationSteps =
createImmutableJavaScriptApplicationGraphSteps(application);
const semanticSteps = createImmutableJavaScriptSemanticGraphSteps(semantic);
application.limitations.push("Changed after factory input was cloned.");
semantic.limitations.push("Changed after factory input was cloned.");
expect(complete(applicationSteps)).toEqual(ordinaryApplication);
expect(complete(semanticSteps)).toEqual(ordinarySemantic);
expect(
isValidatedImmutableJavaScriptApplicationGraph(ordinaryApplication),
).toBe(false);
expect(isValidatedImmutableJavaScriptSemanticGraph(ordinarySemantic)).toBe(
false,
);
});
it.each(["mutable", "shallow-frozen", "json-sealed"])(
"fully validates %s imported graphs instead of trusting their IDs",
(representation) => {
const input = example();
input.graph.graph_id = `jag_${"f".repeat(64)}`;
if (representation === "shallow-frozen") Object.freeze(input.graph);
if (representation === "json-sealed") freezeJsonSnapshot(input.graph);
expect(isValidatedImmutableJavaScriptApplicationGraph(input.graph)).toBe(
false,
);
const issues = issuesFrom(() =>
complete(parseOwnedJavaScriptApplicationAnalysisSteps(input)),
);
expect(issues).toEqual(
issuesFrom(() =>
javascriptApplicationAnalysisResultSchema.parse(input),
),
);
expect(
issues.some(({ path }) => path.join("/") === "graph/graph_id"),
).toBe(true);
},
);
it("fully parses valid imported graphs without conferring an owned proof", () => {
const input = example();
const parsed = complete(
parseOwnedJavaScriptApplicationAnalysisSteps(input),
);
expect(parsed).toEqual(input);
expect(parsed.graph).not.toBe(input.graph);
expect(parsed.semantic_graph).not.toBe(input.semantic_graph);
expect(
isValidatedImmutableJavaScriptSemanticGraph(parsed.semantic_graph),
).toBe(false);
});
it("rejects a forged semantic graph paired with an authenticated application graph", () => {
const input = ownedExample();
input.semantic_graph = freezeJsonSnapshot({
...input.semantic_graph,
graph_id: `jsrg_${"f".repeat(64)}`,
});
expect(
isValidatedImmutableJavaScriptSemanticGraph(input.semantic_graph),
).toBe(false);
expect(
issuesFrom(() =>
complete(parseOwnedJavaScriptApplicationAnalysisSteps(input)),
),
).toEqual(
issuesFrom(() => javascriptApplicationAnalysisResultSchema.parse(input)),
);
});
it("does not authenticate an abandoned sealing operation", () => {
const input = example();
const { graph_id: _semanticId, ...semantic } = input.semantic_graph;
const originalNode = semantic.nodes[0];
if (originalNode === undefined) throw new Error("Expected a semantic node");
semantic.nodes[0] = {
...originalNode,
properties: {
rows: Array.from({ length: 10_000 }, (_, value) => ({ value })),
},
};
const mutable = createJavaScriptSemanticGraph(semantic);
const { graph_id: _mutableId, ...mutableInput } = mutable;
const steps = createImmutableJavaScriptSemanticGraphSteps(mutableInput);
expect(steps.next().done).toBe(false);
steps.return(mutable);
expect(isValidatedImmutableJavaScriptSemanticGraph(mutable)).toBe(false);
const sealed = complete(
createImmutableJavaScriptSemanticGraphSteps(mutableInput),
);
expect(sealed).toEqual(mutable);
expect(isValidatedImmutableJavaScriptSemanticGraph(sealed)).toBe(true);
});
});
describe("owned JavaScript result bindings", () => {
it.each(["application", "root", "node"])(
"retains the %s binding check for individually authenticated graphs",
(binding) => {
const input = ownedExample();
if (binding === "root") input.root_artifact_sha256 = "e".repeat(64);
else {
const { graph_id: _semanticId, ...semantic } = input.semantic_graph;
if (binding === "application")
semantic.application_graph_id = `jag_${"e".repeat(64)}`;
else {
const node = semantic.nodes[0];
if (node === undefined) throw new Error("Expected a semantic node");
semantic.nodes = [
{ ...node, application_node_ids: [`jag_node_${"e".repeat(64)}`] },
...semantic.nodes.slice(1),
];
}
input.semantic_graph = complete(
createImmutableJavaScriptSemanticGraphSteps(semantic),
);
}
expect(
issuesFrom(() =>
complete(parseOwnedJavaScriptApplicationAnalysisSteps(input)),
),
).toEqual(
issuesFrom(() =>
javascriptApplicationAnalysisResultSchema.parse(input),
),
);
},
);
it("yields while checking a wide binding set and closes an abandoned iteration", () => {
const input = ownedExample();
const { graph_id: _semanticId, ...semantic } = input.semantic_graph;
const node = semantic.nodes[0];
if (node === undefined) throw new Error("Expected a semantic node");
semantic.nodes = [
{
...node,
application_node_ids: Array.from(
{ length: 10_000 },
(_, index) => `jag_node_${index.toString(16).padStart(64, "0")}`,
),
},
...semantic.nodes.slice(1),
];
input.semantic_graph = complete(
createImmutableJavaScriptSemanticGraphSteps(semantic),
);
const steps = parseOwnedJavaScriptApplicationAnalysisSteps(input);
expect(steps.next().done).toBe(false);
steps.return(input);
expect(steps.next().done).toBe(true);
expect(
issuesFrom(() =>
complete(parseOwnedJavaScriptApplicationAnalysisSteps(input)),
),
).toEqual(
issuesFrom(() => javascriptApplicationAnalysisResultSchema.parse(input)),
);
});
it("retains strict metadata validation alongside authenticated graphs", () => {
const input = {
...ownedExample(),
statistics: { ...example().statistics, findings: -1 },
extra: true,
};
expect(
issuesFrom(() =>
complete(parseOwnedJavaScriptApplicationAnalysisSteps(input)),
),
).toEqual(
issuesFrom(() => javascriptApplicationAnalysisResultSchema.parse(input)),
);
});
});
@@ -1,6 +1,7 @@
import { z } from "zod";
import { canonicalDigest, canonicalJson } from "../comparisonSemantics.js";
import { compareCodePoints, uniqueSorted } from "../canonicalOrdering.js";
import { freezeOwnedJsonSnapshotSteps } from "../immutableJson.js";
import type {
ApplicationGraphEvidence,
@@ -450,6 +451,30 @@ export const createJavaScriptApplicationGraph = (
});
};
const validatedImmutableApplicationGraphs = new WeakSet<object>();
/** Clone and validate synchronously, then return steps to seal the owned graph. */
export const createImmutableJavaScriptApplicationGraphSteps = (
input: unknown,
): Generator<void, JavaScriptApplicationGraph> =>
sealValidatedApplicationGraphSteps(createJavaScriptApplicationGraph(input));
function* sealValidatedApplicationGraphSteps(
graph: JavaScriptApplicationGraph,
): Generator<void, JavaScriptApplicationGraph> {
yield* freezeOwnedJsonSnapshotSteps(graph);
validatedImmutableApplicationGraphs.add(graph);
return graph;
}
/** Recognize the exact completely sealed graph produced by the owned factory. */
export const isValidatedImmutableJavaScriptApplicationGraph = (
value: unknown,
): value is JavaScriptApplicationGraph =>
typeof value === "object" &&
value !== null &&
validatedImmutableApplicationGraphs.has(value);
/** Parse a stored graph and reject stale IDs. */
export const parseJavaScriptApplicationGraph = (
input: unknown,
@@ -2,6 +2,7 @@ import { z } from "zod";
import { canonicalDigest, canonicalJson } from "../comparisonSemantics.js";
import { compareCodePoints, uniqueSorted } from "../canonicalOrdering.js";
import { freezeOwnedJsonSnapshotSteps } from "../immutableJson.js";
import type { ApplicationGraphEvidence } from "./javascriptApplicationEvidenceSchemas.js";
import {
JAVASCRIPT_SEMANTIC_RELATION_FAMILIES,
@@ -561,4 +562,28 @@ export const createJavaScriptSemanticGraph = (
return record;
};
const validatedImmutableSemanticGraphs = new WeakSet<object>();
/** Clone and validate synchronously, then return steps to seal the owned graph. */
export const createImmutableJavaScriptSemanticGraphSteps = (
input: unknown,
): Generator<void, JavaScriptSemanticGraph> =>
sealValidatedSemanticGraphSteps(createJavaScriptSemanticGraph(input));
function* sealValidatedSemanticGraphSteps(
graph: JavaScriptSemanticGraph,
): Generator<void, JavaScriptSemanticGraph> {
yield* freezeOwnedJsonSnapshotSteps(graph);
validatedImmutableSemanticGraphs.add(graph);
return graph;
}
/** Recognize the exact completely sealed graph produced by the owned factory. */
export const isValidatedImmutableJavaScriptSemanticGraph = (
value: unknown,
): value is JavaScriptSemanticGraph =>
typeof value === "object" &&
value !== null &&
validatedImmutableSemanticGraphs.has(value);
export type { JavaScriptSemanticGraphNode, JavaScriptSemanticGraphRelation };