fix(evidence)!: distinguish inline records from bundles (#7895)

* fix(evidence): distinguish inline records from bundles

* fix(evidence): preserve bundle integrity failure reasons
This commit is contained in:
morluto authored and GitHub committed 2026-10-29 07:34:31 +08:00
1 parent 5082b50f07
commit 473cfa36b8
5 files changed
+134 -6

No files matched your search

@@ -38,7 +38,10 @@ export const resolveReconstructionObligationLedgerRequest = (
): Result<ResolvedReconstructionObligationRequest, AnalysisError> => {
const frame = parseAnalysisInput(OPERATION, input, requestSchema);
if (!frame.ok) return frame;
const bundle = readEvidenceBundle(frame.value.evidence_bundle);
const bundle = readEvidenceBundle(frame.value.evidence_bundle, {
operation: OPERATION,
path: ["evidence_bundle"],
});
if (!bundle.ok) return bundle;
const records = new Map(
bundle.value.records.map((record) => [record.evidence_id, record]),
+16 -3
View File
@@ -2,6 +2,7 @@ import { InvalidResidualUnknownGraphError } from "./residualUnknownGraphError.js
import { withEvidenceSubjectDeclaration } from "./evidenceSubject.js";
import { digestSchema } from "./digests.js";
import { EvidenceIntegrityError } from "./evidenceErrors.js";
import { AnalysisInputError } from "./analysisErrorCore.js";
import { err, ok, type Result } from "./result.js";
import { describeValidationFailure } from "./validationFailure.js";
import { equalJsonText } from "./jsonSerialization.js";
@@ -295,14 +296,26 @@ const parseBundle = (input: unknown, seal: boolean): EvidenceBundle => {
/** Read a source bundle while preserving unexpected implementation failures. */
export const readEvidenceBundle = (
input: unknown,
): Result<EvidenceBundle, EvidenceIntegrityError> => {
context: {
readonly operation: string;
readonly path: readonly (string | number)[];
},
): Result<EvidenceBundle, AnalysisInputError | EvidenceIntegrityError> => {
const parsed = safeParseEvidenceBundle(input);
if (parsed.ok) return parsed;
const { cause } = parsed.error;
const { cause, kind, message } = parsed.error;
if (kind === "single-record")
return err(
new AnalysisInputError(
context.operation,
{ cause, userMessage: message, remediationAction: message },
[{ path: context.path, reason: "invalid_value", message }],
),
);
return err(
new EvidenceIntegrityError(cause.message, {
cause,
userMessage: cause.message,
userMessage: message,
}),
);
};
+4 -1
View File
@@ -234,7 +234,10 @@ export const readReconstructionCoverageData = (
})),
),
);
const bundle = readEvidenceBundle(parsed.data.evidence_bundle);
const bundle = readEvidenceBundle(parsed.data.evidence_bundle, {
operation: "evaluate_reconstruction_coverage",
path: [...path, "evidence_bundle"],
});
if (!bundle.ok) return bundle;
const coverage = { ...parsed.data, evidence_bundle: bundle.value };
try {
+4 -1
View File
@@ -60,7 +60,10 @@ export const verifyReconstruction = (
{ cause: parsedSpecification.error },
);
const specification = parsedSpecification.data;
const bundleResult = readEvidenceBundle(bundleInput);
const bundleResult = readEvidenceBundle(bundleInput, {
operation: "verify_reconstruction",
path: ["evidence_bundle"],
});
if (!bundleResult.ok) throw bundleResult.error;
const bundle = bundleResult.value;
const records = new Map(
@@ -0,0 +1,106 @@
import { writeFile } from "node:fs/promises";
import { join } from "node:path";
import { expect } from "vitest";
import { createEvidence } from "../../../src/domain/evidence.js";
import { completeReconstructionCoverageData } from "../../../src/domain/reconstructionCoverage.fixture.js";
import { readyProcessLedgerRequest } from "../../../src/application/ReconstructionObligationLedger.fixture.js";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import { parseMcpToolError } from "../../fixtures/mcpToolError.js";
import { cliTest } from "../../support/cli/cliFixture.js";
cliTest(
"inline reconstruction distinguishes an authenticated record from a corrupt bundle",
async ({ cli }) => {
const record = createEvidence(
undefined,
{ id: "fixture", name: "Fixture", version: "1" },
{
operation: "observe",
parameters: {},
result: { observed: true },
},
);
const root = await createTestTempDirectory("rea-inline-bundle-kind-");
const call = await cli.sharedMcpCall?.();
if (call === undefined) throw new Error("Expected MCP session");
const cases = [
{
command: "build-reconstruction-obligation-ledger",
tool: "build_reconstruction_obligation_ledger",
path: ["evidence_bundle"],
input: { ...readyProcessLedgerRequest(), evidence_bundle: record },
},
{
command: "evaluate-reconstruction-coverage",
tool: "evaluate_reconstruction_coverage",
path: ["coverage", "evidence_bundle"],
input: {
coverage: {
...completeReconstructionCoverageData(),
evidence_bundle: record,
},
boundary_id: "replacement.cli",
},
},
];
for (const scenario of cases) {
const request = join(root, `${scenario.command}.json`);
await writeFile(request, JSON.stringify(scenario.input));
const result = await cli.run({
arguments: [scenario.command, request, "--json"],
cwd: root,
});
const response = await call(scenario.tool, scenario.input);
expect(result.exitCode).toBe(1);
const expected = {
code: "invalid_request",
category: "invalid_input",
details: {
issues: [
{
path: scenario.path,
reason: "invalid_value",
message: expect.stringContaining("one Evidence record"),
},
],
},
remediation: {
action: expect.stringContaining("export_evidence_bundle"),
},
};
expect(result.json).toMatchObject(expected);
expect(response.isError).toBe(true);
// The advertised MCP schema rejects the wrong shape before the domain reader.
expect(parseMcpToolError(response).error).toMatchObject({
code: "invalid_request",
category: "invalid_input",
});
}
// A record with a changed payload is not authenticated as a standalone record.
const altered = { ...record, normalized_result: { observed: false } };
const malformed = {
...readyProcessLedgerRequest(),
evidence_bundle: altered,
};
const request = join(root, "altered.json");
await writeFile(request, JSON.stringify(malformed));
const result = await cli.run({
arguments: ["build-reconstruction-obligation-ledger", request, "--json"],
cwd: root,
});
const response = await call(
"build_reconstruction_obligation_ledger",
malformed,
);
expect(result.exitCode).toBe(1);
expect(result.json).toMatchObject({
code: "evidence_integrity_mismatch",
category: "integrity_mismatch",
});
expect(parseMcpToolError(response).error).toMatchObject({
code: "invalid_request",
});
},
);