mirror of
https://github.com/morluto/rea.git
synced 2026-10-26 17:35:11 +00:00
fix(process): propagate comparison contradictions (#6298)
Co-authored-by: gongfpp <22683417+gongfpp@users.noreply.github.com>
This commit is contained in:
1 parent
510838bd70
commit
77c72b96aa
4 files changed
+202
-9
No files matched your search
@@ -18,8 +18,6 @@ import {
|
||||
import { functionComparisonResultSchema } from "./functionComparisonSchemas.js";
|
||||
import {
|
||||
comparisonStatusSchema,
|
||||
deriveProcessComparisonStatus,
|
||||
PROCESS_COMPARISON_DIMENSIONS,
|
||||
processCaptureComparisonSchema,
|
||||
} from "./process/processComparison.js";
|
||||
import { prefixedDigestSchema } from "./../domain/digests.js";
|
||||
@@ -209,13 +207,6 @@ const parseResult = (
|
||||
const result = processCaptureComparisonSchema.parse(
|
||||
evidence.normalized_result,
|
||||
);
|
||||
const expected = deriveProcessComparisonStatus(
|
||||
PROCESS_COMPARISON_DIMENSIONS.map((dimension) => result[dimension]),
|
||||
);
|
||||
if (result.status !== expected)
|
||||
throw new TypeError(
|
||||
"Process comparison status contradicts its dimensions",
|
||||
);
|
||||
return { operation, evidence, result };
|
||||
}
|
||||
if (operation === "compare_functions") {
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
import { expect, it } from "vitest";
|
||||
|
||||
import {
|
||||
deriveProcessComparisonStatus,
|
||||
PROCESS_COMPARISON_DIMENSIONS,
|
||||
processCaptureComparisonSchema,
|
||||
} from "./processComparison.js";
|
||||
|
||||
import {
|
||||
accountFullyObservedProcessCapture,
|
||||
compareUnverifiedProcessCaptures as compareProcessCaptures,
|
||||
@@ -17,6 +23,58 @@ const compareFullyObserved = (
|
||||
options,
|
||||
);
|
||||
|
||||
it.each(PROCESS_COMPARISON_DIMENSIONS)(
|
||||
"requires a contradiction aggregate for a contradictory %s dimension",
|
||||
(dimension) => {
|
||||
const baseline = compareFullyObserved(emptyCapture(), emptyCapture());
|
||||
const result = { ...baseline, [dimension]: "contradiction" as const };
|
||||
expect(
|
||||
deriveProcessComparisonStatus(
|
||||
PROCESS_COMPARISON_DIMENSIONS.map((name) => result[name]),
|
||||
),
|
||||
).toBe("contradiction");
|
||||
expect(processCaptureComparisonSchema.safeParse(result).success).toBe(
|
||||
false,
|
||||
);
|
||||
expect(
|
||||
processCaptureComparisonSchema.parse({
|
||||
...result,
|
||||
status: "contradiction",
|
||||
}),
|
||||
).toMatchObject({ status: "contradiction", [dimension]: "contradiction" });
|
||||
},
|
||||
);
|
||||
|
||||
it.each([
|
||||
["unchanged", "contradiction"],
|
||||
["added", "contradiction"],
|
||||
["removed", "contradiction"],
|
||||
["changed", "contradiction"],
|
||||
["unknown", "contradiction"],
|
||||
["contradiction", "contradiction"],
|
||||
["truncated", "truncated"],
|
||||
] as const)(
|
||||
"combines contradiction with %s as %s independently of dimension order",
|
||||
(other, expected) => {
|
||||
for (const [terminal, interaction] of [
|
||||
["contradiction", other],
|
||||
[other, "contradiction"],
|
||||
] as const) {
|
||||
expect(deriveProcessComparisonStatus([terminal, interaction])).toBe(
|
||||
expected,
|
||||
);
|
||||
expect(
|
||||
processCaptureComparisonSchema.parse({
|
||||
...compareFullyObserved(emptyCapture(), emptyCapture()),
|
||||
terminal,
|
||||
interaction,
|
||||
status: expected,
|
||||
}),
|
||||
).toMatchObject({ status: expected });
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
it("compares normalized terminal facts while retaining different source text for audit", () => {
|
||||
const capture = emptyCapture();
|
||||
const frame = { sequence: 0, at_ms: 0, data: "listen:<port> score=20" };
|
||||
|
||||
@@ -49,6 +49,7 @@ export const deriveProcessComparisonStatus = (
|
||||
dimensions: readonly ComparisonStatus[],
|
||||
): ComparisonStatus => {
|
||||
if (dimensions.includes("truncated")) return "truncated";
|
||||
if (dimensions.includes("contradiction")) return "contradiction";
|
||||
if (
|
||||
dimensions.some((status) =>
|
||||
["changed", "added", "removed"].includes(status),
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
import { writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
|
||||
import { expect, it, onTestFinished } from "vitest";
|
||||
|
||||
import { createEvidence, parseEvidence } from "../../../src/domain/evidence.js";
|
||||
import { createEvidenceBundle } from "../../../src/domain/evidenceBundle.js";
|
||||
import { jsonValueSchema } from "../../../src/domain/jsonValue.js";
|
||||
import { EMPTY_PROCESS_CAPTURE_EXAMPLE } from "../../../src/domain/process/processCaptureExample.js";
|
||||
import { parseProcessCapture } from "../../../src/domain/process/processCaptureParsing.js";
|
||||
import { compareProcessCaptures } from "../../../src/domain/process/processComparison.js";
|
||||
import { PROCESS_PROVIDER } from "../../../src/domain/process/processEvidenceProvider.js";
|
||||
import { reconstructionVerificationResultSchema } from "../../../src/domain/reconstructionVerificationSchemas.js";
|
||||
import { createApplicationMcpHarness } from "../../fixtures/applicationMcpHarness.js";
|
||||
import { createTestBinarySession } from "../../fixtures/binarySession.js";
|
||||
import { parseMcpToolError } from "../../fixtures/mcpToolError.js";
|
||||
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
||||
|
||||
it("rejects an inconsistent imported process comparison while retaining its Evidence", async () => {
|
||||
const dimension = "terminal" as const;
|
||||
const source = (side: string) =>
|
||||
createEvidence(undefined, PROCESS_PROVIDER, {
|
||||
predicateType: "rea.process-capture",
|
||||
operation: "capture_process_scenario",
|
||||
parameters: { side },
|
||||
result: EMPTY_PROCESS_CAPTURE_EXAMPLE,
|
||||
confidence: "observed",
|
||||
authority: "controlled-replay",
|
||||
environment: {
|
||||
id: "fixture",
|
||||
platform: "fixture",
|
||||
architecture: "fixture",
|
||||
isolation: "process",
|
||||
},
|
||||
});
|
||||
const left = source("left");
|
||||
const right = source("right");
|
||||
const nativeResult = compareProcessCaptures(
|
||||
parseProcessCapture(left.normalized_result),
|
||||
parseProcessCapture(right.normalized_result),
|
||||
);
|
||||
expect(nativeResult.status).toBe("unchanged");
|
||||
const comparison = (status: "unchanged" | "contradiction") =>
|
||||
createEvidence(undefined, PROCESS_PROVIDER, {
|
||||
predicateType: "rea.process-comparison",
|
||||
operation: "compare_process_captures",
|
||||
parameters: {
|
||||
left_evidence_id: left.evidence_id,
|
||||
right_evidence_id: right.evidence_id,
|
||||
},
|
||||
result: jsonValueSchema.parse({
|
||||
...nativeResult,
|
||||
[dimension]: "contradiction",
|
||||
status,
|
||||
}),
|
||||
confidence: "derived",
|
||||
authority: "analyst-inference",
|
||||
evidenceLinks: [left.evidence_id, right.evidence_id],
|
||||
});
|
||||
const inconsistent = comparison("unchanged");
|
||||
const consistent = comparison("contradiction");
|
||||
const root = await createTestTempDirectory("rea-process-verification-");
|
||||
const path = join(root, "bundle.json");
|
||||
await writeFile(
|
||||
path,
|
||||
JSON.stringify(
|
||||
createEvidenceBundle([left, right, inconsistent, consistent]),
|
||||
),
|
||||
);
|
||||
let providerLaunches = 0;
|
||||
const session = createTestBinarySession(() => {
|
||||
providerLaunches += 1;
|
||||
throw new Error("Imported process verification must not launch a provider");
|
||||
});
|
||||
const harness = await createApplicationMcpHarness({ session });
|
||||
onTestFinished(harness.close);
|
||||
const imported = await harness.client.callTool({
|
||||
name: "import_evidence_bundle",
|
||||
arguments: { path },
|
||||
});
|
||||
expect(imported.isError).not.toBe(true);
|
||||
const verify = (evidenceId: string, selected: "overall" | typeof dimension) =>
|
||||
harness.client.callTool({
|
||||
name: "verify_reconstruction",
|
||||
arguments: {
|
||||
specification: {
|
||||
name: "Process comparison consistency",
|
||||
claims: [
|
||||
{
|
||||
kind: "behavioral",
|
||||
claim_id: selected,
|
||||
title: "Process replay agrees",
|
||||
comparison_evidence_id: evidenceId,
|
||||
dimension: selected,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
for (const selected of ["overall", dimension] as const) {
|
||||
const rejected = await verify(inconsistent.evidence_id, selected);
|
||||
expect(parseMcpToolError(rejected).error).toMatchObject({
|
||||
code: "invalid_request",
|
||||
details: {
|
||||
issues: [
|
||||
{
|
||||
reason: "invalid_value",
|
||||
message: expect.stringContaining(
|
||||
"Process comparison status contradicts its dimensions",
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
const failed = await verify(consistent.evidence_id, selected);
|
||||
expect(failed.isError).not.toBe(true);
|
||||
expect(
|
||||
reconstructionVerificationResultSchema.parse(
|
||||
parseEvidence(failed.structuredContent).normalized_result,
|
||||
),
|
||||
).toMatchObject({
|
||||
status: "fail",
|
||||
claims: { items: [{ observed_status: "contradiction" }] },
|
||||
});
|
||||
}
|
||||
for (const record of [left, right, inconsistent, consistent])
|
||||
expect(session.evidenceById(record.evidence_id)).toEqual(record);
|
||||
const recomputed = await harness.client.callTool({
|
||||
name: "compare_process_captures",
|
||||
arguments: { left, right },
|
||||
});
|
||||
expect(recomputed.isError).not.toBe(true);
|
||||
const control = parseEvidence(recomputed.structuredContent);
|
||||
const verified = await verify(control.evidence_id, "overall");
|
||||
expect(verified.isError).not.toBe(true);
|
||||
expect(
|
||||
reconstructionVerificationResultSchema.parse(
|
||||
parseEvidence(verified.structuredContent).normalized_result,
|
||||
),
|
||||
).toMatchObject({ status: "pass" });
|
||||
expect(providerLaunches).toBe(0);
|
||||
await harness.client.ping();
|
||||
});
|
||||
Reference in new issue
Block a user