mirror of
https://github.com/morluto/rea.git
synced 2026-10-12 02:19:24 +00:00
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:
@@ -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
|
||||
|
||||
@@ -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 };
|
||||
|
||||
Reference in New Issue
Block a user