refactor(domain): encode comparison and lifecycle states exactly (#473)

* refactor(domain): encode comparison states exactly

* refactor(domain): make evidence lifecycles explicit

* refactor(browser): parse capture states once

* test: prefer behavior over implementation seams

* chore: refresh schema-derived metadata

* test(process): preserve lifecycle cleanup coverage

* test(hopper): bind rename cache coverage to dispatch
This commit is contained in:
morluto authored and GitHub committed 2026-08-11 18:04:36 +08:00
1 parent e91c8c39c2
commit d65730c4c2
43 files changed
+1732 -780

No files matched your search

+2 -2
View File
@@ -680,10 +680,10 @@
"mcp_resource_templates": 11
},
"digests": {
"tools_sha256": "1a4533fef9cbe075fdca76b19a138924bd69d93a6413cdb767b2a9240a46fd04",
"tools_sha256": "b21e7a4794bf82b27931a373a7812978b54f136904a88681094f2c545e4413a6",
"prompts_sha256": "3886a3bf55a448def79e3eb429e6a56eb2ba075423c5cb1e5747642b389ef8fe",
"resources_sha256": "e0e872276ebf6f88e916ef24930a99eed27ce4c93e494e60820e9bd8cc8497c3",
"combined_sha256": "8bd40516d78875cb75ba1c4a8786286311036742ad660914d7327c2941b2f73a",
"combined_sha256": "6624247db5bcf7b52ce53173bbc0ac11bd51e9217e9e3be4298b755bcc60ea50",
"providers_sha256": "7446b51788734ebf6f4f702e3a5a8c9f0b61967490c245f30bd37b2060827e6b"
}
}
@@ -1,6 +1,6 @@
{
"schema_version": 1,
"manifest_id": "ecm_5add7389cf00e86bfec005df9e0e0cf8d7aceb2f3ec3e5f6b74edb5f880bd9a7",
"manifest_id": "ecm_1b79ae0f4063ccac065a4eff5dec420f968ea39bc4d93eb3bddcaaf318eb01a6",
"verifier": {
"id": "rea-managed-conformance",
"version": "1"
@@ -14,7 +14,7 @@
"skill_digests": [
{
"skill_id": "reverse-engineer-anything",
"sha256": "2cca31c9550fe32c24adcb8134edb4b077aa772fb0f0f1057c99964cac6d9a8f"
"sha256": "709a1ace114ea7ffa6f36a67c2f2df9ffa1029c2cad8508aebf40c5105862643"
}
],
"claims": [
+1 -1
View File
@@ -4,7 +4,7 @@ description: Reverse engineer native, managed, Electron/JavaScript, packaged, an
metadata:
version: "23"
tool_count: 116
catalog_digest: "8bd40516d78875cb75ba1c4a8786286311036742ad660914d7327c2941b2f73a"
catalog_digest: "6624247db5bcf7b52ce53173bbc0ac11bd51e9217e9e3be4298b755bcc60ea50"
---
# REA
+67 -54
View File
@@ -3,8 +3,10 @@ import { AnyMap, eachMapping } from "@jridgewell/trace-mapping";
import { sanitizeBrowserUrl } from "../domain/browserObservation.js";
import type {
AnalyzeWebBundleInput,
WebBundleAnalysis,
WebSourceMapItem,
WebSourceMaps,
} from "../domain/webBundleAnalysis.js";
import { webSourceMapsSchema } from "../domain/webBundleAnalysis.js";
import { createWebTextArtifact } from "../domain/webContentArtifact.js";
export interface WebSourceMapRequest {
@@ -17,8 +19,9 @@ interface SourceMapFetchHost {
readonly fetch: typeof fetch;
}
type SourceMaps = WebBundleAnalysis["observations"]["source_maps"];
type SourceMapItem = SourceMaps["items"][number];
type SourceMaps = WebSourceMaps;
type SourceMapItem = WebSourceMapItem;
type ParsedSourceMapItem = Extract<SourceMapItem, { status: "included" }>;
/** Fetch and validate approved source maps without browser credentials. */
export const fetchWebSourceMaps = async (
@@ -51,7 +54,7 @@ export const fetchWebSourceMaps = async (
const dropped = requests.length - items.length;
const truncated =
dropped > 0 || items.some(({ status }) => status === "truncated");
return {
return webSourceMapsSchema.parse({
status: truncated
? "truncated"
: items.length === 0
@@ -68,7 +71,7 @@ export const fetchWebSourceMaps = async (
.slice(items.length)
.map(({ scriptKey }) => scriptKey),
items,
};
});
};
interface SourceMapFetchResult {
@@ -88,15 +91,13 @@ const fetchOne = async (
context: SourceMapFetchContext,
): Promise<SourceMapFetchResult> => {
const { input, maximumBytes, signal, host } = context;
const base = emptyItem(request);
if (!approvedUrl(request.fetchUrl, input.allowed_origins))
return {
item: {
...base,
status: "policy_filtered",
limitation:
"Declared source-map URL is outside the approved exact origins.",
},
item: emptySourceMapItem(
request,
"policy_filtered",
"Declared source-map URL is outside the approved exact origins.",
),
bytesRead: 0,
};
try {
@@ -108,20 +109,20 @@ const fetchOne = async (
);
if (response === undefined)
return {
item: {
...base,
status: "policy_filtered",
limitation: "A source-map redirect left the approved exact origins.",
},
item: emptySourceMapItem(
request,
"policy_filtered",
"A source-map redirect left the approved exact origins.",
),
bytesRead: 0,
};
if (!response.ok)
return {
item: {
...base,
status: "fetch_failed",
limitation: `Source-map server returned HTTP ${String(response.status)}.`,
},
item: emptySourceMapItem(
request,
"fetch_failed",
`Source-map server returned HTTP ${String(response.status)}.`,
),
bytesRead: 0,
};
const body = await boundedResponseText(response, maximumBytes);
@@ -132,13 +133,13 @@ const fetchOne = async (
} catch (cause: unknown) {
if (signal?.aborted === true) throw cause;
return {
item: {
...base,
status: isLimitError(cause) ? "truncated" : "fetch_failed",
limitation: isLimitError(cause)
item: emptySourceMapItem(
request,
isLimitError(cause) ? "truncated" : "fetch_failed",
isLimitError(cause)
? "Source-map response exceeded the remaining approved byte budget."
: "Source-map fetch or validation failed.",
},
),
bytesRead: cause instanceof SourceMapByteLimitError ? cause.bytesRead : 0,
};
}
@@ -215,21 +216,20 @@ const normalizeSourceMap = (
text: string,
input: AnalyzeWebBundleInput,
): SourceMapItem => {
const base = emptyItem(request);
if (!validSourceMapEnvelope(text))
return {
...base,
status: "invalid",
limitation: "Source-map JSON is not a bounded version 3 map.",
};
return emptySourceMapItem(
request,
"invalid",
"Source-map JSON is not a bounded version 3 map.",
);
try {
const map = new AnyMap(text, request.fetchUrl);
if (map.sources.length > input.analysis_limits.max_original_sources)
return {
...base,
status: "truncated",
limitation: "Source map exceeds the original-source inventory limit.",
};
return emptySourceMapItem(
request,
"truncated",
"Source map exceeds the original-source inventory limit.",
);
const resolvedBySource = new Map<string, string>();
const originalSources = map.sources.map((source, index) => {
const content = map.sourcesContent?.[index];
@@ -244,7 +244,7 @@ const normalizeSourceMap = (
: null,
};
});
const mappings: SourceMapItem["mappings"] = [];
const mappings: ParsedSourceMapItem["mappings"] = [];
let totalMappings = 0;
eachMapping(map, (mapping) => {
if (
@@ -284,32 +284,37 @@ const normalizeSourceMap = (
]
: []),
];
return {
...base,
status: mappingTruncated || modules.truncated ? "truncated" : "included",
const parsed = {
...sourceMapContext(request),
artifact: createWebTextArtifact(text, "application/source-map+json"),
original_sources: originalSources,
original_module_edges: modules.edges,
mappings,
limitation: limitations.length === 0 ? null : limitations.join(" "),
};
return limitations.length === 0
? { ...parsed, status: "included", limitation: null }
: {
...parsed,
status: "truncated",
limitation: limitations.join(" "),
};
} catch {
return {
...base,
status: "invalid",
limitation: "Source-map mappings could not be decoded safely.",
};
return emptySourceMapItem(
request,
"invalid",
"Source-map mappings could not be decoded safely.",
);
}
};
const originalModuleEdges = (
sources: SourceMapItem["original_sources"],
sources: ParsedSourceMapItem["original_sources"],
maximum: number,
): {
readonly edges: SourceMapItem["original_module_edges"];
readonly edges: ParsedSourceMapItem["original_module_edges"];
readonly truncated: boolean;
} => {
const edges: SourceMapItem["original_module_edges"] = [];
const edges: ParsedSourceMapItem["original_module_edges"] = [];
const seen = new Set<string>();
for (const source of sources) {
if (source.artifact === null) continue;
@@ -383,15 +388,23 @@ const approvedUrl = (
}
};
const emptyItem = (request: WebSourceMapRequest): SourceMapItem => ({
const sourceMapContext = (request: WebSourceMapRequest) => ({
script_key: request.scriptKey,
declared_url: request.declaredUrl,
status: "fetch_failed",
});
const emptySourceMapItem = (
request: WebSourceMapRequest,
status: Extract<SourceMapItem, { readonly artifact: null }>["status"],
limitation: string,
): Extract<SourceMapItem, { readonly artifact: null }> => ({
...sourceMapContext(request),
status,
artifact: null,
original_sources: [],
original_module_edges: [],
mappings: [],
limitation: null,
limitation,
});
const sanitizeSource = (value: string): string => {
+18 -3
View File
@@ -93,6 +93,16 @@ describe("call path reconstruction", () => {
expect(callPathResultSchema.parse(result)).toEqual(result);
expect(result.status).toBe("found");
expect(result.shortest_hops).toBe(2);
expect(
callPathResultSchema.safeParse({ ...result, shortest_hops: null })
.success,
).toBe(false);
expect(
callPathResultSchema.safeParse({
...result,
paths: { ...result.paths, total: null },
}).success,
).toBe(false);
expect(
result.paths.items.map((path) =>
path.nodes.map(({ address }) => address),
@@ -118,9 +128,14 @@ describe("call path reconstruction", () => {
});
it("only reports not_found for exhaustive closure", () => {
expect(run([observe("0x1000", []), observe("0x4000", [])]).status).toBe(
"not_found",
);
const notFound = run([observe("0x1000", []), observe("0x4000", [])]);
expect(notFound.status).toBe("not_found");
expect(
callPathResultSchema.safeParse({
...notFound,
search_scope: { ...notFound.search_scope, exhaustive: false },
}).success,
).toBe(false);
const unknown = run([observe("0x1000", ["0x2000"])]);
expect(unknown.status).toBe("unknown");
expect(unknown.limitations).toContain(
+118 -152
View File
@@ -1,102 +1,25 @@
import { DirectedGraph } from "graphology";
import { bidirectional } from "graphology-shortest-path/unweighted.js";
import { z } from "zod";
import { evidenceSchema } from "./evidence.js";
import {
callPathInputSchema,
callPathResultSchema,
parseCallPathAddress,
type CallPathEvidenceGroup,
type CallPathInput,
type CallPathResult,
type OutputCallPath,
} from "./callPathSchemas.js";
import {
parseFunctionEvidence,
type FunctionSnapshot,
} from "./functionDossierEvidence.js";
const addressSchema = z.string().regex(/^0x(?:0|[1-9a-f][0-9a-f]*)$/u);
const inputAddressSchema = z
.string()
.regex(/^0[xX][0-9a-fA-F]+$/u)
.transform((address) => `0x${BigInt(address).toString(16)}`);
const evidenceIdSchema = z.string().regex(/^ev_[a-f0-9]{64}$/u);
const evidenceGroupSchema = z.union([
evidenceSchema,
z.array(evidenceSchema).min(1).max(100),
]);
const MAX_EVIDENCE_PAGES = 2_000;
const MAX_GRAPH_EDGES = 10_000;
const MAX_PATH_EXPANSIONS = 100_000;
/** Strict, bounded input for explicit call-path reconstruction. */
export const callPathInputSchema = z
.object({
functions: z.array(evidenceGroupSchema).min(1).max(500),
start: z.object({ address: inputAddressSchema }).strict(),
goal: z.object({ address: inputAddressSchema }).strict(),
max_depth: z.number().int().min(0).max(32).default(8),
max_paths: z.number().int().min(1).max(100).default(10),
offset: z.number().int().min(0).default(0),
limit: z.number().int().min(1).max(100).default(100),
unknown_registry_approved: z.literal(true).optional(),
})
.superRefine(({ functions }, context) => {
const pages = functions.reduce(
(total, group) => total + (Array.isArray(group) ? group.length : 1),
0,
);
if (pages > MAX_EVIDENCE_PAGES)
context.addIssue({
code: "custom",
message: `Call-path Evidence exceeds ${MAX_EVIDENCE_PAGES} pages`,
path: ["functions"],
});
});
const citedNodeSchema = z.object({
address: addressSchema,
name: z.string().nullable(),
evidence_links: z.array(evidenceIdSchema).min(1).max(100),
});
const citedEdgeSchema = z.object({
source: addressSchema,
target: addressSchema,
evidence_links: z.array(evidenceIdSchema).min(1).max(100),
});
const pathSchema = z.object({
hops: z.number().int().min(0),
nodes: z.array(citedNodeSchema).min(1).max(33),
edges: z.array(citedEdgeSchema).max(32),
evidence_links: z.array(evidenceIdSchema).min(1).max(3_300),
});
/** Evidence-cited, bounded directed call-path result. */
export const callPathResultSchema = z.object({
status: z.enum(["found", "not_found", "unknown", "truncated"]),
start: addressSchema,
goal: addressSchema,
shortest_hops: z.number().int().min(0).nullable(),
search_scope: z.object({
max_depth: z.number().int().min(0).max(32),
max_paths: z.number().int().min(1).max(100),
exhaustive: z.boolean(),
}),
explored: z.object({
nodes: z.number().int().min(0),
edges: z.number().int().min(0),
depth_reached: z.number().int().min(0),
}),
paths: z.object({
items: z.array(pathSchema).max(100),
offset: z.number().int().min(0),
limit: z.number().int().min(1).max(100),
total: z.number().int().min(0).nullable(),
returned: z.number().int().min(0).max(100),
truncated: z.boolean(),
lower_bound: z.number().int().min(0),
next_offset: z.number().int().min(0).nullable(),
}),
evidence_links: z.array(evidenceIdSchema).min(1).max(50_000),
limitations: z.array(z.string()),
});
export type CallPathInput = z.infer<typeof callPathInputSchema>;
export type CallPathResult = z.infer<typeof callPathResultSchema>;
type OutputPath = z.infer<typeof pathSchema>;
export { callPathInputSchema, callPathResultSchema };
export type { CallPathInput, CallPathResult };
interface SearchState {
readonly graph: DirectedGraph;
@@ -109,13 +32,7 @@ interface SearchState {
/** Reconstruct bounded direct-callee paths from explicit analyze_function Evidence. */
export const buildCallPath = (input: CallPathInput): CallPathResult => {
const parsed = callPathInputSchema.parse(input);
const snapshots = parseSnapshots(parsed.functions);
assertCompatible(snapshots);
if (!snapshots.has(parsed.start.address))
throw new TypeError(
`No analyze_function Evidence was supplied for start ${parsed.start.address}`,
);
const graph = createGraph(snapshots);
const { snapshots, graph } = prepareCallGraph(parsed);
const search = inspectSearch({
graph,
snapshots,
@@ -144,75 +61,125 @@ export const buildCallPath = (input: CallPathInput): CallPathResult => {
const paths = retained.map((path) => citePath(path, snapshots));
const total = paths.length;
const items = paths.slice(parsed.offset, parsed.offset + parsed.limit);
const found = paths.length > 0;
const shortestHops = paths[0]?.hops;
const found = shortestHops !== undefined;
const exhaustive = search.exhaustive && !capped;
const status = callPathStatus({ capped, found, exhaustive });
const limitations = [...search.limitations];
if (capped)
limitations.push(
enumeration.budgetExhausted
? `Path enumeration stopped at ${MAX_PATH_EXPANSIONS} expansions`
: `Path enumeration stopped at max_paths=${parsed.max_paths}`,
);
if (!found && shortest !== null && shortest.length - 1 > parsed.max_depth)
limitations.push(
`The shortest observed path exceeds max_depth=${parsed.max_depth}`,
);
return callPathResultSchema.parse({
status,
const limitations = deriveLimitations(
search,
{ budgetExhausted: enumeration.budgetExhausted, capped, found, shortest },
parsed,
);
const resultContext = {
start: parsed.start.address,
goal: parsed.goal.address,
shortest_hops: found ? (paths[0]?.hops ?? null) : null,
search_scope: {
max_depth: parsed.max_depth,
max_paths: parsed.max_paths,
exhaustive,
},
explored: {
nodes: search.reached.size,
edges: [...search.reached.keys()].reduce(
(count, node) =>
count + (graph.hasNode(node) ? graph.outDegree(node) : 0),
0,
),
depth_reached: Math.max(0, ...search.reached.values()),
},
paths: {
items,
offset: parsed.offset,
limit: parsed.limit,
total: capped ? null : total,
returned: items.length,
truncated: capped,
lower_bound: total + (hasKnownExtraPath ? 1 : 0),
next_offset:
parsed.offset + items.length < total
? parsed.offset + items.length
: null,
},
explored: summarizeSearch(graph, search.reached),
evidence_links: uniqueEvidence(snapshots.values()),
limitations: [...new Set(limitations)].sort((left, right) =>
left.localeCompare(right),
),
};
const searchScope = {
max_depth: parsed.max_depth,
max_paths: parsed.max_paths,
};
const pathPage = {
items,
offset: parsed.offset,
limit: parsed.limit,
returned: items.length,
lower_bound: total + (hasKnownExtraPath ? 1 : 0),
next_offset:
parsed.offset + items.length < total
? parsed.offset + items.length
: null,
};
if (capped)
return callPathResultSchema.parse({
...resultContext,
status: "truncated",
shortest_hops: shortestHops ?? null,
search_scope: { ...searchScope, exhaustive: false },
paths: { ...pathPage, total: null, truncated: true },
});
if (found)
return callPathResultSchema.parse({
...resultContext,
status: "found",
shortest_hops: shortestHops,
search_scope: { ...searchScope, exhaustive },
paths: { ...pathPage, total, truncated: false },
});
return callPathResultSchema.parse({
...resultContext,
status: exhaustive ? "not_found" : "unknown",
shortest_hops: null,
search_scope: { ...searchScope, exhaustive },
paths: {
...pathPage,
items: [],
total: 0,
returned: 0,
truncated: false,
lower_bound: 0,
next_offset: null,
},
});
};
const callPathStatus = ({
capped,
found,
exhaustive,
}: {
interface PathOutcome {
readonly budgetExhausted: boolean;
readonly capped: boolean;
readonly found: boolean;
readonly exhaustive: boolean;
}): CallPathResult["status"] => {
if (capped) return "truncated";
if (found) return "found";
return exhaustive ? "not_found" : "unknown";
readonly shortest: readonly string[] | null;
}
const deriveLimitations = (
search: SearchState,
outcome: PathOutcome,
input: CallPathInput,
): string[] => {
const limitations = [...search.limitations];
if (outcome.capped)
limitations.push(
outcome.budgetExhausted
? `Path enumeration stopped at ${MAX_PATH_EXPANSIONS} expansions`
: `Path enumeration stopped at max_paths=${input.max_paths}`,
);
if (
!outcome.found &&
outcome.shortest !== null &&
outcome.shortest.length - 1 > input.max_depth
)
limitations.push(
`The shortest observed path exceeds max_depth=${input.max_depth}`,
);
return limitations;
};
const summarizeSearch = (
graph: DirectedGraph,
reached: ReadonlyMap<string, number>,
) => ({
nodes: reached.size,
edges: [...reached.keys()].reduce(
(count, node) => count + (graph.hasNode(node) ? graph.outDegree(node) : 0),
0,
),
depth_reached: Math.max(0, ...reached.values()),
});
const prepareCallGraph = (input: CallPathInput) => {
const snapshots = parseSnapshots(input.functions);
assertCompatible(snapshots);
if (!snapshots.has(input.start.address))
throw new TypeError(
`No analyze_function Evidence was supplied for start ${input.start.address}`,
);
return { snapshots, graph: createGraph(snapshots) };
};
const parseSnapshots = (
groups: readonly z.infer<typeof evidenceGroupSchema>[],
groups: readonly CallPathEvidenceGroup[],
): Map<string, FunctionSnapshot> => {
const snapshots = new Map<string, FunctionSnapshot>();
for (const group of groups) {
@@ -416,7 +383,7 @@ const enumerateAtDepth = (state: EnumerationState, remaining: number): void => {
const citePath = (
addresses: readonly string[],
snapshots: ReadonlyMap<string, FunctionSnapshot>,
): OutputPath => {
): OutputCallPath => {
const edges = addresses.slice(0, -1).map((source, index) => {
const target = addresses[index + 1];
if (target === undefined)
@@ -463,5 +430,4 @@ const uniqueEvidence = (snapshots: Iterable<FunctionSnapshot>): string[] =>
...new Set([...snapshots].flatMap((snapshot) => snapshotLinks(snapshot))),
].sort((left, right) => left.localeCompare(right));
const normalizeAddress = (input: string): string =>
inputAddressSchema.parse(input);
const normalizeAddress = (input: string): string => parseCallPathAddress(input);
+194
View File
@@ -0,0 +1,194 @@
import { z } from "zod";
import { evidenceSchema } from "./evidence.js";
const addressSchema = z.string().regex(/^0x(?:0|[1-9a-f][0-9a-f]*)$/u);
const inputAddressSchema = z
.string()
.regex(/^0[xX][0-9a-fA-F]+$/u)
.transform((address) => `0x${BigInt(address).toString(16)}`);
/** Parse a caller-supplied hexadecimal address into canonical form. */
export const parseCallPathAddress = (input: unknown): string =>
inputAddressSchema.parse(input);
const evidenceIdSchema = z.string().regex(/^ev_[a-f0-9]{64}$/u);
const evidenceGroupSchema = z.union([
evidenceSchema,
z.array(evidenceSchema).min(1).max(100),
]);
/** Strict, bounded input for explicit call-path reconstruction. */
export const callPathInputSchema = z
.object({
functions: z.array(evidenceGroupSchema).min(1).max(500),
start: z.object({ address: inputAddressSchema }).strict(),
goal: z.object({ address: inputAddressSchema }).strict(),
max_depth: z.number().int().min(0).max(32).default(8),
max_paths: z.number().int().min(1).max(100).default(10),
offset: z.number().int().min(0).default(0),
limit: z.number().int().min(1).max(100).default(100),
unknown_registry_approved: z.literal(true).optional(),
})
.superRefine(({ functions }, context) => {
const pages = functions.reduce(
(total, group) => total + (Array.isArray(group) ? group.length : 1),
0,
);
if (pages > 2_000)
context.addIssue({
code: "custom",
message: "Call-path Evidence exceeds 2000 pages",
path: ["functions"],
});
});
const citedNodeSchema = z.object({
address: addressSchema,
name: z.string().nullable(),
evidence_links: z.array(evidenceIdSchema).min(1).max(100),
});
const citedEdgeSchema = z.object({
source: addressSchema,
target: addressSchema,
evidence_links: z.array(evidenceIdSchema).min(1).max(100),
});
const pathSchema = z
.object({
hops: z.number().int().min(0),
nodes: z.array(citedNodeSchema).min(1).max(33),
edges: z.array(citedEdgeSchema).max(32),
evidence_links: z.array(evidenceIdSchema).min(1).max(3_300),
})
.superRefine((path, context) => {
const edgesConnectNodes = path.edges.every(
(edge, index) =>
edge.source === path.nodes[index]?.address &&
edge.target === path.nodes[index + 1]?.address,
);
if (
path.hops !== path.edges.length ||
path.nodes.length !== path.edges.length + 1 ||
!edgesConnectNodes
)
context.addIssue({
code: "custom",
message: "Path hops and edges must connect the ordered nodes",
});
});
const searchScopeShape = {
max_depth: z.number().int().min(0).max(32),
max_paths: z.number().int().min(1).max(100),
};
const pathPageShape = {
offset: z.number().int().min(0),
limit: z.number().int().min(1).max(100),
};
const pageContextShape = {
...pathPageShape,
items: z.array(pathSchema).max(100),
returned: z.number().int().min(0).max(100),
next_offset: z.number().int().min(0).nullable(),
};
const completePathPageSchema = z
.object({
...pageContextShape,
total: z.number().int().min(1),
truncated: z.literal(false),
lower_bound: z.number().int().min(1),
})
.superRefine((page, context) => {
const expectedNext =
page.offset + page.items.length < page.total
? page.offset + page.items.length
: null;
if (
page.returned !== page.items.length ||
page.lower_bound !== page.total ||
page.next_offset !== expectedNext
)
context.addIssue({
code: "custom",
message: "Complete path page counters are inconsistent",
});
});
const emptyPathPageSchema = z.object({
...pathPageShape,
items: z.tuple([]),
total: z.literal(0),
returned: z.literal(0),
truncated: z.literal(false),
lower_bound: z.literal(0),
next_offset: z.null(),
});
const truncatedPathPageSchema = z
.object({
...pageContextShape,
total: z.null(),
truncated: z.literal(true),
lower_bound: z.number().int().min(0),
})
.superRefine((page, context) => {
if (page.returned !== page.items.length)
context.addIssue({
code: "custom",
message: "Returned path count must equal retained items",
path: ["returned"],
});
});
const resultContextShape = {
start: addressSchema,
goal: addressSchema,
explored: z.object({
nodes: z.number().int().min(0),
edges: z.number().int().min(0),
depth_reached: z.number().int().min(0),
}),
evidence_links: z.array(evidenceIdSchema).min(1).max(50_000),
limitations: z.array(z.string()),
};
/** Evidence-cited, bounded directed call-path result. */
export const callPathResultSchema = z.union([
z.object({
...resultContextShape,
status: z.literal("found"),
shortest_hops: z.number().int().min(0),
search_scope: z.object({ ...searchScopeShape, exhaustive: z.boolean() }),
paths: completePathPageSchema,
}),
z.object({
...resultContextShape,
status: z.literal("not_found"),
shortest_hops: z.null(),
search_scope: z.object({
...searchScopeShape,
exhaustive: z.literal(true),
}),
paths: emptyPathPageSchema,
}),
z.object({
...resultContextShape,
status: z.literal("unknown"),
shortest_hops: z.null(),
search_scope: z.object({
...searchScopeShape,
exhaustive: z.literal(false),
}),
paths: emptyPathPageSchema,
}),
z.object({
...resultContextShape,
status: z.literal("truncated"),
shortest_hops: z.number().int().min(0).nullable(),
search_scope: z.object({
...searchScopeShape,
exhaustive: z.literal(false),
}),
paths: truncatedPathPageSchema,
}),
]);
export type CallPathEvidenceGroup = z.infer<typeof evidenceGroupSchema>;
export type CallPathInput = z.infer<typeof callPathInputSchema>;
export type CallPathResult = z.infer<typeof callPathResultSchema>;
export type OutputCallPath = z.infer<typeof pathSchema>;
+11
View File
@@ -3,6 +3,7 @@ import { describe, expect, it } from "vitest";
import {
changedBehaviorResultSchema,
findChangedBehavior,
type ChangedBehaviorResult,
} from "./changedBehavior.js";
import { createEvidence, type Evidence } from "./evidence.js";
import type { JsonValue } from "./jsonValue.js";
@@ -123,6 +124,15 @@ const artifactResult = (
limitations: [],
});
const expectInvalidSummary = (result: ChangedBehaviorResult): void => {
expect(
changedBehaviorResultSchema.safeParse({
...result,
summary: { ...result.summary, observed_changes: 0 },
}).success,
).toBe(false);
};
describe("changed behavior", () => {
it("accepts declared-trace process comparison Evidence v4", () => {
const result = findChangedBehavior(
@@ -169,6 +179,7 @@ describe("changed behavior", () => {
summary: { observed_changes: 1, static_candidates: 0 },
findings: { total: 1, next_offset: null },
});
expectInvalidSummary(result);
expect(result.findings.items[0]).toMatchObject({
scope: "runtime",
dimension: "terminal",
+68 -24
View File
@@ -53,30 +53,74 @@ const findingSchema = z.object({
});
/** Truthful aggregate: runtime observations remain separate from static candidates. */
export const changedBehaviorResultSchema = z.object({
behavior_status: z.enum([
"observed_changed",
"observed_unchanged",
"unknown",
"truncated",
]),
summary: z.object({
observed_changes: z.number().int().min(0),
static_candidates: z.number().int().min(0),
contradictions: z.number().int().min(0),
unresolved: z.number().int().min(0),
}),
findings: z.object({
items: z.array(findingSchema).max(100),
offset: z.number().int().min(0),
limit: z.number().int().min(1).max(100),
total: z.number().int().min(0),
next_offset: z.number().int().min(0).nullable(),
}),
evidence_links: z.array(evidenceIdSchema).min(3).max(20_100),
limitations: z.array(z.string()),
investigation_run: investigationRunSummarySchema.optional(),
});
export const changedBehaviorResultSchema = z
.object({
behavior_status: z.enum([
"observed_changed",
"observed_unchanged",
"unknown",
"truncated",
]),
summary: z.object({
observed_changes: z.number().int().min(0),
static_candidates: z.number().int().min(0),
contradictions: z.number().int().min(0),
unresolved: z.number().int().min(0),
}),
findings: z.object({
items: z.array(findingSchema).max(100),
offset: z.number().int().min(0),
limit: z.number().int().min(1).max(100),
total: z.number().int().min(0),
next_offset: z.number().int().min(0).nullable(),
}),
evidence_links: z.array(evidenceIdSchema).min(3).max(20_100),
limitations: z.array(z.string()),
investigation_run: investigationRunSummarySchema.optional(),
})
.superRefine((result, context) => {
const summaryTotal = Object.values(result.summary).reduce(
(total, count) => total + count,
0,
);
const expectedNextOffset =
result.findings.offset + result.findings.items.length <
result.findings.total
? result.findings.offset + result.findings.items.length
: null;
if (summaryTotal !== result.findings.total)
context.addIssue({
code: "custom",
message: "Finding classification counts must equal the finding total",
path: ["summary"],
});
if (result.findings.next_offset !== expectedNextOffset)
context.addIssue({
code: "custom",
message: "Finding cursor must identify the next retained offset",
path: ["findings", "next_offset"],
});
if (
result.findings.offset < result.findings.total &&
result.findings.items.length === 0
)
context.addIssue({
code: "custom",
message: "A nonterminal finding page must retain at least one item",
path: ["findings", "items"],
});
if (
(result.behavior_status === "observed_changed" &&
result.summary.observed_changes === 0) ||
(result.behavior_status === "observed_unchanged" &&
result.summary.observed_changes > 0)
)
context.addIssue({
code: "custom",
message: "Observed-change status must agree with runtime findings",
path: ["behavior_status"],
});
});
export type ChangedBehaviorResult = z.infer<typeof changedBehaviorResultSchema>;
type Finding = z.infer<typeof findingSchema>;
+55 -21
View File
@@ -17,25 +17,59 @@ export const scenarioReplayResultSchema = z.strictObject({
});
/** Result of replaying an entire conformance package. */
export const packageReplayResultSchema = z.strictObject({
package_id: z.string().min(1),
total_scenarios: z.number().int().positive(),
passed: z.number().int().nonnegative(),
failed: z.number().int().nonnegative(),
errored: z.number().int().nonnegative(),
skipped: z.number().int().nonnegative(),
scenario_results: z.array(scenarioReplayResultSchema),
trust_gate_results: z.array(trustGateResultSchema),
drift_detected: z.boolean(),
first_drift: z
.strictObject({
scenario_id: z.string().min(1),
dimension: z.string().min(1),
evidence_id: z.string().min(1),
message: z.string(),
})
.nullable(),
});
export const packageReplayResultSchema = z
.strictObject({
package_id: z.string().min(1),
total_scenarios: z.number().int().positive(),
passed: z.number().int().nonnegative(),
failed: z.number().int().nonnegative(),
errored: z.number().int().nonnegative(),
skipped: z.number().int().nonnegative(),
scenario_results: z.array(scenarioReplayResultSchema),
trust_gate_results: z.array(trustGateResultSchema),
drift_detected: z.boolean(),
first_drift: z
.strictObject({
scenario_id: z.string().min(1),
dimension: z.string().min(1),
evidence_id: z.string().min(1),
message: z.string(),
})
.nullable(),
})
.superRefine((result, context) => {
const count = (status: ScenarioReplayResult["status"]): number =>
result.scenario_results.filter((item) => item.status === status).length;
const countMismatch =
result.passed !== count("pass") ||
result.failed !== count("fail") ||
result.errored !== count("error") ||
result.skipped !== count("skipped");
const driftDetected = result.trust_gate_results.some(
({ verdict }) => verdict === "fail",
);
if (
result.total_scenarios !== result.scenario_results.length ||
countMismatch
)
context.addIssue({
code: "custom",
message: "Scenario totals must summarize scenario results",
path: ["total_scenarios"],
});
if (result.drift_detected !== driftDetected)
context.addIssue({
code: "custom",
message: "Drift status must summarize trust-gate failures",
path: ["drift_detected"],
});
if (!driftDetected && result.first_drift !== null)
context.addIssue({
code: "custom",
message: "A drift witness requires a failed trust gate",
path: ["first_drift"],
});
});
export type ScenarioReplayResult = z.infer<typeof scenarioReplayResultSchema>;
export type PackageReplayResult = z.infer<typeof packageReplayResultSchema>;
@@ -120,7 +154,7 @@ export async function replayConformancePackage(
}
: null;
return {
return packageReplayResultSchema.parse({
package_id: pkg.package_id,
total_scenarios: pkg.scenarios.length,
passed,
@@ -131,5 +165,5 @@ export async function replayConformancePackage(
trust_gate_results: trustGateResults,
drift_detected,
first_drift,
};
});
}
+22 -12
View File
@@ -13,20 +13,30 @@ export const dimensionResultSchema = z.strictObject({
evidence_ids: z.array(z.string()).default([]),
});
/** Result of evaluating a trust gate for one scenario. */
export const trustGateResultSchema = z.strictObject({
const trustGateResultShape = {
scenario_id: z.string().min(1),
verdict: z.enum(["pass", "fail", "unknown"]),
dimension_results: z.array(dimensionResultSchema),
first_divergence: z
.strictObject({
dimension: z.string().min(1),
evidence_id: z.string().min(1),
message: z.string(),
})
.nullable(),
};
const divergenceSchema = z.strictObject({
dimension: z.string().min(1),
evidence_id: z.string().min(1),
message: z.string(),
});
/** Result of evaluating a trust gate for one scenario. */
export const trustGateResultSchema = z.union([
z.strictObject({
...trustGateResultShape,
verdict: z.enum(["pass", "unknown"]),
first_divergence: z.null(),
}),
z.strictObject({
...trustGateResultShape,
verdict: z.literal("fail"),
first_divergence: divergenceSchema.nullable(),
}),
]);
export type DimensionResult = z.infer<typeof dimensionResultSchema>;
export type TrustGateResult = z.infer<typeof trustGateResultSchema>;
@@ -366,12 +376,12 @@ export function evaluateTrustGate(
? "unknown"
: "pass";
return {
return trustGateResultSchema.parse({
scenario_id: contract.scenario_id,
verdict,
dimension_results: dimensionResults,
first_divergence: firstDivergence,
};
});
}
/**
+26
View File
@@ -145,6 +145,7 @@ describe("function comparison", () => {
FUNCTION_COMPARISON_EXAMPLE.left.evidence_id,
FUNCTION_COMPARISON_EXAMPLE.right.evidence_id,
]);
expectFunctionDimensionAlgebra(result);
});
it("normalizes CFG relocation but preserves changed edges and constants", () => {
@@ -272,3 +273,28 @@ describe("function comparison", () => {
expect(pseudocode?.left_digest).not.toBe(pseudocode?.right_digest);
});
});
const expectFunctionDimensionAlgebra = (
result: ReturnType<typeof compareFunctions>,
): void => {
const observed = result.dimensions.find(
({ status }) => status === "unchanged" || status === "changed",
);
expect(observed).toBeDefined();
if (observed === undefined) return;
expect(
functionComparisonResultSchema.safeParse({
...result,
dimensions: [
{ ...observed, left_digest: null, right_digest: null },
...result.dimensions.slice(1),
],
}).success,
).toBe(false);
expect(
functionComparisonResultSchema.safeParse({
...result,
function_match: { ...result.function_match, method: "explicit" },
}).success,
).toBe(false);
};
+23 -14
View File
@@ -23,21 +23,31 @@ export interface DimensionResultInput {
export const dimensionResult = (
input: DimensionResultInput,
): FunctionDimension => ({
dimension: input.dimension,
status: input.status,
left_digest: digest(input.left),
right_digest: digest(input.right),
left_count: input.leftCount,
right_count: input.rightCount,
text_delta: input.textDelta,
conclusion_kind:
input.status === "changed" && input.providersDiffer
): FunctionDimension => {
const context = {
dimension: input.dimension,
left_digest: digest(input.left),
right_digest: digest(input.right),
left_count: input.leftCount,
right_count: input.rightCount,
text_delta: input.textDelta,
evidence_links: [...input.links],
limitations: [...input.limitations],
};
if (input.status === "unchanged")
return {
...context,
status: "unchanged",
conclusion_kind: "derived_relationship",
};
return {
...context,
status: "changed",
conclusion_kind: input.providersDiffer
? "contradiction"
: "derived_relationship",
evidence_links: [...input.links],
limitations: [...input.limitations],
});
};
};
export const unresolvedDimension = (input: {
readonly dimension: DimensionName;
@@ -67,7 +77,6 @@ export const overallStatus = (
return "truncated";
if (
match === "ambiguous" ||
match === "unknown" ||
dimensions.some(({ status }) => status === "unknown")
)
return "unknown";
+45 -19
View File
@@ -2,8 +2,6 @@ import { z } from "zod";
const comparisonStatusSchema = z.enum([
"unchanged",
"added",
"removed",
"changed",
"truncated",
"unknown",
@@ -36,32 +34,60 @@ const textDeltaSchema = z.object({
hunks: z.number().int().min(0),
});
const functionDimensionSchema = z.object({
const functionDimensionContextShape = {
dimension: dimensionNameSchema,
status: comparisonStatusSchema,
left_digest: digestSchema.nullable(),
right_digest: digestSchema.nullable(),
left_count: z.number().int().min(0).nullable(),
right_count: z.number().int().min(0).nullable(),
text_delta: textDeltaSchema.nullable(),
conclusion_kind: z.enum([
"derived_relationship",
"contradiction",
"unresolved_branch",
]),
evidence_links: z.array(evidenceIdSchema).min(2).max(200),
limitations: z.array(z.string()),
});
};
const functionDimensionSchema = z.union([
z.object({
...functionDimensionContextShape,
status: z.literal("unchanged"),
left_digest: digestSchema,
right_digest: digestSchema,
text_delta: textDeltaSchema.nullable(),
conclusion_kind: z.literal("derived_relationship"),
}),
z.object({
...functionDimensionContextShape,
status: z.literal("changed"),
left_digest: digestSchema,
right_digest: digestSchema,
text_delta: textDeltaSchema.nullable(),
conclusion_kind: z.enum(["derived_relationship", "contradiction"]),
}),
z.object({
...functionDimensionContextShape,
status: z.enum(["truncated", "unknown"]),
left_digest: z.null(),
right_digest: z.null(),
text_delta: z.null(),
conclusion_kind: z.literal("unresolved_branch"),
}),
]);
const functionMatchSchema = z.union([
z.object({
status: z.literal("matched"),
method: z.literal("symbol"),
left_name: z.string(),
right_name: z.string(),
}),
z.object({
status: z.enum(["mismatched", "ambiguous"]),
method: z.literal("explicit"),
left_name: z.string(),
right_name: z.string(),
}),
]);
/** Deterministic dimension-classified function comparison Evidence payload. */
export const functionComparisonResultSchema = z.object({
status: comparisonStatusSchema,
function_match: z.object({
status: z.enum(["matched", "mismatched", "ambiguous", "unknown"]),
method: z.enum(["explicit", "symbol"]),
left_name: z.string(),
right_name: z.string(),
}),
function_match: functionMatchSchema,
left_subject_sha256: digestSchema,
right_subject_sha256: digestSchema,
summary: z.object({
+76 -43
View File
@@ -96,38 +96,80 @@ const targetSchema = z.object({
export type InvestigationRunTarget = z.infer<typeof targetSchema>;
const stageSchema = z.enum([
"inventory_left",
"inventory_right",
"compare_artifacts",
"find_changed_behavior",
]);
const investigationRunBaseSchema = z.object({
const investigationRunIdentityShape = {
schema_version: z.literal(1),
run_id: z.string().regex(/^run_[a-f0-9]{64}$/u),
request_sha256: digestSchema,
left: targetSchema,
right: targetSchema,
options: investigationRunOptionsSchema,
left_inventory_evidence_ids: z.array(evidenceIdSchema).min(1).max(100),
right_inventory_evidence_ids: z.array(evidenceIdSchema).min(1).max(100),
limitations: z.array(z.string().max(4_096)).max(100),
};
const investigationRunContextShape = {
...investigationRunIdentityShape,
integrity_policy: z.enum(["fail", "record-and-continue"]),
integrity_continue_approved: z.boolean(),
max_integrity_mismatches: z.number().int().min(1).max(100),
status: z.enum(["running", "complete"]),
completed_stages: z.array(stageSchema).max(4),
left_inventory_evidence_ids: z.array(evidenceIdSchema).min(1).max(100),
right_inventory_evidence_ids: z.array(evidenceIdSchema).min(1).max(100),
comparison_evidence_id: evidenceIdSchema.nullable(),
result_evidence_id: evidenceIdSchema.nullable(),
limitations: z.array(z.string().max(4_096)).max(100),
});
};
const inventoryRunStateShape = {
status: z.literal("running"),
completed_stages: z.tuple([
z.literal("inventory_left"),
z.literal("inventory_right"),
]),
comparison_evidence_id: z.null(),
result_evidence_id: z.null(),
};
const comparisonRunStateShape = {
status: z.literal("running"),
completed_stages: z.tuple([
z.literal("inventory_left"),
z.literal("inventory_right"),
z.literal("compare_artifacts"),
]),
comparison_evidence_id: evidenceIdSchema,
result_evidence_id: z.null(),
};
const completedRunStateShape = {
status: z.literal("complete"),
completed_stages: z.tuple([
z.literal("inventory_left"),
z.literal("inventory_right"),
z.literal("compare_artifacts"),
z.literal("find_changed_behavior"),
]),
comparison_evidence_id: evidenceIdSchema,
result_evidence_id: evidenceIdSchema,
};
const investigationRunStateSchemas = [
z.object({ ...investigationRunContextShape, ...inventoryRunStateShape }),
z.object({ ...investigationRunContextShape, ...comparisonRunStateShape }),
z.object({ ...investigationRunContextShape, ...completedRunStateShape }),
] as const;
const legacyIdentityRunStateSchemas = [
z.object({
...investigationRunContextShape,
...inventoryRunStateShape,
legacy_request_identity: z.literal(true),
}),
z.object({
...investigationRunContextShape,
...comparisonRunStateShape,
legacy_request_identity: z.literal(true),
}),
z.object({
...investigationRunContextShape,
...completedRunStateShape,
legacy_request_identity: z.literal(true),
}),
] as const;
export const investigationRunSchema = z
.union([
investigationRunBaseSchema.extend({
legacy_request_identity: z.literal(true),
}),
investigationRunBaseSchema,
z.union(legacyIdentityRunStateSchemas),
z.union(investigationRunStateSchemas),
])
.superRefine((run, context) => {
if (!("legacy_request_identity" in run)) return;
@@ -175,13 +217,20 @@ export const investigationWorkspaceSchema = z.object({
runs: z.array(investigationRunSchema).max(1_000),
});
const legacyInvestigationRunSchema = investigationRunBaseSchema
.omit({
integrity_policy: true,
integrity_continue_approved: true,
max_integrity_mismatches: true,
})
.strict();
const legacyInvestigationRunSchema = z.union([
z.strictObject({
...investigationRunIdentityShape,
...inventoryRunStateShape,
}),
z.strictObject({
...investigationRunIdentityShape,
...comparisonRunStateShape,
}),
z.strictObject({
...investigationRunIdentityShape,
...completedRunStateShape,
}),
]);
const legacyInvestigationWorkspaceSchema = investigationWorkspaceSchema
.extend({
@@ -324,7 +373,6 @@ const validateRunReferences = (workspace: InvestigationWorkspace): void => {
run.request_sha256 !== legacyIdentity.requestSha256)
)
throw new TypeError("Investigation run identity is invalid");
validateRunStages(run);
const references = [
...run.left_inventory_evidence_ids,
...run.right_inventory_evidence_ids,
@@ -417,21 +465,6 @@ const migrateLegacyWorkspace = (input: unknown): InvestigationWorkspace => {
return migrated;
};
const validateRunStages = (run: InvestigationRun): void => {
const expected = [
"inventory_left",
"inventory_right",
...(run.comparison_evidence_id === null ? [] : ["compare_artifacts"]),
...(run.result_evidence_id === null ? [] : ["find_changed_behavior"]),
];
if (JSON.stringify(run.completed_stages) !== JSON.stringify(expected))
throw new TypeError("Investigation run stages are inconsistent");
if (run.status === "complete" && run.result_evidence_id === null)
throw new TypeError("Completed investigation run has no result Evidence");
if (run.status === "running" && run.result_evidence_id !== null)
throw new TypeError("Running investigation run has final result Evidence");
};
const workspaceIdFor = (name: string): string =>
`ws_${digestCanonical({ schema: "rea.workspace/v1", name })}`;
@@ -69,6 +69,7 @@ export const compareJavaScriptApplicationVersions = (
omitted_graph_edges: changeGraph.omittedEdges,
omitted_graph_observations: changeGraph.omittedObservations,
};
const coverage = comparisonCoverage(input, omissions);
const semantic = {
schema_version: 1 as const,
left: {
@@ -85,12 +86,7 @@ export const compareJavaScriptApplicationVersions = (
matching: matchingSummary(projection.items),
items,
graph: changeGraph.graph,
coverage: {
status: comparisonCoverageStatus(input, omissions),
left_graph_status: input.left.graph.coverage.status,
right_graph_status: input.right.graph.coverage.status,
...omissions,
},
coverage,
evidence_links: evidenceLinks,
limitations: comparisonLimitations(input, omissions),
};
@@ -134,27 +130,52 @@ const countBasis = (
): number => items.filter(({ match }) => match.basis === basis).length;
type ComparisonOmissions = ApplicationVersionComparisonResult["coverage"];
type ComparisonOmissionCounts = Pick<
ComparisonOmissions,
| "omitted_comparison_items"
| "omitted_candidate_references"
| "omitted_graph_nodes"
| "omitted_graph_edges"
| "omitted_graph_observations"
>;
const comparisonCoverageStatus = (
const comparisonCoverage = (
input: ApplicationVersionComparisonProjectionInput,
omissions: Omit<
ComparisonOmissions,
"status" | "left_graph_status" | "right_graph_status"
>,
): ApplicationVersionComparisonResult["coverage"]["status"] =>
Object.values(omissions).some((value) => value > 0)
? "truncated"
: input.left.graph.coverage.status === "complete" &&
input.right.graph.coverage.status === "complete"
? "complete-within-inputs"
: "partial";
omissions: ComparisonOmissionCounts,
): ApplicationVersionComparisonResult["coverage"] => {
const context = {
left_graph_status: input.left.graph.coverage.status,
right_graph_status: input.right.graph.coverage.status,
};
if (Object.values(omissions).some((value) => value > 0))
return { ...context, ...omissions, status: "truncated" };
if (
input.left.graph.coverage.status === "complete" &&
input.right.graph.coverage.status === "complete"
)
return {
...context,
status: "complete-within-inputs",
omitted_comparison_items: 0,
omitted_candidate_references: 0,
omitted_graph_nodes: 0,
omitted_graph_edges: 0,
omitted_graph_observations: 0,
};
return {
...context,
status: "partial",
omitted_comparison_items: 0,
omitted_candidate_references: 0,
omitted_graph_nodes: 0,
omitted_graph_edges: 0,
omitted_graph_observations: 0,
};
};
const comparisonLimitations = (
input: ApplicationVersionComparisonProjectionInput,
omissions: Omit<
ComparisonOmissions,
"status" | "left_graph_status" | "right_graph_status"
>,
omissions: ComparisonOmissionCounts,
): string[] =>
uniqueSorted([
"Application comparison never uses bundler module ordinals as persistent identity and performs no fuzzy pairing.",
@@ -51,29 +51,66 @@ export const compareApplicationVersionsInputSchema = z
}
});
const applicationVersionMatchBasisSchema = z.enum([
"exact-node-identity",
"exact-content-digest",
"exact-module-source-digest",
"source-map-identity",
"structural-fingerprint",
"semantic-key",
"none",
const emptyCandidateNodesSchema = z.tuple([]);
const candidateNodesSchema = z
.tuple([nodeIdSchema])
.rest(nodeIdSchema)
.check(z.maxLength(1_000));
const exactMatchSchema = z.strictObject({
status: z.literal("matched"),
basis: z.enum([
"exact-node-identity",
"exact-content-digest",
"exact-module-source-digest",
]),
confidence: z.literal("exact"),
candidate_left_node_ids: emptyCandidateNodesSchema,
candidate_right_node_ids: emptyCandidateNodesSchema,
});
const sourceMapMatchSchema = z.strictObject({
status: z.literal("matched"),
basis: z.literal("source-map-identity"),
confidence: z.literal("high"),
candidate_left_node_ids: emptyCandidateNodesSchema,
candidate_right_node_ids: emptyCandidateNodesSchema,
});
const inferredMatchSchema = z.strictObject({
status: z.literal("matched"),
basis: z.enum(["structural-fingerprint", "semantic-key"]),
confidence: z.literal("medium"),
candidate_left_node_ids: emptyCandidateNodesSchema,
candidate_right_node_ids: emptyCandidateNodesSchema,
});
const matchedItemMatchSchema = z.union([
exactMatchSchema,
sourceMapMatchSchema,
inferredMatchSchema,
]);
const unmatchedItemMatchSchema = z.strictObject({
status: z.literal("unmatched"),
basis: z.literal("none"),
confidence: z.literal("unknown"),
candidate_left_node_ids: emptyCandidateNodesSchema,
candidate_right_node_ids: emptyCandidateNodesSchema,
});
const leftAmbiguousMatchSchema = z.strictObject({
status: z.literal("ambiguous"),
basis: z.literal("none"),
confidence: z.literal("unknown"),
candidate_left_node_ids: emptyCandidateNodesSchema,
candidate_right_node_ids: candidateNodesSchema,
});
const rightAmbiguousMatchSchema = z.strictObject({
status: z.literal("ambiguous"),
basis: z.literal("none"),
confidence: z.literal("unknown"),
candidate_left_node_ids: candidateNodesSchema,
candidate_right_node_ids: emptyCandidateNodesSchema,
});
const comparisonItemSchema = z.strictObject({
const comparisonItemContextShape = {
item_id: z.string().regex(/^javc_item_[a-f0-9]{64}$/u),
status: z.enum(["unchanged", "added", "removed", "changed", "unknown"]),
node_kind: z.enum(JAVASCRIPT_APPLICATION_NODE_KINDS),
left_node_id: nodeIdSchema.nullable(),
right_node_id: nodeIdSchema.nullable(),
match: z.strictObject({
status: z.enum(["matched", "unmatched", "ambiguous"]),
basis: applicationVersionMatchBasisSchema,
confidence: z.enum(["exact", "high", "medium", "unknown"]),
candidate_left_node_ids: z.array(nodeIdSchema).max(1_000),
candidate_right_node_ids: z.array(nodeIdSchema).max(1_000),
}),
dimensions: z
.array(
z.enum([
@@ -88,64 +125,147 @@ const comparisonItemSchema = z.strictObject({
.max(6),
evidence_links: z.array(evidenceIdSchema).min(2).max(130),
limitations: z.array(boundedTextSchema).max(100),
});
};
const comparisonItemSchema = z.union([
z.strictObject({
...comparisonItemContextShape,
status: z.enum(["unchanged", "changed", "unknown"]),
left_node_id: nodeIdSchema,
right_node_id: nodeIdSchema,
match: matchedItemMatchSchema,
}),
z.strictObject({
...comparisonItemContextShape,
status: z.enum(["removed", "unknown"]),
left_node_id: nodeIdSchema,
right_node_id: z.null(),
match: unmatchedItemMatchSchema,
}),
z.strictObject({
...comparisonItemContextShape,
status: z.enum(["added", "unknown"]),
left_node_id: z.null(),
right_node_id: nodeIdSchema,
match: unmatchedItemMatchSchema,
}),
z.strictObject({
...comparisonItemContextShape,
status: z.literal("unknown"),
left_node_id: nodeIdSchema,
right_node_id: z.null(),
match: leftAmbiguousMatchSchema,
}),
z.strictObject({
...comparisonItemContextShape,
status: z.literal("unknown"),
left_node_id: z.null(),
right_node_id: nodeIdSchema,
match: rightAmbiguousMatchSchema,
}),
]);
const comparisonCoverageContextShape = {
left_graph_status: z.enum(["complete", "partial", "unknown", "unavailable"]),
right_graph_status: z.enum(["complete", "partial", "unknown", "unavailable"]),
};
const zeroComparisonOmissionsShape = {
omitted_comparison_items: z.literal(0),
omitted_candidate_references: z.literal(0),
omitted_graph_nodes: z.literal(0),
omitted_graph_edges: z.literal(0),
omitted_graph_observations: z.literal(0),
};
const comparisonOmissionsShape = {
omitted_comparison_items: z.number().int().min(0),
omitted_candidate_references: z.number().int().min(0),
omitted_graph_nodes: z.number().int().min(0),
omitted_graph_edges: z.number().int().min(0),
omitted_graph_observations: z.number().int().min(0),
};
const comparisonCoverageSchema = z.union([
z.strictObject({
...comparisonCoverageContextShape,
...zeroComparisonOmissionsShape,
status: z.literal("complete-within-inputs"),
}),
z.strictObject({
...comparisonCoverageContextShape,
...zeroComparisonOmissionsShape,
status: z.literal("partial"),
}),
z
.strictObject({
...comparisonCoverageContextShape,
...comparisonOmissionsShape,
status: z.literal("truncated"),
})
.refine(
(coverage) =>
[
coverage.omitted_comparison_items,
coverage.omitted_candidate_references,
coverage.omitted_graph_nodes,
coverage.omitted_graph_edges,
coverage.omitted_graph_observations,
].some((value) => value > 0),
{ message: "Truncated comparison coverage must name an omission" },
),
]);
/** Tiered module/entity matching plus a bounded cross-version change graph. */
export const applicationVersionComparisonResultSchema = z.strictObject({
schema_version: z.literal(1),
comparison_id: z.string().regex(/^javc_[a-f0-9]{64}$/u),
left: z.strictObject({
evidence_id: evidenceIdSchema,
graph_id: z.string().regex(/^jag_[a-f0-9]{64}$/u),
root_artifact_sha256: z.string().regex(/^[a-f0-9]{64}$/u),
}),
right: z.strictObject({
evidence_id: evidenceIdSchema,
graph_id: z.string().regex(/^jag_[a-f0-9]{64}$/u),
root_artifact_sha256: z.string().regex(/^[a-f0-9]{64}$/u),
}),
summary: z.strictObject({
unchanged: z.number().int().min(0),
added: z.number().int().min(0),
removed: z.number().int().min(0),
changed: z.number().int().min(0),
unknown: z.number().int().min(0),
}),
matching: z.strictObject({
exact_node_identity: z.number().int().min(0),
exact_content_digest: z.number().int().min(0),
exact_module_source_digest: z.number().int().min(0),
source_map_identity: z.number().int().min(0),
structural_fingerprint: z.number().int().min(0),
semantic_key: z.number().int().min(0),
ambiguous: z.number().int().min(0),
unmatched: z.number().int().min(0),
}),
items: z.array(comparisonItemSchema).max(50_000),
graph: javascriptApplicationGraphSchema,
coverage: z.strictObject({
status: z.enum(["complete-within-inputs", "partial", "truncated"]),
left_graph_status: z.enum([
"complete",
"partial",
"unknown",
"unavailable",
]),
right_graph_status: z.enum([
"complete",
"partial",
"unknown",
"unavailable",
]),
omitted_comparison_items: z.number().int().min(0),
omitted_candidate_references: z.number().int().min(0),
omitted_graph_nodes: z.number().int().min(0),
omitted_graph_edges: z.number().int().min(0),
omitted_graph_observations: z.number().int().min(0),
}),
evidence_links: z.array(evidenceIdSchema).min(2).max(130),
limitations: z.array(boundedTextSchema).max(1_000),
});
export const applicationVersionComparisonResultSchema = z
.strictObject({
schema_version: z.literal(1),
comparison_id: z.string().regex(/^javc_[a-f0-9]{64}$/u),
left: z.strictObject({
evidence_id: evidenceIdSchema,
graph_id: z.string().regex(/^jag_[a-f0-9]{64}$/u),
root_artifact_sha256: z.string().regex(/^[a-f0-9]{64}$/u),
}),
right: z.strictObject({
evidence_id: evidenceIdSchema,
graph_id: z.string().regex(/^jag_[a-f0-9]{64}$/u),
root_artifact_sha256: z.string().regex(/^[a-f0-9]{64}$/u),
}),
summary: z.strictObject({
unchanged: z.number().int().min(0),
added: z.number().int().min(0),
removed: z.number().int().min(0),
changed: z.number().int().min(0),
unknown: z.number().int().min(0),
}),
matching: z.strictObject({
exact_node_identity: z.number().int().min(0),
exact_content_digest: z.number().int().min(0),
exact_module_source_digest: z.number().int().min(0),
source_map_identity: z.number().int().min(0),
structural_fingerprint: z.number().int().min(0),
semantic_key: z.number().int().min(0),
ambiguous: z.number().int().min(0),
unmatched: z.number().int().min(0),
}),
items: z.array(comparisonItemSchema).max(50_000),
graph: javascriptApplicationGraphSchema,
coverage: comparisonCoverageSchema,
evidence_links: z.array(evidenceIdSchema).min(2).max(130),
limitations: z.array(boundedTextSchema).max(1_000),
})
.superRefine((result, context) => {
const sourceGraphsComplete =
result.coverage.left_graph_status === "complete" &&
result.coverage.right_graph_status === "complete";
if (
result.coverage.status !== "truncated" &&
(result.coverage.status === "complete-within-inputs") !==
sourceGraphsComplete
)
context.addIssue({
code: "custom",
path: ["coverage", "status"],
message: "Comparison coverage must match source graph completeness",
});
});
export type CompareApplicationVersionsInput = z.infer<
typeof compareApplicationVersionsInputSchema
@@ -156,6 +276,3 @@ export type ApplicationVersionComparisonItem = z.infer<
export type ApplicationVersionComparisonResult = z.infer<
typeof applicationVersionComparisonResultSchema
>;
export type ApplicationVersionMatchBasis = z.infer<
typeof applicationVersionMatchBasisSchema
>;
+171 -63
View File
@@ -36,6 +36,27 @@ interface ItemContext {
readonly maxCandidates: number;
}
type OneSidedNode =
| {
readonly left: ApplicationNode;
readonly right?: never;
readonly candidateRightNodeIds: readonly string[];
readonly candidateLeftNodeIds?: never;
}
| {
readonly left?: never;
readonly right: ApplicationNode;
readonly candidateRightNodeIds?: never;
readonly candidateLeftNodeIds: readonly string[];
};
type ComparisonItemSemantic<
Item extends
ApplicationVersionComparisonItem = ApplicationVersionComparisonItem,
> = Item extends ApplicationVersionComparisonItem
? Omit<Item, "item_id">
: never;
/** Classify matched, unmatched, and ambiguous nodes without inventing absence. */
export const classifyJavaScriptApplicationVersions = (
matching: ApplicationVersionMatchingProjection,
@@ -64,9 +85,10 @@ export const classifyJavaScriptApplicationVersions = (
candidates.length - context.maxCandidates,
);
return unmatchedItem(
node,
"left",
candidates.slice(0, context.maxCandidates),
{
left: node,
candidateRightNodeIds: candidates.slice(0, context.maxCandidates),
},
fullContext,
);
});
@@ -77,9 +99,10 @@ export const classifyJavaScriptApplicationVersions = (
candidates.length - context.maxCandidates,
);
return unmatchedItem(
node,
"right",
candidates.slice(0, context.maxCandidates),
{
right: node,
candidateLeftNodeIds: candidates.slice(0, context.maxCandidates),
},
fullContext,
);
});
@@ -112,18 +135,12 @@ const matchedItem = (
status === "unknown" && !dimensions.includes("coverage")
? [...dimensions, "coverage" as const]
: dimensions;
const semantic = {
const semantic: ComparisonItemSemantic = {
status,
node_kind: pair.left.kind,
left_node_id: pair.left.node_id,
right_node_id: pair.right.node_id,
match: {
status: "matched" as const,
basis: pair.basis,
confidence: pair.confidence,
candidate_left_node_ids: [],
candidate_right_node_ids: [],
},
match: matchedMatch(pair),
dimensions: finalDimensions,
evidence_links: itemEvidenceLinks(pair.left, pair.right, context),
limitations: pairLimitations(pair, status),
@@ -132,59 +149,150 @@ const matchedItem = (
};
const unmatchedItem = (
member: OneSidedNode,
context: ItemContext,
): ApplicationVersionComparisonItem =>
member.left === undefined
? rightOnlyItem(member.right, member.candidateLeftNodeIds, context)
: leftOnlyItem(member.left, member.candidateRightNodeIds, context);
const leftOnlyItem = (
node: ApplicationNode,
side: "left" | "right",
candidates: readonly string[],
candidateNodeIds: readonly string[],
context: ItemContext,
): ApplicationVersionComparisonItem => {
const ambiguous = candidates.length > 0;
const oppositeComplete =
(side === "left" ? context.rightGraph : context.leftGraph).coverage
.status === "complete";
const status: ApplicationVersionComparisonItem["status"] = ambiguous
? "unknown"
: oppositeComplete
? side === "left"
? "removed"
: "added"
: "unknown";
const semantic = {
status,
const [firstCandidate, ...remainingCandidates] = candidateNodeIds;
const common = {
node_kind: node.kind,
left_node_id: side === "left" ? node.node_id : null,
right_node_id: side === "right" ? node.node_id : null,
match: {
status: ambiguous ? ("ambiguous" as const) : ("unmatched" as const),
basis: "none" as const,
confidence: "unknown" as const,
candidate_left_node_ids: side === "right" ? [...candidates] : [],
candidate_right_node_ids: side === "left" ? [...candidates] : [],
},
dimensions: [
"availability" as const,
...(status === "unknown" && !ambiguous ? (["coverage"] as const) : []),
],
evidence_links: itemEvidenceLinks(
side === "left" ? node : undefined,
side === "right" ? node : undefined,
context,
),
limitations: [
...(ambiguous
? [
"Multiple nodes share the same permitted match key; no cross-version pair was selected.",
]
: []),
...(status === "unknown" && !ambiguous
? [
"The opposite graph is incomplete, so an unmatched node cannot be classified as added or removed.",
]
: []),
],
evidence_links: itemEvidenceLinks(node, undefined, context),
};
return itemWithId(semantic);
if (firstCandidate !== undefined)
return itemWithId({
...common,
status: "unknown",
left_node_id: node.node_id,
right_node_id: null,
match: {
status: "ambiguous",
basis: "none",
confidence: "unknown",
candidate_left_node_ids: [],
candidate_right_node_ids: [firstCandidate, ...remainingCandidates],
},
dimensions: ["availability"],
limitations: [AMBIGUOUS_MATCH_LIMITATION],
});
const absenceObserved = context.rightGraph.coverage.status === "complete";
return itemWithId({
...common,
status: absenceObserved ? "removed" : "unknown",
left_node_id: node.node_id,
right_node_id: null,
match: unmatchedMatch(),
dimensions: absenceObserved
? ["availability"]
: ["availability", "coverage"],
limitations: absenceObserved ? [] : [INCOMPLETE_GRAPH_LIMITATION],
});
};
const rightOnlyItem = (
node: ApplicationNode,
candidateNodeIds: readonly string[],
context: ItemContext,
): ApplicationVersionComparisonItem => {
const [firstCandidate, ...remainingCandidates] = candidateNodeIds;
const common = {
node_kind: node.kind,
evidence_links: itemEvidenceLinks(undefined, node, context),
};
if (firstCandidate !== undefined)
return itemWithId({
...common,
status: "unknown",
left_node_id: null,
right_node_id: node.node_id,
match: {
status: "ambiguous",
basis: "none",
confidence: "unknown",
candidate_left_node_ids: [firstCandidate, ...remainingCandidates],
candidate_right_node_ids: [],
},
dimensions: ["availability"],
limitations: [AMBIGUOUS_MATCH_LIMITATION],
});
const absenceObserved = context.leftGraph.coverage.status === "complete";
return itemWithId({
...common,
status: absenceObserved ? "added" : "unknown",
left_node_id: null,
right_node_id: node.node_id,
match: unmatchedMatch(),
dimensions: absenceObserved
? ["availability"]
: ["availability", "coverage"],
limitations: absenceObserved ? [] : [INCOMPLETE_GRAPH_LIMITATION],
});
};
const matchedMatch = (
pair: ApplicationVersionNodePair,
): Extract<
ApplicationVersionComparisonItem["match"],
{ readonly status: "matched" }
> => {
const candidates: {
readonly candidate_left_node_ids: [];
readonly candidate_right_node_ids: [];
} = {
candidate_left_node_ids: [],
candidate_right_node_ids: [],
};
switch (pair.basis) {
case "exact-node-identity":
case "exact-content-digest":
case "exact-module-source-digest":
return {
status: "matched",
basis: pair.basis,
confidence: "exact",
...candidates,
};
case "source-map-identity":
return {
status: "matched",
basis: pair.basis,
confidence: "high",
...candidates,
};
case "structural-fingerprint":
case "semantic-key":
return {
status: "matched",
basis: pair.basis,
confidence: "medium",
...candidates,
};
}
};
const unmatchedMatch = (): Extract<
ApplicationVersionComparisonItem["match"],
{ readonly status: "unmatched" }
> => ({
status: "unmatched",
basis: "none",
confidence: "unknown",
candidate_left_node_ids: [],
candidate_right_node_ids: [],
});
const AMBIGUOUS_MATCH_LIMITATION =
"Multiple nodes share the same permitted match key; no cross-version pair was selected.";
const INCOMPLETE_GRAPH_LIMITATION =
"The opposite graph is incomplete, so an unmatched node cannot be classified as added or removed.";
const changedDimensions = (
pair: ApplicationVersionNodePair,
context: ItemContext,
@@ -340,9 +448,9 @@ const pairLimitations = (
: []),
];
const itemWithId = (
semantic: Omit<ApplicationVersionComparisonItem, "item_id">,
): ApplicationVersionComparisonItem => ({
const itemWithId = <Semantic extends ComparisonItemSemantic>(
semantic: Semantic,
): Semantic & { readonly item_id: string } => ({
...semantic,
item_id: `javc_item_${digestCanonical(semantic)}`,
});
+43 -16
View File
@@ -1,15 +1,29 @@
import type { ApplicationNode } from "./javascriptApplicationGraph.js";
import { compareCodePoints } from "./javascriptApplicationGraph.js";
import type { ApplicationVersionMatchBasis } from "./javascriptApplicationVersionComparisonSchemas.js";
import type { JsonValue } from "./jsonValue.js";
/** One deterministic one-to-one match between two graph versions. */
export interface ApplicationVersionNodePair {
type ApplicationVersionPairMatch =
| {
readonly basis:
| "exact-node-identity"
| "exact-content-digest"
| "exact-module-source-digest";
readonly confidence: "exact";
}
| {
readonly basis: "source-map-identity";
readonly confidence: "high";
}
| {
readonly basis: "structural-fingerprint" | "semantic-key";
readonly confidence: "medium";
};
export type ApplicationVersionNodePair = {
readonly left: ApplicationNode;
readonly right: ApplicationNode;
readonly basis: Exclude<ApplicationVersionMatchBasis, "none">;
readonly confidence: "exact" | "high" | "medium";
}
} & ApplicationVersionPairMatch;
/** Matched nodes plus ambiguity candidates that were never forced into pairs. */
export interface ApplicationVersionMatchingProjection {
@@ -21,26 +35,35 @@ export interface ApplicationVersionMatchingProjection {
}
interface MatchTier {
readonly basis: ApplicationVersionNodePair["basis"];
readonly confidence: ApplicationVersionNodePair["confidence"];
readonly match: ApplicationVersionPairMatch;
readonly key: (node: ApplicationNode) => string | null;
}
const matchTiers = (): readonly MatchTier[] => [
{ basis: "exact-content-digest", confidence: "exact", key: contentDigestKey },
{
basis: "exact-module-source-digest",
confidence: "exact",
match: { basis: "exact-content-digest", confidence: "exact" },
key: contentDigestKey,
},
{
match: { basis: "exact-module-source-digest", confidence: "exact" },
key: moduleSourceDigestKey,
},
{ basis: "exact-node-identity", confidence: "exact", key: nodeIdentityKey },
{ basis: "source-map-identity", confidence: "high", key: sourceMapKey },
{
basis: "structural-fingerprint",
confidence: "medium",
match: { basis: "exact-node-identity", confidence: "exact" },
key: nodeIdentityKey,
},
{
match: { basis: "source-map-identity", confidence: "high" },
key: sourceMapKey,
},
{
match: { basis: "structural-fingerprint", confidence: "medium" },
key: structuralFingerprintKey,
},
{ basis: "semantic-key", confidence: "medium", key: semanticKey },
{
match: { basis: "semantic-key", confidence: "medium" },
key: semanticKey,
},
];
/** Apply ordered, unique-only matching tiers without fuzzy pairing. */
@@ -99,7 +122,11 @@ const applyTier = (context: TierContext): void => {
const leftNode = leftGroup[0];
const rightNode = rightGroup[0];
if (leftNode === undefined || rightNode === undefined) continue;
context.pairs.push({ left: leftNode, right: rightNode, ...context.tier });
context.pairs.push({
left: leftNode,
right: rightNode,
...context.tier.match,
});
context.left.delete(leftNode.node_id);
context.right.delete(rightNode.node_id);
}
@@ -3,6 +3,7 @@ import { EMPTY_PROCESS_CAPTURE_EXAMPLE } from "../contracts/processCaptureExampl
import { parseProcessCapture, type ProcessCapture } from "./processCapture.js";
import {
compareProcessTraces,
processTraceComparisonResultSchema,
type ProcessTraceSpecification,
} from "./processTraceComparison.js";
const emptyCapture = parseProcessCapture(EMPTY_PROCESS_CAPTURE_EXAMPLE);
@@ -195,6 +196,12 @@ describe("process trace comparison", () => {
locations: [{ capture_order: 2 }, { capture_order: 1 }],
},
});
expect(
processTraceComparisonResultSchema.safeParse({
...comparison,
right: { ...comparison.right, matched_variant: "invented" },
}).success,
).toBe(false);
});
it("enforces explicit negative not-before constraints", () => {
const specification: ProcessTraceSpecification = {
+15 -4
View File
@@ -26,12 +26,23 @@ const matchedEventSchema = z.strictObject({
location: locationSchema,
});
const sideResultSchema = z.strictObject({
status: z.enum(["pass", "fail", "unknown"]),
matched_variant: identifierSchema.nullable(),
const sideResultShape = {
satisfied_constraints: z.array(z.string()),
raw_trace: z.array(matchedEventSchema),
});
};
const sideResultSchema = z.union([
z.strictObject({
...sideResultShape,
status: z.literal("pass"),
matched_variant: identifierSchema.nullable(),
}),
z.strictObject({
...sideResultShape,
status: z.enum(["fail", "unknown"]),
matched_variant: z.null(),
}),
]);
const diagnosticSchema = z.strictObject({
kind: z.enum([
@@ -232,6 +232,18 @@ describe("reconstruction verification", () => {
summary: { total: 1, passed: 1, failed: 0, unknown: 0 },
claims: { total: 1, next_offset: null },
});
expect(
reconstructionVerificationResultSchema.safeParse({
...result,
summary: { ...result.summary, passed: 0 },
}).success,
).toBe(false);
expect(
reconstructionVerificationResultSchema.safeParse({
...result,
status: "unknown",
}).success,
).toBe(false);
expect(result.claims.items[0]?.evidence_links).toEqual(
expect.arrayContaining([
comparison.evidence_id,
+62 -31
View File
@@ -114,37 +114,68 @@ export const reconstructionClaimResultSchema = z.object({
});
/** Evidence-cited result over every declared claim, independently paged. */
export const reconstructionVerificationResultSchema = z.object({
status: verificationStatusSchema,
specification_sha256: digestSchema,
summary: z.object({
total: z.number().int().min(1).max(100),
passed: z.number().int().min(0),
failed: z.number().int().min(0),
unknown: z.number().int().min(0),
behavioral: z.number().int().min(0),
structural: z.number().int().min(0),
}),
claims: z.object({
items: z.array(reconstructionClaimResultSchema).max(100),
offset: z.number().int().min(0),
limit: z.number().int().min(1).max(100),
total: z.number().int().min(1).max(100),
next_offset: z.number().int().min(0).nullable(),
}),
recommended_probes: z
.array(
z.object({
operation: z.string().min(1).max(200),
rationale: z.string().min(1).max(1_000),
claim_ids: z.array(claimIdSchema).min(1).max(100),
unknown_ids: z.array(unknownIdSchema).max(100),
}),
)
.max(2_000),
evidence_links: z.array(evidenceIdSchema).min(3).max(20_100),
limitations: z.array(z.string()),
});
export const reconstructionVerificationResultSchema = z
.object({
status: verificationStatusSchema,
specification_sha256: digestSchema,
summary: z.object({
total: z.number().int().min(1).max(100),
passed: z.number().int().min(0),
failed: z.number().int().min(0),
unknown: z.number().int().min(0),
behavioral: z.number().int().min(0),
structural: z.number().int().min(0),
}),
claims: z.object({
items: z.array(reconstructionClaimResultSchema).max(100),
offset: z.number().int().min(0),
limit: z.number().int().min(1).max(100),
total: z.number().int().min(1).max(100),
next_offset: z.number().int().min(0).nullable(),
}),
recommended_probes: z
.array(
z.object({
operation: z.string().min(1).max(200),
rationale: z.string().min(1).max(1_000),
claim_ids: z.array(claimIdSchema).min(1).max(100),
unknown_ids: z.array(unknownIdSchema).max(100),
}),
)
.max(2_000),
evidence_links: z.array(evidenceIdSchema).min(3).max(20_100),
limitations: z.array(z.string()),
})
.superRefine((result, context) => {
const { summary } = result;
const expectedStatus =
summary.failed > 0 ? "fail" : summary.unknown > 0 ? "unknown" : "pass";
const issues = [
[
summary.total !== summary.passed + summary.failed + summary.unknown,
"Claim status counts must equal the summary total",
["summary", "total"],
],
[
summary.total !== summary.behavioral + summary.structural,
"Claim kind counts must equal the summary total",
["summary", "total"],
],
[
result.claims.total !== summary.total,
"Paged claim total must equal the summary total",
["claims", "total"],
],
[
result.status !== expectedStatus,
"Verification status must summarize claim statuses",
["status"],
],
] as const;
for (const [invalid, message, path] of issues)
if (invalid)
context.addIssue({ code: "custom", message, path: [...path] });
});
export type ReconstructionClaim = z.infer<typeof reconstructionClaimSchema>;
export type ReconstructionClaimResult = z.infer<
+30 -7
View File
@@ -16,6 +16,8 @@ const HASH_B = "b".repeat(64);
type Entry = HistoricalSourceGraphInput["entries"][number];
type DirectoryEntry = Extract<Entry, { kind: "directory" }>;
type FileEntry = Extract<Entry, { kind: "file" }>;
type HashedFileEntry = Extract<FileEntry, { content_state: "hashed" }>;
type UnavailableFileEntry = Exclude<FileEntry, HashedFileEntry>;
type SymlinkEntry = Extract<Entry, { kind: "symlink" }>;
const directoryEntry = (
@@ -28,7 +30,9 @@ const directoryEntry = (
limitations: [],
});
const fileEntry = (overrides: Partial<FileEntry> = {}): FileEntry => ({
const fileEntry = (
overrides: Partial<HashedFileEntry> = {},
): HashedFileEntry => ({
path: "src/main.ts",
kind: "file",
sha256: HASH_B,
@@ -40,6 +44,20 @@ const fileEntry = (overrides: Partial<FileEntry> = {}): FileEntry => ({
...overrides,
});
const unavailableFileEntry = (
overrides: Partial<UnavailableFileEntry> = {},
): UnavailableFileEntry => ({
path: "src/main.ts",
kind: "file",
sha256: null,
size: null,
language: "TypeScript",
classifications: ["source"],
content_state: "unreadable",
limitations: ["Permission denied"],
...overrides,
});
const symlinkEntry = (overrides: Partial<SymlinkEntry> = {}): SymlinkEntry => ({
path: "src/package.json",
kind: "symlink",
@@ -91,6 +109,16 @@ describe("historical source graph", () => {
expect(parseHistoricalSourceGraph(graph)).toEqual(graph);
expect(parseHistoricalSourceManifest(first)).toEqual(first);
expect(first.graph_sha256).toBe(computeHistoricalSourceGraphSha256(graph));
expect(() =>
createHistoricalSourceGraph({
...graphInput(),
entries: [
directoryEntry(),
{ ...fileEntry(), content_state: "unreadable" },
symlinkEntry(),
],
}),
).toThrow();
});
it("rejects stale roots and commits file, directory, and symlink semantics", () => {
@@ -161,12 +189,7 @@ describe("partial historical source graphs", () => {
it("preserves partial observations but rejects every partial complete graph", () => {
const partial = graphInput();
partial.inventory_state = "partial";
partial.entries[1] = fileEntry({
sha256: null,
size: null,
content_state: "unreadable",
limitations: ["Permission denied"],
});
partial.entries[1] = unavailableFileEntry();
partial.parse_failures = [
{ path: "src/main.ts", parser: "typescript", reason: "Malformed input" },
];
+19 -24
View File
@@ -38,37 +38,32 @@ const entryBaseShape = {
limitations: z.array(boundedTextSchema).max(100),
};
const sourceFileSchema = z
.strictObject({
...entryBaseShape,
kind: z.literal("file"),
sha256: digestSchema.nullable(),
const sourceFileBaseShape = {
...entryBaseShape,
kind: z.literal("file"),
language: z.string().min(1).max(100).nullable(),
};
const sourceFileSchema = z.union([
z.strictObject({
...sourceFileBaseShape,
sha256: digestSchema,
size: z.number().int().min(0).max(Number.MAX_SAFE_INTEGER),
content_state: z.literal("hashed"),
}),
z.strictObject({
...sourceFileBaseShape,
sha256: z.null(),
size: z.number().int().min(0).max(Number.MAX_SAFE_INTEGER).nullable(),
language: z.string().min(1).max(100).nullable(),
content_state: z.enum([
"hashed",
"redacted-secret",
"excluded",
"unreadable",
"too-large",
"unknown",
]),
})
.superRefine((file, context) => {
checkSortedUnique(file.classifications, "classifications", context);
if ((file.content_state === "hashed") !== (file.sha256 !== null))
context.addIssue({
code: "custom",
message: "Only hashed files have a content digest",
path: ["sha256"],
});
if (file.content_state === "hashed" && file.size === null)
context.addIssue({
code: "custom",
message: "Hashed files require a byte size",
path: ["size"],
});
});
}),
]);
const sourceDirectorySchema = z.strictObject({
...entryBaseShape,
@@ -108,7 +103,7 @@ const sourceSymlinkSchema = z.strictObject({
]),
});
const sourceEntrySchema = z.discriminatedUnion("kind", [
const sourceEntrySchema = z.union([
sourceFileSchema,
sourceDirectorySchema,
sourceSymlinkSchema,
+74 -37
View File
@@ -81,43 +81,58 @@ const findingSchema = z.object({
location: sourceLocationSchema,
});
const sourceMapSchema = z.object({
const originalSourceSchema = z.object({
source: z.string(),
artifact: webTextArtifactSchema.nullable(),
});
const originalModuleEdgeSchema = z.object({
from_source: z.string(),
kind: z.enum(["static_import", "dynamic_import", "require"]),
specifier: z.string(),
resolved_source: z.string().nullable(),
});
const sourceMapMappingSchema = z.object({
generated_line: z.number().int().min(1),
generated_column: z.number().int().min(0),
source: z.string(),
original_line: z.number().int().min(1),
original_column: z.number().int().min(0),
name: z.string().nullable(),
});
const sourceMapContextShape = {
script_key: z.string(),
declared_url: z.string(),
status: z.enum([
"included",
"fetch_failed",
"invalid",
"policy_filtered",
"truncated",
]),
artifact: webTextArtifactSchema.nullable(),
original_sources: z.array(
z.object({
source: z.string(),
artifact: webTextArtifactSchema.nullable(),
}),
),
original_module_edges: z.array(
z.object({
from_source: z.string(),
kind: z.enum(["static_import", "dynamic_import", "require"]),
specifier: z.string(),
resolved_source: z.string().nullable(),
}),
),
mappings: z.array(
z.object({
generated_line: z.number().int().min(1),
generated_column: z.number().int().min(0),
source: z.string(),
original_line: z.number().int().min(1),
original_column: z.number().int().min(0),
name: z.string().nullable(),
}),
),
limitation: z.string().nullable(),
});
};
const parsedSourceMapShape = {
artifact: webTextArtifactSchema,
original_sources: z.array(originalSourceSchema),
original_module_edges: z.array(originalModuleEdgeSchema),
mappings: z.array(sourceMapMappingSchema),
};
const sourceMapSchema = z.union([
z.object({
...sourceMapContextShape,
...parsedSourceMapShape,
status: z.literal("included"),
limitation: z.null(),
}),
z.object({
...sourceMapContextShape,
...parsedSourceMapShape,
status: z.literal("truncated"),
limitation: z.string(),
}),
z.object({
...sourceMapContextShape,
status: z.enum(["fetch_failed", "invalid", "policy_filtered", "truncated"]),
artifact: z.null(),
original_sources: z.tuple([]),
original_module_edges: z.tuple([]),
mappings: z.tuple([]),
limitation: z.string(),
}),
]);
const webTextArtifactSummarySchema = z.object({
uri: z.string().regex(/^rea:\/\/web-content\/sha256\/[a-f0-9]{64}$/u),
@@ -128,7 +143,7 @@ const webTextArtifactSummarySchema = z.object({
text_available: z.literal(true),
});
const sourceMapsSchema = z
export const webSourceMapsSchema = z
.object({
status: z.enum([
"not_requested",
@@ -153,8 +168,30 @@ const sourceMapsSchema = z
code: "custom",
message: "Source-map coverage counts are inconsistent",
});
const statuses = sourceMaps.items.map(({ status }) => status);
const included = statuses.filter((status) => status === "included").length;
const hasTruncated = statuses.includes("truncated");
const allowedStatuses =
sourceMaps.dropped > 0 || hasTruncated
? ["truncated"]
: sourceMaps.items.length === 0
? ["not_requested", "unavailable"]
: included === sourceMaps.items.length
? ["included"]
: included > 0
? ["partial"]
: ["unavailable"];
if (!allowedStatuses.includes(sourceMaps.status))
context.addIssue({
code: "custom",
message: "Source-map status contradicts retained coverage",
path: ["status"],
});
});
export type WebSourceMapItem = z.infer<typeof sourceMapSchema>;
export type WebSourceMaps = z.infer<typeof webSourceMapsSchema>;
/** Provider-neutral result of bounded JavaScript bundle reverse engineering. */
export const webBundleAnalysisSchema = z.object({
schema_version: z.literal(1),
@@ -200,7 +237,7 @@ export const webBundleAnalysisSchema = z.object({
location: sourceLocationSchema,
}),
),
source_maps: sourceMapsSchema,
source_maps: webSourceMapsSchema,
}),
inferences: z.array(
z.object({
+39 -71
View File
@@ -1,74 +1,26 @@
import { createHash } from "node:crypto";
import canonicalize from "canonicalize";
import { z } from "zod";
import type { WebPageInspection } from "./browserObservation.js";
import {
webPageInspectionSchema,
type WebPageInspection,
} from "./browserObservation.js";
import {
webMcpDiscoverySchema,
type WebMcpDiscovery,
} from "./webMcpDiscovery.js";
webCaptureDiffSchema,
type CompareWebCapturesInput,
type WebCaptureChange,
type WebCaptureDiff,
type WebCaptureDimension,
} from "./webCaptureDiffSchemas.js";
import type { WebMcpDiscovery } from "./webMcpDiscovery.js";
/** One normalized passive page snapshot accepted by capture comparison. */
export const captureSnapshotSchema = z.object({
inspection: webPageInspectionSchema,
webmcp: webMcpDiscoverySchema.nullable().default(null),
});
export {
captureSnapshotSchema,
compareWebCapturesInputSchema,
webCaptureDiffSchema,
} from "./webCaptureDiffSchemas.js";
export type { CompareWebCapturesInput, WebCaptureDiff };
/** Input for deterministic comparison of two normalized web captures. */
export const compareWebCapturesInputSchema = z.object({
before: captureSnapshotSchema,
after: captureSnapshotSchema,
max_changes: z.number().int().min(1).max(20_000).default(2_000),
});
export type CompareWebCapturesInput = z.infer<
typeof compareWebCapturesInputSchema
>;
const changeSchema = z.object({
identity: z.string(),
change: z.enum(["added", "removed", "modified"]),
});
const dimensionSchema = z.object({
status: z.enum(["changed", "unchanged", "unknown"]),
total_changes: z.number().int().min(0),
changes: z.array(changeSchema),
omitted_changes: z.number().int().min(0),
reason: z.string().nullable(),
});
const legacyUnknownDimension = {
status: "unknown" as const,
total_changes: 0,
changes: [],
omitted_changes: 0,
reason: "Dimension was not recorded by this version 1 capture diff.",
};
/** Completeness-aware changes across stable browser evidence dimensions. */
export const webCaptureDiffSchema = z.object({
schema_version: z.literal(1),
overall_status: z.enum(["changed", "unchanged", "unknown"]),
before_target: z.object({ target_id: z.string(), url: z.string() }),
after_target: z.object({ target_id: z.string(), url: z.string() }),
dimensions: z.object({
dom_structure: dimensionSchema,
scripts: dimensionSchema,
resources: dimensionSchema,
network: dimensionSchema,
metadata: dimensionSchema,
webmcp: dimensionSchema,
accessibility: dimensionSchema.default(legacyUnknownDimension),
storage: dimensionSchema.default(legacyUnknownDimension),
}),
limitations: z.array(z.string()),
});
export type WebCaptureDiff = z.infer<typeof webCaptureDiffSchema>;
type Dimension =
WebCaptureDiff["dimensions"][keyof WebCaptureDiff["dimensions"]];
type Change = Dimension["changes"][number];
type Dimension = WebCaptureDimension;
type Change = WebCaptureChange;
/** Compare normalized observations without treating incomplete absence as proof. */
export const compareWebCaptures = (
@@ -135,13 +87,29 @@ const compareDimension =
const all = compareIdentities(before, after);
const retained = all.slice(0, remaining.value);
remaining.value -= retained.length;
return {
status: all.length > 0 ? "changed" : complete ? "unchanged" : "unknown",
total_changes: all.length,
changes: retained,
omitted_changes: all.length - retained.length,
reason: all.length === 0 && !complete ? reason : null,
};
if (all.length > 0)
return {
status: "changed",
total_changes: all.length,
changes: retained,
omitted_changes: all.length - retained.length,
reason: null,
};
return complete
? {
status: "unchanged",
total_changes: 0,
changes: [],
omitted_changes: 0,
reason: null,
}
: {
status: "unknown",
total_changes: 0,
changes: [],
omitted_changes: 0,
reason,
};
};
const accessibilityDimension = (
+109
View File
@@ -0,0 +1,109 @@
import { z } from "zod";
import { webPageInspectionSchema } from "./browserObservation.js";
import { webMcpDiscoverySchema } from "./webMcpDiscovery.js";
/** One normalized passive page snapshot accepted by capture comparison. */
export const captureSnapshotSchema = z.object({
inspection: webPageInspectionSchema,
webmcp: webMcpDiscoverySchema.nullable().default(null),
});
/** Input for deterministic comparison of two normalized web captures. */
export const compareWebCapturesInputSchema = z.object({
before: captureSnapshotSchema,
after: captureSnapshotSchema,
max_changes: z.number().int().min(1).max(20_000).default(2_000),
});
const changeSchema = z.object({
identity: z.string(),
change: z.enum(["added", "removed", "modified"]),
});
const emptyDimensionShape = {
total_changes: z.literal(0),
changes: z.tuple([]),
omitted_changes: z.literal(0),
};
const dimensionSchema = z.union([
z
.object({
status: z.literal("changed"),
total_changes: z.number().int().min(1),
changes: z.array(changeSchema),
omitted_changes: z.number().int().min(0),
reason: z.null(),
})
.superRefine((dimension, context) => {
if (
dimension.changes.length + dimension.omitted_changes !==
dimension.total_changes
)
context.addIssue({
code: "custom",
message: "Retained and omitted changes must equal total changes",
path: ["total_changes"],
});
}),
z.object({
...emptyDimensionShape,
status: z.literal("unchanged"),
reason: z.null(),
}),
z.object({
...emptyDimensionShape,
status: z.literal("unknown"),
reason: z.string(),
}),
]);
const legacyUnknownDimension: z.input<typeof dimensionSchema> = {
status: "unknown",
total_changes: 0,
changes: [],
omitted_changes: 0,
reason: "Dimension was not recorded by this version 1 capture diff.",
};
/** Completeness-aware changes across stable browser evidence dimensions. */
export const webCaptureDiffSchema = z
.object({
schema_version: z.literal(1),
overall_status: z.enum(["changed", "unchanged", "unknown"]),
before_target: z.object({ target_id: z.string(), url: z.string() }),
after_target: z.object({ target_id: z.string(), url: z.string() }),
dimensions: z.object({
dom_structure: dimensionSchema,
scripts: dimensionSchema,
resources: dimensionSchema,
network: dimensionSchema,
metadata: dimensionSchema,
webmcp: dimensionSchema,
accessibility: dimensionSchema.default(legacyUnknownDimension),
storage: dimensionSchema.default(legacyUnknownDimension),
}),
limitations: z.array(z.string()),
})
.superRefine((comparison, context) => {
const statuses = Object.values(comparison.dimensions).map(
({ status }) => status,
);
const expected = statuses.includes("changed")
? "changed"
: statuses.includes("unknown")
? "unknown"
: "unchanged";
if (comparison.overall_status !== expected)
context.addIssue({
code: "custom",
message: "Overall status must summarize dimension statuses",
path: ["overall_status"],
});
});
export type CompareWebCapturesInput = z.infer<
typeof compareWebCapturesInputSchema
>;
export type WebCaptureDiff = z.infer<typeof webCaptureDiffSchema>;
export type WebCaptureDimension =
WebCaptureDiff["dimensions"][keyof WebCaptureDiff["dimensions"]];
export type WebCaptureChange = WebCaptureDimension["changes"][number];
+1 -1
View File
@@ -30,7 +30,7 @@ interface GeneratedPayload {
}
const GENERATED_PAYLOAD_JSON =
'{"auxiliaryProviders":[{"capabilities":[{"available":true,"effects":{"changesPermissions":false,"launchesProcess":true,"mayAccessNetwork":false,"mayShowUi":false,"mayWriteFilesystem":false,"mutatesArtifact":false,"requiresRoot":false},"exhaustive":false,"inputContractVersion":1,"limitations":["DMG child inventory is macOS-only and requires per-call approval plus operator policy; PKG remains root-hash-only.","ASAR files discovered in filesystem-backed inventories are expanded without bulk extraction; other nested containers remain recorded only."],"limits":{"maxPayloadBytes":4194304,"maxResults":500,"timeoutMs":120000},"operation":"inventory_artifact","outputContractVersion":1,"pagination":"offset","provider":{"id":"rea-artifact-graph","name":"REA safe artifact graph provider","version":"1"},"reason":null},{"available":true,"effects":{"changesPermissions":false,"launchesProcess":true,"mayAccessNetwork":false,"mayShowUi":false,"mayWriteFilesystem":false,"mutatesArtifact":false,"requiresRoot":false},"exhaustive":false,"inputContractVersion":1,"limitations":["DMG child inventory is macOS-only and requires per-call approval plus operator policy; PKG remains root-hash-only.","ASAR files discovered in filesystem-backed inventories are expanded without bulk extraction; other nested containers remain recorded only."],"limits":{"maxPayloadBytes":4194304,"maxResults":500,"timeoutMs":120000},"operation":"inspect_artifact","outputContractVersion":1,"pagination":"none","provider":{"id":"rea-artifact-graph","name":"REA safe artifact graph provider","version":"1"},"reason":null},{"available":true,"effects":{"changesPermissions":false,"launchesProcess":true,"mayAccessNetwork":false,"mayShowUi":false,"mayWriteFilesystem":true,"mutatesArtifact":false,"requiresRoot":false},"exhaustive":false,"inputContractVersion":1,"limitations":["DMG child inventory is macOS-only and requires per-call approval plus operator policy; PKG remains root-hash-only.","ASAR files discovered in filesystem-backed inventories are expanded without bulk extraction; other nested containers remain recorded only."],"limits":{"maxPayloadBytes":4194304,"maxResults":500,"timeoutMs":120000},"operation":"extract_artifact","outputContractVersion":1,"pagination":"offset","provider":{"id":"rea-artifact-graph","name":"REA safe artifact graph provider","version":"1"},"reason":null}],"identity":{"id":"rea-artifact-graph","name":"REA safe artifact graph provider","version":"1"}},{"capabilities":[{"available":true,"effects":{"changesPermissions":false,"launchesProcess":true,"mayAccessNetwork":false,"mayShowUi":false,"mayWriteFilesystem":false,"mutatesArtifact":false,"requiresRoot":false},"exhaustive":false,"inputContractVersion":1,"limitations":["Availability and textual formats depend on the installed macOS/Xcode toolchain."],"limits":{"maxPayloadBytes":4194304,"maxResults":null,"timeoutMs":30000},"operation":"inspect_macho","outputContractVersion":1,"pagination":"none","provider":{"id":"native-macos","name":"macOS native inspection utilities","version":null},"reason":null},{"available":true,"effects":{"changesPermissions":false,"launchesProcess":true,"mayAccessNetwork":false,"mayShowUi":false,"mayWriteFilesystem":false,"mutatesArtifact":false,"requiresRoot":false},"exhaustive":true,"inputContractVersion":1,"limitations":["Availability and textual formats depend on the installed macOS/Xcode toolchain."],"limits":{"maxPayloadBytes":4194304,"maxResults":null,"timeoutMs":30000},"operation":"inspect_signature","outputContractVersion":1,"pagination":"none","provider":{"id":"native-macos","name":"macOS native inspection utilities","version":null},"reason":null},{"available":true,"effects":{"changesPermissions":false,"launchesProcess":true,"mayAccessNetwork":false,"mayShowUi":false,"mayWriteFilesystem":false,"mutatesArtifact":false,"requiresRoot":false},"exhaustive":true,"inputContractVersion":1,"limitations":["Availability and textual formats depend on the installed macOS/Xcode toolchain."],"limits":{"maxPayloadBytes":4194304,"maxResults":null,"timeoutMs":30000},"operation":"inspect_plist","outputContractVersion":1,"pagination":"none","provider":{"id":"native-macos","name":"macOS native inspection utilities","version":null},"reason":null},{"available":true,"effects":{"changesPermissions":false,"launchesProcess":true,"mayAccessNetwork":false,"mayShowUi":false,"mayWriteFilesystem":false,"mutatesArtifact":false,"requiresRoot":false},"exhaustive":true,"inputContractVersion":1,"limitations":["Availability and textual formats depend on the installed macOS/Xcode toolchain."],"limits":{"maxPayloadBytes":4194304,"maxResults":null,"timeoutMs":30000},"operation":"list_architectures","outputContractVersion":1,"pagination":"none","provider":{"id":"native-macos","name":"macOS native inspection utilities","version":null},"reason":null},{"available":true,"effects":{"changesPermissions":false,"launchesProcess":true,"mayAccessNetwork":false,"mayShowUi":false,"mayWriteFilesystem":false,"muLine truncated
'{"auxiliaryProviders":[{"capabilities":[{"available":true,"effects":{"changesPermissions":false,"launchesProcess":true,"mayAccessNetwork":false,"mayShowUi":false,"mayWriteFilesystem":false,"mutatesArtifact":false,"requiresRoot":false},"exhaustive":false,"inputContractVersion":1,"limitations":["DMG child inventory is macOS-only and requires per-call approval plus operator policy; PKG remains root-hash-only.","ASAR files discovered in filesystem-backed inventories are expanded without bulk extraction; other nested containers remain recorded only."],"limits":{"maxPayloadBytes":4194304,"maxResults":500,"timeoutMs":120000},"operation":"inventory_artifact","outputContractVersion":1,"pagination":"offset","provider":{"id":"rea-artifact-graph","name":"REA safe artifact graph provider","version":"1"},"reason":null},{"available":true,"effects":{"changesPermissions":false,"launchesProcess":true,"mayAccessNetwork":false,"mayShowUi":false,"mayWriteFilesystem":false,"mutatesArtifact":false,"requiresRoot":false},"exhaustive":false,"inputContractVersion":1,"limitations":["DMG child inventory is macOS-only and requires per-call approval plus operator policy; PKG remains root-hash-only.","ASAR files discovered in filesystem-backed inventories are expanded without bulk extraction; other nested containers remain recorded only."],"limits":{"maxPayloadBytes":4194304,"maxResults":500,"timeoutMs":120000},"operation":"inspect_artifact","outputContractVersion":1,"pagination":"none","provider":{"id":"rea-artifact-graph","name":"REA safe artifact graph provider","version":"1"},"reason":null},{"available":true,"effects":{"changesPermissions":false,"launchesProcess":true,"mayAccessNetwork":false,"mayShowUi":false,"mayWriteFilesystem":true,"mutatesArtifact":false,"requiresRoot":false},"exhaustive":false,"inputContractVersion":1,"limitations":["DMG child inventory is macOS-only and requires per-call approval plus operator policy; PKG remains root-hash-only.","ASAR files discovered in filesystem-backed inventories are expanded without bulk extraction; other nested containers remain recorded only."],"limits":{"maxPayloadBytes":4194304,"maxResults":500,"timeoutMs":120000},"operation":"extract_artifact","outputContractVersion":1,"pagination":"offset","provider":{"id":"rea-artifact-graph","name":"REA safe artifact graph provider","version":"1"},"reason":null}],"identity":{"id":"rea-artifact-graph","name":"REA safe artifact graph provider","version":"1"}},{"capabilities":[{"available":true,"effects":{"changesPermissions":false,"launchesProcess":true,"mayAccessNetwork":false,"mayShowUi":false,"mayWriteFilesystem":false,"mutatesArtifact":false,"requiresRoot":false},"exhaustive":false,"inputContractVersion":1,"limitations":["Availability and textual formats depend on the installed macOS/Xcode toolchain."],"limits":{"maxPayloadBytes":4194304,"maxResults":null,"timeoutMs":30000},"operation":"inspect_macho","outputContractVersion":1,"pagination":"none","provider":{"id":"native-macos","name":"macOS native inspection utilities","version":null},"reason":null},{"available":true,"effects":{"changesPermissions":false,"launchesProcess":true,"mayAccessNetwork":false,"mayShowUi":false,"mayWriteFilesystem":false,"mutatesArtifact":false,"requiresRoot":false},"exhaustive":true,"inputContractVersion":1,"limitations":["Availability and textual formats depend on the installed macOS/Xcode toolchain."],"limits":{"maxPayloadBytes":4194304,"maxResults":null,"timeoutMs":30000},"operation":"inspect_signature","outputContractVersion":1,"pagination":"none","provider":{"id":"native-macos","name":"macOS native inspection utilities","version":null},"reason":null},{"available":true,"effects":{"changesPermissions":false,"launchesProcess":true,"mayAccessNetwork":false,"mayShowUi":false,"mayWriteFilesystem":false,"mutatesArtifact":false,"requiresRoot":false},"exhaustive":true,"inputContractVersion":1,"limitations":["Availability and textual formats depend on the installed macOS/Xcode toolchain."],"limits":{"maxPayloadBytes":4194304,"maxResults":null,"timeoutMs":30000},"operation":"inspect_plist","outputContractVersion":1,"pagination":"none","provider":{"id":"native-macos","name":"macOS native inspection utilities","version":null},"reason":null},{"available":true,"effects":{"changesPermissions":false,"launchesProcess":true,"mayAccessNetwork":false,"mayShowUi":false,"mayWriteFilesystem":false,"mutatesArtifact":false,"requiresRoot":false},"exhaustive":true,"inputContractVersion":1,"limitations":["Availability and textual formats depend on the installed macOS/Xcode toolchain."],"limits":{"maxPayloadBytes":4194304,"maxResults":null,"timeoutMs":30000},"operation":"list_architectures","outputContractVersion":1,"pagination":"none","provider":{"id":"native-macos","name":"macOS native inspection utilities","version":null},"reason":null},{"available":true,"effects":{"changesPermissions":false,"launchesProcess":true,"mayAccessNetwork":false,"mayShowUi":false,"mayWriteFilesystem":false,"muLine truncated
const generatedPayload: unknown = JSON.parse(GENERATED_PAYLOAD_JSON);
if (!isGeneratedPayload(generatedPayload))
throw new TypeError("Generated MCP catalog payload is invalid");
+11 -6
View File
@@ -7,7 +7,7 @@ import { GHIDRA_INVENTORY_OPERATIONS } from "./GhidraInventoryValues.js";
import { GhidraProvider } from "./GhidraProvider.js";
describe("Ghidra provider capabilities", () => {
it("publishes only admitted immutable read-only operations", () => {
it("publishes only admitted read-only operations and resists caller mutation", () => {
const config = parseConfig({});
expect(config.ok).toBe(true);
if (!config.ok) return;
@@ -17,10 +17,10 @@ describe("Ghidra provider capabilities", () => {
probeJava: () => undefined,
});
const capabilities = provider.capabilities();
const published = structuredClone(capabilities);
expect(capabilities.map(({ operation }) => operation).sort()).toEqual(
[...GHIDRA_INVENTORY_OPERATIONS, ...GHIDRA_FUNCTION_OPERATIONS].sort(),
);
expect(Object.isFrozen(capabilities)).toBe(true);
for (const descriptor of capabilities) {
expect(descriptor).toMatchObject({
available: true,
@@ -33,10 +33,15 @@ describe("Ghidra provider capabilities", () => {
requiresRoot: false,
},
});
expect(Object.isFrozen(descriptor)).toBe(true);
expect(Object.isFrozen(descriptor.effects)).toBe(true);
expect(Object.isFrozen(descriptor.limits)).toBe(true);
expect(Object.isFrozen(descriptor.limitations)).toBe(true);
}
const first = capabilities[0];
if (first === undefined) throw new Error("Ghidra capabilities are empty");
Reflect.set(capabilities, 0, { ...first, available: false });
Reflect.set(first, "available", false);
Reflect.set(first.effects, "mutatesArtifact", true);
Reflect.set(first.limits, "maxResults", 0);
Reflect.set(first.limitations, 0, "forged limitation");
expect(provider.capabilities()).toEqual(published);
});
});
+11 -6
View File
@@ -5,17 +5,17 @@ import { silentLogger } from "../logger.js";
import { HopperProvider } from "./HopperProvider.js";
describe("Hopper provider capabilities", () => {
it("publishes deterministic immutable capability descriptors", () => {
it("publishes deterministic descriptors and resists caller mutation", () => {
const config = parseConfig({});
expect(config.ok).toBe(true);
if (!config.ok) return;
const provider = new HopperProvider(config.value, silentLogger);
const capabilities = provider.capabilities();
const published = structuredClone(capabilities);
expect(capabilities).toHaveLength(37);
expect(new Set(capabilities.map(({ operation }) => operation)).size).toBe(
capabilities.length,
);
expect(Object.isFrozen(capabilities)).toBe(true);
for (const descriptor of capabilities) {
expect(descriptor.provider).toEqual(provider.identity());
expect(descriptor).toMatchObject({
@@ -24,10 +24,6 @@ describe("Hopper provider capabilities", () => {
available: true,
reason: null,
});
expect(Object.isFrozen(descriptor)).toBe(true);
expect(Object.isFrozen(descriptor.effects)).toBe(true);
expect(Object.isFrozen(descriptor.limits)).toBe(true);
expect(Object.isFrozen(descriptor.limitations)).toBe(true);
}
expect(
capabilities.find(({ operation }) => operation === "list_procedures"),
@@ -37,5 +33,14 @@ describe("Hopper provider capabilities", () => {
).toMatchObject({
effects: { mutatesArtifact: true, mayWriteFilesystem: true },
});
const first = capabilities[0];
if (first === undefined) throw new Error("Hopper capabilities are empty");
Reflect.set(capabilities, 0, { ...first, available: false });
Reflect.set(first, "available", false);
Reflect.set(first.effects, "mutatesArtifact", true);
Reflect.set(first.limits, "maxResults", 0);
Reflect.set(first.limitations, 0, "forged limitation");
expect(provider.capabilities()).toEqual(published);
});
});
@@ -7,7 +7,10 @@ import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import { analyzeJavaScriptApplication } from "../../../src/application/JavaScriptApplicationService.js";
import { compareApplicationVersionsEvidence } from "../../../src/application/JavaScriptApplicationWorkflowService.js";
import { createEvidence } from "../../../src/domain/evidence.js";
import { applicationVersionComparisonResultSchema } from "../../../src/domain/javascriptApplicationVersionComparisonSchemas.js";
import {
applicationVersionComparisonResultSchema,
type ApplicationVersionComparisonResult,
} from "../../../src/domain/javascriptApplicationVersionComparisonSchemas.js";
import { compareJavaScriptApplicationVersions } from "../../../src/domain/javascriptApplicationVersionComparison.js";
import { createJavaScriptApplicationGraph } from "../../../src/domain/javascriptApplicationGraph.js";
import { traceApplicationFeature } from "../../../src/domain/javascriptFeatureTrace.js";
@@ -188,6 +191,7 @@ describe("JavaScript application comparison workflows", () => {
expect(result.limitations.join(" ")).toMatch(
/module ordinals|fuzzy pairing/u,
);
expectComparisonItemAlgebra(result);
});
it("pairs source-map originals without promoting the match to exact", async () => {
@@ -291,3 +295,42 @@ const analyzeFixture = async (root: string, sourceMaps = false) => {
if (!result.ok) throw result.error;
return result.value;
};
const expectComparisonItemAlgebra = (
result: ApplicationVersionComparisonResult,
): void => {
const matched = result.items.find(({ match }) => match.status === "matched");
expect(matched).toBeDefined();
if (matched === undefined) return;
expect(
applicationVersionComparisonResultSchema.safeParse({
...result,
items: [{ ...matched, right_node_id: null }],
}).success,
).toBe(false);
expect(
applicationVersionComparisonResultSchema.safeParse({
...result,
items: [
{
...matched,
match: {
...matched.match,
basis: "none",
confidence: "unknown",
},
},
],
}).success,
).toBe(false);
expect(
applicationVersionComparisonResultSchema.safeParse({
...result,
coverage: {
...result.coverage,
status: "complete-within-inputs",
omitted_comparison_items: 1,
},
}).success,
).toBe(false);
};
@@ -13,6 +13,9 @@ import {
} from "../../fixtures/fakeCdpBrowser.js";
const browsers: FakeCdpBrowser[] = [];
const expectInvalidDiff = (input: unknown): void => {
expect(webCaptureDiffSchema.safeParse(input).success).toBe(false);
};
afterEach(async () => {
await Promise.all(browsers.splice(0).map(async (browser) => browser.close()));
@@ -58,6 +61,21 @@ describe("web capture diff", () => {
status: "unknown",
total_changes: 0,
});
expectInvalidDiff({ ...result, overall_status: "unchanged" });
expectInvalidDiff({
...result,
dimensions: {
...result.dimensions,
webmcp: { ...result.dimensions.webmcp, reason: null },
},
});
expectInvalidDiff({
...result,
dimensions: {
...result.dimensions,
scripts: { ...result.dimensions.scripts, total_changes: 2 },
},
});
const {
accessibility: _accessibility,
storage: _storage,
@@ -1,7 +1,11 @@
import { describe, expect, it } from "vitest";
import { fetchWebSourceMaps } from "../../../src/browser/WebSourceMapFetcher.js";
import { analyzeWebBundleInputSchema } from "../../../src/domain/webBundleAnalysis.js";
import {
analyzeWebBundleInputSchema,
webSourceMapsSchema,
type WebSourceMaps,
} from "../../../src/domain/webBundleAnalysis.js";
const origin = "https://app.example.test";
const request = {
@@ -10,6 +14,18 @@ const request = {
fetchUrl: `${origin}/assets/app.js.map?token=secret`,
};
const expectInvalidSourceMaps = (value: unknown): void => {
expect(webSourceMapsSchema.safeParse(value).success).toBe(false);
};
const expectInvalidIncludedSourceMaps = (result: WebSourceMaps): void => {
expectInvalidSourceMaps({
...result,
items: [{ ...result.items[0], artifact: null }],
});
expectInvalidSourceMaps({ ...result, status: "unavailable" });
};
describe("web source-map fetching and validation", () => {
it("fetches without credentials and derives mappings and original modules", async () => {
const calls: Array<{ url: string; init: RequestInit | undefined }> = [];
@@ -77,6 +93,7 @@ describe("web source-map fetching and validation", () => {
},
],
});
expectInvalidIncludedSourceMaps(result);
});
it("validates sectioned maps and reports bounded mapping truncation", async () => {
@@ -136,6 +153,10 @@ describe("web source-map fetching and validation", () => {
status: "unavailable",
items: [{ status: "policy_filtered" }],
});
expectInvalidSourceMaps({
...result,
items: [{ ...result.items[0], limitation: null }],
});
});
it("distinguishes invalid JSON from declared or streamed byte truncation", async () => {
@@ -82,6 +82,9 @@ describe("persistent cross-version investigation workspace", () => {
const run = completed.value.workspace.runs[0];
if (run === undefined || run.comparison_evidence_id === null)
throw new Error("missing completed run");
expect(
investigationRunSchema.safeParse({ ...run, status: "running" }).success,
).toBe(false);
const partialRun = investigationRunSchema.parse({
...run,
status: "running",
@@ -1,6 +1,6 @@
import { Client, InMemoryTransport } from "@modelcontextprotocol/client";
import type { CallToolResult } from "@modelcontextprotocol/server";
import { describe, expect, it, vi } from "vitest";
import { describe, expect, it } from "vitest";
import { z } from "zod";
import { PermissionAuthority } from "../../../src/application/PermissionAuthority.js";
@@ -26,8 +26,16 @@ describe("evidence bundle release", () => {
it("notifies snapshot observers and invalidates the retained URI", async () => {
const session = createSession();
const server = createServer(session, session);
const resourceUpdated = vi.spyOn(server.server, "sendResourceUpdated");
const client = new Client({ name: "release-test", version: "1" });
const bundleUpdated = new Promise<string>((resolve) => {
client.setNotificationHandler(
"notifications/resources/updated",
(notice) => {
if (notice.params.uri.startsWith("rea://evidence-bundle/"))
resolve(notice.params.uri);
},
);
});
let updates = 0;
session.onAnalysisSnapshotChanged(() => {
updates += 1;
@@ -64,9 +72,7 @@ describe("evidence bundle release", () => {
released: true,
});
expect(updates).toBe(1);
expect(resourceUpdated).toHaveBeenCalledWith({
uri: snapshot.bundle_uri,
});
await expect(bundleUpdated).resolves.toBe(snapshot.bundle_uri);
await expect(
client.readResource({ uri: snapshot.bundle_uri }),
).rejects.toThrow(/not found/iu);
@@ -76,7 +76,7 @@ describe("provider process runtime and wait primitives", () => {
});
});
it("clears the startup timer and external cancellation listener", async () => {
it("classifies external cancellation during startup", async () => {
vi.useFakeTimers();
const controller = new AbortController();
const deadline = new ProviderStartupDeadline(1_000, controller.signal);
@@ -120,7 +120,7 @@ describe("provider process runtime and wait primitives", () => {
deadline.dispose();
});
it("releases pending request timers and abort listeners on every outcome", async () => {
it("settles pending requests once on cancellation, timeout, and failure", async () => {
vi.useFakeTimers();
const operations = new PendingOperations<number, string>();
const controller = new AbortController();
@@ -134,6 +134,7 @@ describe("provider process runtime and wait primitives", () => {
controller.abort();
await expect(cancelled).resolves.toBe("cancelled");
expect(getEventListeners(controller.signal, "abort")).toHaveLength(0);
expect(operations.settle(1, "late reply")).toBe(false);
const timedOut = operations.wait(2, {
timeoutMs: 50,
@@ -142,6 +143,7 @@ describe("provider process runtime and wait primitives", () => {
});
await vi.advanceTimersByTimeAsync(50);
await expect(timedOut).resolves.toBe("timeout");
expect(operations.settle(2, "late reply")).toBe(false);
const failed = operations.wait(3, {
timeoutMs: 1_000,
@@ -150,6 +152,7 @@ describe("provider process runtime and wait primitives", () => {
});
operations.failAll((key) => `failed:${String(key)}`);
await expect(failed).resolves.toBe("failed:3");
expect(operations.settle(3, "late reply")).toBe(false);
expect(operations.size).toBe(0);
expect(vi.getTimerCount()).toBe(0);
});
@@ -4,29 +4,17 @@ import { fileURLToPath } from "node:url";
import { beforeAll, describe, expect, it } from "vitest";
let bridgeSource = "";
let queueSource = "";
beforeAll(async () => {
[bridgeSource, queueSource] = await Promise.all([
readFile(
fileURLToPath(
new URL(
"../../../../bridge/ghidra/ReaGhidraBridge.java",
import.meta.url,
),
bridgeSource = await readFile(
fileURLToPath(
new URL(
"../../../../bridge/ghidra/ReaGhidraBridge.java",
import.meta.url,
),
"utf8",
),
readFile(
fileURLToPath(
new URL(
"../../../../src/ghidra/GhidraRequestQueue.ts",
import.meta.url,
),
),
"utf8",
),
]);
"utf8",
);
});
describe("Ghidra bridge truthfulness", () => {
@@ -76,15 +64,6 @@ describe("Ghidra bridge truthfulness", () => {
);
});
it("serializes a bounded request queue before crossing the socket", () => {
expect(queueSource).toContain("class GhidraRequestQueue");
expect(queueSource).toContain("if (this.#size >= this.maximum)");
expect(queueSource).toContain("if (this.#active) return");
expect(queueSource).toContain(
"remaining = entry.deadline - performance.now()",
);
});
it("binds the Windows fallback to authenticated IPv4 loopback", () => {
expect(bridgeSource).toContain(
'descriptor.transport.equals("unix-socket")',
@@ -379,7 +379,7 @@ describe("GhidraClient established requests", () => {
});
describe("GhidraClient cleanup and diagnostics", () => {
it("makes double close idempotent and detaches process listeners", async () => {
it("makes double close idempotent and releases its process runtime", async () => {
const launcher = new FixtureLauncher();
const client = clientFor(launcher);
await expect(client.start()).resolves.toMatchObject({ ok: true });
@@ -79,34 +79,6 @@ describe("Hopper bridge truthfulness", () => {
);
});
it("bounds search and makes regex an explicitly constrained opt-in", () => {
expect(bridgeSource).toContain('params.get("mode", "literal")');
expect(bridgeSource).toContain("pattern.casefold()");
expect(bridgeSource).toContain("_validate_regex_node(parsed)");
expect(bridgeSource).toContain(
"Nested regex repetitions are not supported",
);
expect(bridgeSource).toContain("MAX_SEARCH_PATTERN_LENGTH = 256");
expect(bridgeSource).toContain("MAX_SEARCH_VALUE_LENGTH = 4096");
expect(bridgeSource).toContain("MAX_REGEX_BACKTRACKING_PATHS = 10000");
expect(bridgeSource).toContain("MAX_REGEX_CANDIDATE_LENGTH = 4096");
expect(bridgeSource).toContain("MAX_REGEX_SEARCH_WORK_UNITS = 1000000");
expect(bridgeSource).toContain("_checked_regex_paths(");
expect(bridgeSource).toContain("_bounded_regex_matcher(");
expect(bridgeSource).toContain('"value_truncated"');
expect(bridgeSource).toContain("page_end = offset + limit");
expect(bridgeSource).toContain("if offset <= total < page_end:");
expect(bridgeSource).not.toContain(
"matching = [item for item in _search_inventory(document, kind)",
);
});
it("caches sorted search inventories and invalidates them after renames", () => {
expect(bridgeSource).toContain("_search_inventory_cache");
expect(bridgeSource).toContain("tuple(sorted(values.items()");
expect(bridgeSource).toContain("_invalidate_search_inventory(document)");
});
it("stops background analysis and closes the session-owned document", () => {
expect(bridgeSource).toContain("def _session_document():");
expect(bridgeSource).toContain("os.path.realpath(REA_TARGET_PATH)");
@@ -128,6 +100,29 @@ describe("Hopper bridge truthfulness", () => {
);
});
it("invalidates cached search inventories in both rename paths", () => {
const singleRenameStart = bridgeSource.indexOf(
'if method == "set_address_name":',
);
const bulkRenameStart = bridgeSource.indexOf(
'if method == "set_addresses_names":',
singleRenameStart,
);
const nextMethodStart = bridgeSource.indexOf(
'if method in ("set_comment", "set_inline_comment"):',
bulkRenameStart,
);
expect(singleRenameStart).toBeGreaterThan(-1);
expect(bulkRenameStart).toBeGreaterThan(singleRenameStart);
expect(nextMethodStart).toBeGreaterThan(bulkRenameStart);
expect(bridgeSource.slice(singleRenameStart, bulkRenameStart)).toContain(
"_invalidate_search_inventory(document)",
);
expect(bridgeSource.slice(bulkRenameStart, nextMethodStart)).toContain(
"_invalidate_search_inventory(document)",
);
});
it("returns deterministic addresses for direct procedure relationships", () => {
expect(bridgeSource).toContain(
"_hex(item.getEntryPoint()) for item in procedure.getAllCallerProcedures()",
@@ -270,7 +270,7 @@ describe("HopperClient cleanup", () => {
}
});
it("makes sequential double-close leave no runtime or process listeners", async () => {
it("makes sequential double-close release its process runtime", async () => {
const launcher = new FixtureLauncher();
const client = new HopperClient({ launcher, startupTimeoutMs: 1_000 });
clients.push(client);
@@ -7,6 +7,7 @@ import {
} from "../../../src/domain/conformancePackage.js";
import { createEvidence } from "../../../src/domain/evidence.js";
import {
packageReplayResultSchema,
replayConformancePackage,
type ScenarioReplayResult,
} from "../../../src/domain/conformanceReplay.js";
@@ -97,6 +98,9 @@ describe("conformance CI replay", () => {
expect(result.passed).toBe(1);
expect(result.failed).toBe(0);
expect(result.errored).toBe(0);
expect(
packageReplayResultSchema.safeParse({ ...result, passed: 0 }).success,
).toBe(false);
});
it("replays a package with failing scenarios", async () => {
@@ -115,6 +119,12 @@ describe("conformance CI replay", () => {
{},
passingRunner,
);
expect(
packageReplayResultSchema.safeParse({
...result,
drift_detected: true,
}).success,
).toBe(false);
expect(result.drift_detected).toBe(false);
});
@@ -6,6 +6,7 @@ import {
evaluateTrustGate,
isSemanticDimension,
isVolatileDimension,
trustGateResultSchema,
} from "../../../src/domain/conformanceTrustGate.js";
const contract: VerifierContract = {
@@ -18,6 +19,21 @@ const contract: VerifierContract = {
timing_tolerance_ms: 100,
};
const expectDivergenceRejected = (
result: ReturnType<typeof evaluateTrustGate>,
): void => {
expect(
trustGateResultSchema.safeParse({
...result,
first_divergence: {
dimension: "exit_code",
evidence_id: "invented",
message: "invented",
},
}).success,
).toBe(false);
};
describe("conformance trust gates", () => {
it("classifies volatile dimensions correctly", () => {
expect(isVolatileDimension("timing")).toBe(true);
@@ -143,6 +159,7 @@ describe("conformance trust gates", () => {
{ exit_code: 0, stdout: "hello" },
);
expect(result.verdict).toBe("pass");
expectDivergenceRejected(result);
});
it("fails when a required dimension mismatches", () => {