mirror of
https://github.com/morluto/rea.git
synced 2026-11-13 02:02:36 +00:00
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:
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]),
|
||||
|
||||
@@ -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,
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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",
|
||||
});
|
||||
},
|
||||
);
|
||||
Reference in new issue
Block a user