mirror of
https://github.com/morluto/rea
synced 2026-10-26 06:16:17 -04:00
fix(reconstruction): share authenticated obligation ledger sources
This commit is contained in:
@@ -12,8 +12,11 @@ import {
|
||||
createJavaScriptSemanticGraphUnknown,
|
||||
JavaScriptSemanticEvidenceContextRegistry,
|
||||
} from "../domain/javascript/javascriptSemanticGraph.js";
|
||||
import { deriveReconstructionObligationCandidates } from "./ReconstructionObligationCandidates.js";
|
||||
import { build, request } from "./ReconstructionObligationLedger.fixture.js";
|
||||
import {
|
||||
build,
|
||||
deriveCandidates,
|
||||
request,
|
||||
} from "./ReconstructionObligationLedger.fixture.js";
|
||||
|
||||
const semanticApplication = (
|
||||
frontiers: readonly {
|
||||
@@ -162,10 +165,7 @@ describe("semantic uncertainty at the obligation candidate boundary", () => {
|
||||
),
|
||||
].sort();
|
||||
for (const order of [records, [...records].reverse()]) {
|
||||
const generated = deriveReconstructionObligationCandidates(
|
||||
createEvidenceBundle(order),
|
||||
[],
|
||||
);
|
||||
const generated = deriveCandidates(createEvidenceBundle(order), []);
|
||||
expect(generated.candidates).toHaveLength(1);
|
||||
expect(generated.candidates[0]).toMatchObject({
|
||||
definitionCount: 1,
|
||||
|
||||
@@ -5,7 +5,10 @@ import {
|
||||
createJavaScriptSemanticGraphNode,
|
||||
JavaScriptSemanticEvidenceContextRegistry,
|
||||
} from "../domain/javascript/javascriptSemanticGraph.js";
|
||||
import { proofEvidence } from "./ReconstructionObligationLedger.fixture.js";
|
||||
import {
|
||||
deriveCandidates,
|
||||
proofEvidence,
|
||||
} from "./ReconstructionObligationLedger.fixture.js";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { createEvidence } from "../domain/evidence.js";
|
||||
@@ -33,7 +36,6 @@ import {
|
||||
import { jsonValueSchema } from "../domain/jsonValue.js";
|
||||
import { MANAGED_WORKFLOW_PROVIDER } from "./InvestigationProviders.js";
|
||||
import { createProcessCaptureEvidence } from "./process/ProcessEvidence.js";
|
||||
import { deriveReconstructionObligationCandidates } from "./ReconstructionObligationCandidates.js";
|
||||
|
||||
const applicationWithUnknowns = (
|
||||
keys: readonly string[],
|
||||
@@ -119,7 +121,7 @@ const applicationWithUnknowns = (
|
||||
return {
|
||||
graph,
|
||||
unknownNodes,
|
||||
generated: deriveReconstructionObligationCandidates(
|
||||
generated: deriveCandidates(
|
||||
createEvidenceBundle([source, application]),
|
||||
[],
|
||||
),
|
||||
@@ -192,10 +194,7 @@ describe("reconstruction obligation candidates", () => {
|
||||
},
|
||||
});
|
||||
const evidence = createProcessCaptureEvidence(scenario, capture);
|
||||
const generated = deriveReconstructionObligationCandidates(
|
||||
createEvidenceBundle([evidence]),
|
||||
[],
|
||||
);
|
||||
const generated = deriveCandidates(createEvidenceBundle([evidence]), []);
|
||||
|
||||
expect(generated.candidates).toEqual([]);
|
||||
expect(generated.limitations).toContain(
|
||||
@@ -227,10 +226,7 @@ describe("reconstruction obligation candidates", () => {
|
||||
},
|
||||
}),
|
||||
});
|
||||
const generated = deriveReconstructionObligationCandidates(
|
||||
createEvidenceBundle([evidence]),
|
||||
[],
|
||||
);
|
||||
const generated = deriveCandidates(createEvidenceBundle([evidence]), []);
|
||||
|
||||
expect(generated.limitations).toEqual([
|
||||
`Process Evidence ${evidence.evidence_id} could not generate candidates: at manifest.completed_at: Invalid input: expected string, received undefined (1 more issues)`,
|
||||
@@ -318,7 +314,7 @@ it("keeps each semantic candidate bound to its own file and authority context",
|
||||
evidenceLinks: sourceRecords.map(({ evidence_id }) => evidence_id),
|
||||
},
|
||||
);
|
||||
const generated = deriveReconstructionObligationCandidates(
|
||||
const generated = deriveCandidates(
|
||||
createEvidenceBundle([...sourceRecords, application]),
|
||||
[],
|
||||
);
|
||||
@@ -425,7 +421,7 @@ it("falls back to a kind-based title when a semantic node label is blank", () =>
|
||||
evidenceLinks: [source.evidence_id],
|
||||
},
|
||||
);
|
||||
const generated = deriveReconstructionObligationCandidates(
|
||||
const generated = deriveCandidates(
|
||||
createEvidenceBundle([source, application]),
|
||||
[],
|
||||
);
|
||||
|
||||
@@ -3,11 +3,11 @@ import { digestCanonicalValue } from "../domain/canonicalDigest.js";
|
||||
|
||||
import type { Evidence } from "../domain/evidence.js";
|
||||
import { describeValidationFailure } from "../domain/validationFailure.js";
|
||||
import { z } from "zod";
|
||||
import {
|
||||
parseEvidenceBundle,
|
||||
type EvidenceBundle,
|
||||
} from "../domain/evidenceBundle.js";
|
||||
import { parseProcessCapture } from "../domain/process/processCaptureParsing.js";
|
||||
parseProcessCapture,
|
||||
type ProcessCapture,
|
||||
} from "../domain/process/processCaptureParsing.js";
|
||||
import type {
|
||||
ReconstructionObligation,
|
||||
ReviewedReconstructionObligation,
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
semanticObligationPolicy,
|
||||
type ReconstructionObligationCandidatePolicy,
|
||||
} from "./ReconstructionObligationPolicies.js";
|
||||
import type { ResolvedReconstructionObligationRequest } from "./ReconstructionObligationLedgerRequest.js";
|
||||
import { compareUnicodeCodePoints } from "../domain/unicodeCodePointOrder.js";
|
||||
|
||||
type CandidateFields = Omit<
|
||||
@@ -36,30 +37,21 @@ export interface ReconstructionObligationCandidateSet {
|
||||
readonly limitations: readonly string[];
|
||||
}
|
||||
|
||||
/** Caller input cites review Evidence that its bundle does not contain. */
|
||||
export class MissingReviewEvidenceError extends TypeError {
|
||||
constructor(evidenceId: string) {
|
||||
super(`Reviewed obligation Evidence is missing: ${evidenceId}`);
|
||||
this.name = "MissingReviewEvidenceError";
|
||||
}
|
||||
}
|
||||
|
||||
/** Derive conservative candidates from graph, process, and reviewed inputs. */
|
||||
export const deriveReconstructionObligationCandidates = (
|
||||
bundleInput: EvidenceBundle,
|
||||
reviewed: readonly ReviewedReconstructionObligation[],
|
||||
request: Pick<
|
||||
ResolvedReconstructionObligationRequest,
|
||||
"sources" | "reviewedObligations"
|
||||
>,
|
||||
): ReconstructionObligationCandidateSet => {
|
||||
const bundle = parseEvidenceBundle(bundleInput);
|
||||
const records = new Map(
|
||||
bundle.records.map((record) => [record.evidence_id, record]),
|
||||
);
|
||||
const { bundle, records } = request.sources;
|
||||
const candidates = new Map<string, ReconstructionObligationCandidate>();
|
||||
const limitations = new Set<string>();
|
||||
for (const evidence of bundle.records) {
|
||||
deriveApplicationCandidates(evidence, records, candidates, limitations);
|
||||
deriveProcessCandidate(evidence, candidates, limitations);
|
||||
}
|
||||
for (const obligation of reviewed)
|
||||
for (const obligation of request.reviewedObligations)
|
||||
addCandidate(candidates, reviewedCandidate(obligation, records));
|
||||
return {
|
||||
candidates: [...candidates.values()].sort((left, right) =>
|
||||
@@ -83,115 +75,109 @@ const deriveApplicationCandidates = (
|
||||
].includes(evidence.operation)
|
||||
)
|
||||
return;
|
||||
try {
|
||||
const parsed = parseApplicationGraphEvidence(evidence);
|
||||
if (!parsed.ok) {
|
||||
limitations.add(
|
||||
`Application Evidence ${evidence.evidence_id} could not generate candidates because it failed graph validation.`,
|
||||
);
|
||||
return;
|
||||
const parsed = parseApplicationGraphEvidence(evidence);
|
||||
if (!parsed.ok) {
|
||||
limitations.add(
|
||||
`Application Evidence ${evidence.evidence_id} could not generate candidates because it failed graph validation.`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
const source = parsed.value;
|
||||
for (const node of source.graph.nodes) {
|
||||
const candidatePolicy = applicationObligationPolicy(node.kind);
|
||||
if (candidatePolicy === undefined) {
|
||||
if (node.kind === "unknown")
|
||||
limitations.add(
|
||||
`Application graph ${source.graph.graph_id} contains unresolved node ${node.node_id}; reconstruction obligations remain unknown.`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
const source = parsed.value;
|
||||
for (const node of source.graph.nodes) {
|
||||
const candidatePolicy = applicationObligationPolicy(node.kind);
|
||||
if (candidatePolicy === undefined) {
|
||||
if (node.kind === "unknown")
|
||||
limitations.add(
|
||||
`Application graph ${source.graph.graph_id} contains unresolved node ${node.node_id}; reconstruction obligations remain unknown.`,
|
||||
);
|
||||
continue;
|
||||
const reference = node.observations.map((observation) =>
|
||||
graphReference(
|
||||
evidence,
|
||||
records,
|
||||
observation.evidence,
|
||||
`${source.graph.graph_id}/node/${node.node_id}/observation/${observation.observation_id}`,
|
||||
),
|
||||
);
|
||||
addCandidate(
|
||||
candidates,
|
||||
generatedCandidate({
|
||||
key: `application:${node.node_id}:${candidatePolicy.family}`,
|
||||
title: node.observations[0]?.label?.trim() || `${node.kind} boundary`,
|
||||
policy: candidatePolicy,
|
||||
artifactSha256: source.rootArtifactSha256,
|
||||
applicationNodeId: node.node_id,
|
||||
semanticNodeId: null,
|
||||
location: `application-graph:${source.graph.graph_id}/nodes/${node.node_id}`,
|
||||
authorityReferences: reference,
|
||||
}),
|
||||
);
|
||||
}
|
||||
if (source.semanticGraph !== null) {
|
||||
const semanticGraph = source.semanticGraph;
|
||||
const unknownIdsByNode = new Map<string, string[]>();
|
||||
for (const unknown of semanticGraph.unknowns) {
|
||||
// Candidate targets are possibilities, not established node bindings.
|
||||
if (unknown.node_id !== null) {
|
||||
const ids = unknownIdsByNode.get(unknown.node_id) ?? [];
|
||||
ids.push(unknown.unknown_id);
|
||||
unknownIdsByNode.set(unknown.node_id, ids);
|
||||
}
|
||||
const reference = node.observations.map((observation) =>
|
||||
graphReference(
|
||||
evidence,
|
||||
records,
|
||||
observation.evidence,
|
||||
`${source.graph.graph_id}/node/${node.node_id}/observation/${observation.observation_id}`,
|
||||
),
|
||||
// Keep every frontier visible, including families without a policy.
|
||||
limitations.add(
|
||||
`Semantic graph ${semanticGraph.graph_id} contains unresolved ${unknown.family} unknown ${unknown.unknown_id}: ${unknown.detail}`,
|
||||
);
|
||||
}
|
||||
if (semanticGraph.coverage.status !== "complete")
|
||||
limitations.add(
|
||||
`Semantic graph ${semanticGraph.graph_id} coverage is ${semanticGraph.coverage.status}; omitted candidates remain unknown.`,
|
||||
);
|
||||
for (const limitation of semanticGraph.limitations)
|
||||
limitations.add(
|
||||
`Semantic graph ${semanticGraph.graph_id}: ${limitation}`,
|
||||
);
|
||||
const evidenceContexts = new Map(
|
||||
semanticGraph.evidence_contexts.map((context) => [
|
||||
context.context_id,
|
||||
context,
|
||||
]),
|
||||
);
|
||||
for (const node of semanticGraph.nodes) {
|
||||
const candidatePolicy = semanticObligationPolicy(node.kind);
|
||||
if (candidatePolicy === undefined) continue;
|
||||
const evidenceContext = evidenceContexts.get(node.evidence.context_id);
|
||||
if (evidenceContext === undefined)
|
||||
throw new TypeError(
|
||||
"Semantic evidence reference names an absent context",
|
||||
);
|
||||
addCandidate(
|
||||
candidates,
|
||||
generatedCandidate({
|
||||
key: `application:${node.node_id}:${candidatePolicy.family}`,
|
||||
title: node.observations[0]?.label?.trim() || `${node.kind} boundary`,
|
||||
key: `semantic:${node.node_id}:${candidatePolicy.family}`,
|
||||
title: node.label?.trim() || `${node.kind} semantic boundary`,
|
||||
policy: candidatePolicy,
|
||||
artifactSha256: source.rootArtifactSha256,
|
||||
applicationNodeId: node.node_id,
|
||||
semanticNodeId: null,
|
||||
location: `application-graph:${source.graph.graph_id}/nodes/${node.node_id}`,
|
||||
authorityReferences: reference,
|
||||
artifactSha256: node.identity.artifact_sha256,
|
||||
applicationNodeId: node.application_node_ids[0] ?? null,
|
||||
semanticNodeId: node.node_id,
|
||||
residualUnknownIds: unknownIdsByNode.get(node.node_id) ?? [],
|
||||
location: `semantic-graph:${semanticGraph.graph_id}/nodes/${node.node_id}`,
|
||||
authorityReferences: [
|
||||
graphReference(
|
||||
evidence,
|
||||
records,
|
||||
evidenceContext,
|
||||
`${semanticGraph.graph_id}/node/${node.node_id}`,
|
||||
),
|
||||
],
|
||||
}),
|
||||
);
|
||||
}
|
||||
if (source.semanticGraph !== null) {
|
||||
const semanticGraph = source.semanticGraph;
|
||||
const unknownIdsByNode = new Map<string, string[]>();
|
||||
for (const unknown of semanticGraph.unknowns) {
|
||||
// Candidate targets are possibilities, not established node bindings.
|
||||
if (unknown.node_id !== null) {
|
||||
const ids = unknownIdsByNode.get(unknown.node_id) ?? [];
|
||||
ids.push(unknown.unknown_id);
|
||||
unknownIdsByNode.set(unknown.node_id, ids);
|
||||
}
|
||||
// Keep every frontier visible, including families without a policy.
|
||||
limitations.add(
|
||||
`Semantic graph ${semanticGraph.graph_id} contains unresolved ${unknown.family} unknown ${unknown.unknown_id}: ${unknown.detail}`,
|
||||
);
|
||||
}
|
||||
if (semanticGraph.coverage.status !== "complete")
|
||||
limitations.add(
|
||||
`Semantic graph ${semanticGraph.graph_id} coverage is ${semanticGraph.coverage.status}; omitted candidates remain unknown.`,
|
||||
);
|
||||
for (const limitation of semanticGraph.limitations)
|
||||
limitations.add(
|
||||
`Semantic graph ${semanticGraph.graph_id}: ${limitation}`,
|
||||
);
|
||||
const evidenceContexts = new Map(
|
||||
semanticGraph.evidence_contexts.map((context) => [
|
||||
context.context_id,
|
||||
context,
|
||||
]),
|
||||
);
|
||||
for (const node of semanticGraph.nodes) {
|
||||
const candidatePolicy = semanticObligationPolicy(node.kind);
|
||||
if (candidatePolicy === undefined) continue;
|
||||
const evidenceContext = evidenceContexts.get(node.evidence.context_id);
|
||||
if (evidenceContext === undefined)
|
||||
throw new TypeError(
|
||||
"Semantic evidence reference names an absent context",
|
||||
);
|
||||
addCandidate(
|
||||
candidates,
|
||||
generatedCandidate({
|
||||
key: `semantic:${node.node_id}:${candidatePolicy.family}`,
|
||||
title: node.label?.trim() || `${node.kind} semantic boundary`,
|
||||
policy: candidatePolicy,
|
||||
artifactSha256: node.identity.artifact_sha256,
|
||||
applicationNodeId: node.application_node_ids[0] ?? null,
|
||||
semanticNodeId: node.node_id,
|
||||
residualUnknownIds: unknownIdsByNode.get(node.node_id) ?? [],
|
||||
location: `semantic-graph:${semanticGraph.graph_id}/nodes/${node.node_id}`,
|
||||
authorityReferences: [
|
||||
graphReference(
|
||||
evidence,
|
||||
records,
|
||||
evidenceContext,
|
||||
`${semanticGraph.graph_id}/node/${node.node_id}`,
|
||||
),
|
||||
],
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
if (source.graph.coverage.status !== "complete")
|
||||
limitations.add(
|
||||
`Application graph ${source.graph.graph_id} coverage is ${source.graph.coverage.status}; omitted candidates remain unknown.`,
|
||||
);
|
||||
} catch (cause: unknown) {
|
||||
limitations.add(
|
||||
`Application Evidence ${evidence.evidence_id} could not generate candidates: ${describeValidationFailure(cause)}`,
|
||||
);
|
||||
}
|
||||
if (source.graph.coverage.status !== "complete")
|
||||
limitations.add(
|
||||
`Application graph ${source.graph.graph_id} coverage is ${source.graph.coverage.status}; omitted candidates remain unknown.`,
|
||||
);
|
||||
};
|
||||
|
||||
const deriveProcessCandidate = (
|
||||
@@ -204,86 +190,89 @@ const deriveProcessCandidate = (
|
||||
evidence.predicate_type !== "rea.process-capture"
|
||||
)
|
||||
return;
|
||||
let capture: ProcessCapture;
|
||||
try {
|
||||
const capture = parseProcessCapture(evidence.normalized_result);
|
||||
const observedCases: ReconstructionObligation["observed_cases"] = [
|
||||
{
|
||||
kind: capture.exit.code === 0 ? "positive" : "negative",
|
||||
evidence_id: evidence.evidence_id,
|
||||
location: "/normalized_result/exit",
|
||||
},
|
||||
{
|
||||
kind: "teardown",
|
||||
evidence_id: evidence.evidence_id,
|
||||
location: "/normalized_result/settlement",
|
||||
},
|
||||
...(capture.exit.signal !== null || capture.exit.reason !== "exited"
|
||||
? [
|
||||
{
|
||||
kind: "cancellation" as const,
|
||||
evidence_id: evidence.evidence_id,
|
||||
location: "/normalized_result/exit",
|
||||
},
|
||||
]
|
||||
: []),
|
||||
];
|
||||
if (capture.manifest.executable_sha256 === null) {
|
||||
limitations.add(
|
||||
`Process capture ${evidence.evidence_id} has no validated executable artifact identity; packaged-process lifecycle obligation was not bound to an artifact.`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
addCandidate(candidates, {
|
||||
obligation_id: obligationId(
|
||||
`process:${capture.manifest.executable_sha256}:${capture.manifest.full_scenario_sha256}`,
|
||||
),
|
||||
obligation_version: 1,
|
||||
title: "Captured packaged-process lifecycle",
|
||||
origin: "generated",
|
||||
application_layer: "process",
|
||||
family: "packaged-process-lifecycle",
|
||||
target: {
|
||||
artifact_sha256: capture.manifest.executable_sha256,
|
||||
application_node_id: null,
|
||||
semantic_node_id: null,
|
||||
location: `/evidence/${evidence.evidence_id}/normalized_result`,
|
||||
},
|
||||
authority_references: [
|
||||
{
|
||||
evidence_id: evidence.evidence_id,
|
||||
authority: evidence.authority,
|
||||
state: "observed",
|
||||
location: "/normalized_result",
|
||||
},
|
||||
],
|
||||
source_state: "observed",
|
||||
observed_cases: observedCases,
|
||||
required: true,
|
||||
required_case_kinds: ["positive", "negative", "cancellation", "teardown"],
|
||||
required_original_authority: "process",
|
||||
required_fixture_authority: "packaged-process",
|
||||
required_verifier_authority: "packaged-process",
|
||||
requires_parser_type: false,
|
||||
dependency_obligation_ids: [],
|
||||
residual_unknown_ids: [],
|
||||
unavailable_authority: capture.residual_unknowns.map(
|
||||
({ scope, reason }) => `${scope}: ${reason}`,
|
||||
),
|
||||
required_next_evidence: capture.truncated
|
||||
? ["Repeat the packaged-process capture without truncation."]
|
||||
: [],
|
||||
definitionCount: 1,
|
||||
disposition: "active",
|
||||
});
|
||||
if (capture.truncated)
|
||||
limitations.add(
|
||||
`Process Evidence ${evidence.evidence_id} is truncated; absence and upper bounds remain unknown.`,
|
||||
);
|
||||
capture = parseProcessCapture(evidence.normalized_result);
|
||||
} catch (cause: unknown) {
|
||||
if (!(cause instanceof z.ZodError)) throw cause;
|
||||
limitations.add(
|
||||
`Process Evidence ${evidence.evidence_id} could not generate candidates: ${describeValidationFailure(cause)}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
const observedCases: ReconstructionObligation["observed_cases"] = [
|
||||
{
|
||||
kind: capture.exit.code === 0 ? "positive" : "negative",
|
||||
evidence_id: evidence.evidence_id,
|
||||
location: "/normalized_result/exit",
|
||||
},
|
||||
{
|
||||
kind: "teardown",
|
||||
evidence_id: evidence.evidence_id,
|
||||
location: "/normalized_result/settlement",
|
||||
},
|
||||
...(capture.exit.signal !== null || capture.exit.reason !== "exited"
|
||||
? [
|
||||
{
|
||||
kind: "cancellation" as const,
|
||||
evidence_id: evidence.evidence_id,
|
||||
location: "/normalized_result/exit",
|
||||
},
|
||||
]
|
||||
: []),
|
||||
];
|
||||
if (capture.manifest.executable_sha256 === null) {
|
||||
limitations.add(
|
||||
`Process capture ${evidence.evidence_id} has no validated executable artifact identity; packaged-process lifecycle obligation was not bound to an artifact.`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
addCandidate(candidates, {
|
||||
obligation_id: obligationId(
|
||||
`process:${capture.manifest.executable_sha256}:${capture.manifest.full_scenario_sha256}`,
|
||||
),
|
||||
obligation_version: 1,
|
||||
title: "Captured packaged-process lifecycle",
|
||||
origin: "generated",
|
||||
application_layer: "process",
|
||||
family: "packaged-process-lifecycle",
|
||||
target: {
|
||||
artifact_sha256: capture.manifest.executable_sha256,
|
||||
application_node_id: null,
|
||||
semantic_node_id: null,
|
||||
location: `/evidence/${evidence.evidence_id}/normalized_result`,
|
||||
},
|
||||
authority_references: [
|
||||
{
|
||||
evidence_id: evidence.evidence_id,
|
||||
authority: evidence.authority,
|
||||
state: "observed",
|
||||
location: "/normalized_result",
|
||||
},
|
||||
],
|
||||
source_state: "observed",
|
||||
observed_cases: observedCases,
|
||||
required: true,
|
||||
required_case_kinds: ["positive", "negative", "cancellation", "teardown"],
|
||||
required_original_authority: "process",
|
||||
required_fixture_authority: "packaged-process",
|
||||
required_verifier_authority: "packaged-process",
|
||||
requires_parser_type: false,
|
||||
dependency_obligation_ids: [],
|
||||
residual_unknown_ids: [],
|
||||
unavailable_authority: capture.residual_unknowns.map(
|
||||
({ scope, reason }) => `${scope}: ${reason}`,
|
||||
),
|
||||
required_next_evidence: capture.truncated
|
||||
? ["Repeat the packaged-process capture without truncation."]
|
||||
: [],
|
||||
definitionCount: 1,
|
||||
disposition: "active",
|
||||
});
|
||||
if (capture.truncated)
|
||||
limitations.add(
|
||||
`Process Evidence ${evidence.evidence_id} is truncated; absence and upper bounds remain unknown.`,
|
||||
);
|
||||
};
|
||||
|
||||
interface GeneratedCandidateInput {
|
||||
@@ -355,7 +344,9 @@ const reviewedCandidate = (
|
||||
authority_references: reviewEvidenceIds.map((evidenceId) => {
|
||||
const evidence = records.get(evidenceId);
|
||||
if (evidence === undefined)
|
||||
throw new MissingReviewEvidenceError(evidenceId);
|
||||
throw new Error(
|
||||
`Resolved reviewed obligation references absent Evidence: ${evidenceId}`,
|
||||
);
|
||||
return {
|
||||
evidence_id: evidenceId,
|
||||
authority: evidence.authority,
|
||||
|
||||
@@ -2,6 +2,8 @@ import { describe, expect, it } from "vitest";
|
||||
|
||||
import {
|
||||
build,
|
||||
deriveCandidates,
|
||||
resolveRequest,
|
||||
originalCases,
|
||||
processEvidence,
|
||||
proofEvidence,
|
||||
@@ -9,7 +11,6 @@ import {
|
||||
request,
|
||||
reviewedObligation,
|
||||
} from "./ReconstructionObligationLedger.fixture.js";
|
||||
import { deriveReconstructionObligationCandidates } from "./ReconstructionObligationCandidates.js";
|
||||
import { evaluateReconstructionObligationLedger } from "./ReconstructionObligationLedgerEvaluation.js";
|
||||
|
||||
describe("reconstruction obligation ledger", () => {
|
||||
@@ -136,7 +137,7 @@ describe("reconstruction obligation ledger Evidence provenance", () => {
|
||||
const capture = processEvidence();
|
||||
const caseEvidence = proofEvidence("distinct-original-case");
|
||||
const bundle = request([capture, caseEvidence]).evidence_bundle;
|
||||
const generated = deriveReconstructionObligationCandidates(bundle, []);
|
||||
const generated = deriveCandidates(bundle, []);
|
||||
const candidates = generated.candidates.map((candidate) => ({
|
||||
...candidate,
|
||||
observed_cases: candidate.observed_cases.map((observedCase) => ({
|
||||
@@ -147,7 +148,11 @@ describe("reconstruction obligation ledger Evidence provenance", () => {
|
||||
|
||||
const result = evaluateReconstructionObligationLedger({
|
||||
candidates,
|
||||
bundle,
|
||||
sources: resolveRequest({
|
||||
evidence_bundle: bundle,
|
||||
reviewed_obligations: [],
|
||||
manifest: { bindings: [], contradictions: [] },
|
||||
}).sources,
|
||||
manifest: { bindings: [], contradictions: [] },
|
||||
generationLimitations: [],
|
||||
});
|
||||
@@ -234,13 +239,13 @@ describe("reconstruction obligation ledger case closure", () => {
|
||||
expect(result.status).toBe("ready");
|
||||
expect(result.summary.required_open).toBe(0);
|
||||
|
||||
const generated = deriveReconstructionObligationCandidates(
|
||||
const generated = deriveCandidates(
|
||||
ledgerRequest.evidence_bundle,
|
||||
ledgerRequest.reviewed_obligations,
|
||||
);
|
||||
const limited = evaluateReconstructionObligationLedger({
|
||||
candidates: generated.candidates,
|
||||
bundle: ledgerRequest.evidence_bundle,
|
||||
sources: resolveRequest(ledgerRequest).sources,
|
||||
manifest: ledgerRequest.manifest,
|
||||
generationLimitations: ["A supplementary source was unavailable."],
|
||||
});
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import { PROCESS_PROVIDER } from "../domain/process/processEvidenceProvider.js";
|
||||
import {
|
||||
buildReconstructionObligationLedgerEvidenceValidated,
|
||||
resolveReconstructionObligationLedgerRequest,
|
||||
} from "./ReconstructionObligationLedgerService.js";
|
||||
import { buildReconstructionObligationLedgerEvidence } from "./ReconstructionObligationLedgerService.js";
|
||||
import { resolveReconstructionObligationLedgerRequest } from "./ReconstructionObligationLedgerRequest.js";
|
||||
import { deriveReconstructionObligationCandidates } from "./ReconstructionObligationCandidates.js";
|
||||
import { EMPTY_PROCESS_CAPTURE_EXAMPLE } from "../domain/process/processCaptureExample.js";
|
||||
import { createEvidence, type Evidence } from "../domain/evidence.js";
|
||||
import { createEvidenceBundle } from "../domain/evidenceBundle.js";
|
||||
import {
|
||||
createEvidenceBundle,
|
||||
type EvidenceBundle,
|
||||
} from "../domain/evidenceBundle.js";
|
||||
import { jsonValueSchema } from "../domain/jsonValue.js";
|
||||
import {
|
||||
reconstructionObligationLedgerSchema,
|
||||
@@ -98,11 +100,7 @@ export const request = (
|
||||
export const build = (
|
||||
input: ReconstructionObligationLedgerInput,
|
||||
): ReconstructionObligationLedger => {
|
||||
const parsed = resolveReconstructionObligationLedgerRequest(input);
|
||||
if (!parsed.ok) throw parsed.error;
|
||||
const result = buildReconstructionObligationLedgerEvidenceValidated(
|
||||
parsed.value,
|
||||
);
|
||||
const result = buildReconstructionObligationLedgerEvidence(input);
|
||||
if (!result.ok) throw result.error;
|
||||
return reconstructionObligationLedgerSchema.parse(
|
||||
result.value.normalized_result,
|
||||
@@ -266,3 +264,23 @@ export const MIXED_AUTHORITY_SUMMARY = {
|
||||
{ key: "unknown", count: 0 },
|
||||
],
|
||||
} as const;
|
||||
|
||||
/** Resolve fixture sources through the production request owner. */
|
||||
export const resolveRequest = (input: ReconstructionObligationLedgerInput) => {
|
||||
const resolved = resolveReconstructionObligationLedgerRequest(input);
|
||||
if (!resolved.ok) throw resolved.error;
|
||||
return resolved.value;
|
||||
};
|
||||
|
||||
/** Derive candidates from fixture requests after authenticating their sources. */
|
||||
export const deriveCandidates = (
|
||||
bundle: EvidenceBundle,
|
||||
reviewed: readonly ReviewedReconstructionObligation[],
|
||||
) =>
|
||||
deriveReconstructionObligationCandidates(
|
||||
resolveRequest({
|
||||
evidence_bundle: bundle,
|
||||
reviewed_obligations: [...reviewed],
|
||||
manifest: { bindings: [], contradictions: [] },
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
import type { Evidence } from "../domain/evidence.js";
|
||||
import {
|
||||
parseEvidenceBundle,
|
||||
type EvidenceBundle,
|
||||
} from "../domain/evidenceBundle.js";
|
||||
import type { ReconstructionObligationSources } from "./ReconstructionObligationLedgerRequest.js";
|
||||
import {
|
||||
reconstructionObligationLedgerSchema,
|
||||
type ReconstructionObligation,
|
||||
@@ -52,22 +49,21 @@ interface EvaluationContext {
|
||||
|
||||
export const evaluateReconstructionObligationLedger = ({
|
||||
candidates: candidatesInput,
|
||||
bundle: bundleInput,
|
||||
sources,
|
||||
manifest,
|
||||
generationLimitations,
|
||||
}: {
|
||||
readonly candidates: readonly ReconstructionObligationCandidate[];
|
||||
readonly bundle: EvidenceBundle;
|
||||
readonly sources: ReconstructionObligationSources;
|
||||
readonly manifest: ReconstructionObligationManifest;
|
||||
readonly generationLimitations: readonly string[];
|
||||
}): ReconstructionObligationLedger => {
|
||||
const bundle = parseEvidenceBundle(bundleInput);
|
||||
const allCandidates = [...candidatesInput].sort((left, right) =>
|
||||
const { bundle, records } = sources;
|
||||
const candidates = [...candidatesInput].sort((left, right) =>
|
||||
compareUnicodeCodePoints(left.obligation_id, right.obligation_id),
|
||||
);
|
||||
const orphanManifestIds = findOrphanManifestIds(allCandidates, manifest);
|
||||
const candidates = allCandidates;
|
||||
const context = createEvaluationContext(candidates, bundle, manifest);
|
||||
const orphanManifestIds = findOrphanManifestIds(candidates, manifest);
|
||||
const context = createEvaluationContext(candidates, records, manifest);
|
||||
const obligations = applyDependencyDiagnostics(
|
||||
candidates.map((candidate) => evaluateObligation(candidate, context)),
|
||||
context,
|
||||
@@ -77,7 +73,7 @@ export const evaluateReconstructionObligationLedger = ({
|
||||
const evidenceLinks = obligationLedgerEvidenceLinks(obligations, manifest);
|
||||
const limitations = ledgerLimitations(
|
||||
generationLimitations,
|
||||
allCandidates.length,
|
||||
candidates.length,
|
||||
orphanManifestIds,
|
||||
);
|
||||
const coverage = reconstructionObligationCoverage(limitations);
|
||||
@@ -115,15 +111,13 @@ export const evaluateReconstructionObligationLedger = ({
|
||||
|
||||
const createEvaluationContext = (
|
||||
candidates: readonly ReconstructionObligationCandidate[],
|
||||
bundle: EvidenceBundle,
|
||||
evidence: ReadonlyMap<string, Evidence>,
|
||||
manifest: ReconstructionObligationManifest,
|
||||
): EvaluationContext => ({
|
||||
candidates: new Map(
|
||||
candidates.map((candidate) => [candidate.obligation_id, candidate]),
|
||||
),
|
||||
evidence: new Map(
|
||||
bundle.records.map((evidence) => [evidence.evidence_id, evidence]),
|
||||
),
|
||||
evidence,
|
||||
bindings: groupManifestBindings(manifest),
|
||||
contradictions: groupManifestContradictions(manifest),
|
||||
});
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { AnalysisInputError } from "../domain/analysisErrorCore.js";
|
||||
import type { AnalysisError } from "../domain/analysisErrorBase.js";
|
||||
import type { Evidence } from "../domain/evidence.js";
|
||||
import {
|
||||
readEvidenceBundle,
|
||||
type EvidenceBundle,
|
||||
} from "../domain/evidenceBundle.js";
|
||||
import {
|
||||
reconstructionObligationLedgerInputSchema,
|
||||
type ReviewedReconstructionObligation,
|
||||
type ReconstructionObligationManifest,
|
||||
} from "../domain/reconstructionObligationLedgerSchemas.js";
|
||||
import { err, ok, type Result } from "../domain/result.js";
|
||||
import { requestInputError } from "./workflowInputError.js";
|
||||
|
||||
const OPERATION = "build_reconstruction_obligation_ledger";
|
||||
const requestSchema = reconstructionObligationLedgerInputSchema.extend({
|
||||
evidence_bundle: z.unknown(),
|
||||
});
|
||||
|
||||
/** Authenticated bundle and its single request-owned record index. */
|
||||
export interface ReconstructionObligationSources {
|
||||
readonly bundle: EvidenceBundle;
|
||||
readonly records: ReadonlyMap<string, Evidence>;
|
||||
}
|
||||
|
||||
export interface ResolvedReconstructionObligationRequest {
|
||||
readonly sources: ReconstructionObligationSources;
|
||||
readonly reviewedObligations: readonly ReviewedReconstructionObligation[];
|
||||
readonly manifest: ReconstructionObligationManifest;
|
||||
}
|
||||
|
||||
/** Authenticate the source bundle and bind every selected review reference. */
|
||||
export const resolveReconstructionObligationLedgerRequest = (
|
||||
input: unknown,
|
||||
): Result<ResolvedReconstructionObligationRequest, AnalysisError> => {
|
||||
const frame = requestSchema.safeParse(input);
|
||||
if (!frame.success)
|
||||
return err(requestInputError(OPERATION, frame.error, input));
|
||||
const bundle = readEvidenceBundle(frame.data.evidence_bundle);
|
||||
if (!bundle.ok) return bundle;
|
||||
const records = new Map(
|
||||
bundle.value.records.map((record) => [record.evidence_id, record]),
|
||||
);
|
||||
for (const [index, obligation] of frame.data.reviewed_obligations.entries()) {
|
||||
for (const [
|
||||
referenceIndex,
|
||||
id,
|
||||
] of obligation.review_evidence_ids.entries()) {
|
||||
if (records.has(id)) continue;
|
||||
const message = `Reviewed obligation Evidence is missing: ${id}`;
|
||||
return err(
|
||||
new AnalysisInputError(OPERATION, { userMessage: message }, [
|
||||
{
|
||||
path: [
|
||||
"reviewed_obligations",
|
||||
index,
|
||||
"review_evidence_ids",
|
||||
referenceIndex,
|
||||
],
|
||||
reason: "invalid_value",
|
||||
message,
|
||||
},
|
||||
]),
|
||||
);
|
||||
}
|
||||
}
|
||||
return ok({
|
||||
sources: { bundle: bundle.value, records },
|
||||
reviewedObligations: frame.data.reviewed_obligations,
|
||||
manifest: frame.data.manifest,
|
||||
});
|
||||
};
|
||||
@@ -1,94 +1,55 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { AnalysisProtocolError } from "../domain/analysisErrorCore.js";
|
||||
import type { AnalysisError } from "../domain/analysisErrorBase.js";
|
||||
import {
|
||||
DuplicateEvidenceRecordError,
|
||||
InvalidResidualUnknownGraphError,
|
||||
NonCanonicalEvidenceBundleError,
|
||||
} from "../domain/evidenceBundle.js";
|
||||
import { createEvidence, type Evidence } from "../domain/evidence.js";
|
||||
import { jsonObjectSchema, jsonValueSchema } from "../domain/jsonValue.js";
|
||||
import {
|
||||
reconstructionObligationLedgerInputSchema,
|
||||
reconstructionObligationLedgerSchema,
|
||||
type ReconstructionObligationLedgerInput,
|
||||
type ReconstructionObligationLedger,
|
||||
} from "../domain/reconstructionObligationLedgerSchemas.js";
|
||||
import { err, ok, type Result } from "../domain/result.js";
|
||||
import { requestInputError, workflowInputError } from "./workflowInputError.js";
|
||||
import type { ReconstructionObligationLedger } from "../domain/reconstructionObligationLedgerSchemas.js";
|
||||
import { ok, type Result } from "../domain/result.js";
|
||||
import { JAVASCRIPT_APPLICATION_WORKFLOW_PROVIDER } from "./InvestigationProviders.js";
|
||||
import {
|
||||
deriveReconstructionObligationCandidates,
|
||||
MissingReviewEvidenceError,
|
||||
} from "./ReconstructionObligationCandidates.js";
|
||||
import { deriveReconstructionObligationCandidates } from "./ReconstructionObligationCandidates.js";
|
||||
import { evaluateReconstructionObligationLedger } from "./ReconstructionObligationLedgerEvaluation.js";
|
||||
import {
|
||||
resolveReconstructionObligationLedgerRequest,
|
||||
type ResolvedReconstructionObligationRequest,
|
||||
} from "./ReconstructionObligationLedgerRequest.js";
|
||||
|
||||
const OPERATION = "build_reconstruction_obligation_ledger" as const;
|
||||
const OPERATION = "build_reconstruction_obligation_ledger";
|
||||
|
||||
/** Parse one portable ledger request for both CLI and MCP adapters. */
|
||||
export const resolveReconstructionObligationLedgerRequest = (
|
||||
/** Build portable ledger Evidence from a caller-selected request. */
|
||||
export const buildReconstructionObligationLedgerEvidence = (
|
||||
input: unknown,
|
||||
): Result<ReconstructionObligationLedgerInput, AnalysisError> => {
|
||||
const parsed = reconstructionObligationLedgerInputSchema.safeParse(input);
|
||||
return parsed.success
|
||||
? ok(parsed.data)
|
||||
: err(requestInputError(OPERATION, parsed.error, input));
|
||||
): Result<Evidence, AnalysisError> => {
|
||||
const request = resolveReconstructionObligationLedgerRequest(input);
|
||||
if (!request.ok) return request;
|
||||
return ok(buildResolvedReconstructionObligationLedgerEvidence(request.value));
|
||||
};
|
||||
|
||||
/** Build the complete deterministic ledger and wrap it in portable Evidence. */
|
||||
export const buildReconstructionObligationLedgerEvidenceValidated = (
|
||||
input: ReconstructionObligationLedgerInput,
|
||||
): Result<Evidence, AnalysisError> => {
|
||||
try {
|
||||
const generated = deriveReconstructionObligationCandidates(
|
||||
input.evidence_bundle,
|
||||
input.reviewed_obligations,
|
||||
);
|
||||
const ledger = evaluateReconstructionObligationLedger({
|
||||
candidates: generated.candidates,
|
||||
bundle: input.evidence_bundle,
|
||||
manifest: input.manifest,
|
||||
generationLimitations: generated.limitations,
|
||||
});
|
||||
return ok(createLedgerEvidence(input, ledger));
|
||||
} catch (cause: unknown) {
|
||||
if (
|
||||
cause instanceof NonCanonicalEvidenceBundleError ||
|
||||
cause instanceof DuplicateEvidenceRecordError ||
|
||||
cause instanceof InvalidResidualUnknownGraphError ||
|
||||
cause instanceof MissingReviewEvidenceError
|
||||
)
|
||||
return err(workflowInputError(OPERATION, cause));
|
||||
if (cause instanceof z.ZodError)
|
||||
return err(workflowInputError(OPERATION, cause));
|
||||
return err(
|
||||
new AnalysisProtocolError(
|
||||
"Reconstruction obligation ledger generation failed",
|
||||
{ cause },
|
||||
),
|
||||
);
|
||||
}
|
||||
/** Generate and evaluate against the same authenticated bundle and record index. */
|
||||
export const buildResolvedReconstructionObligationLedgerEvidence = (
|
||||
request: ResolvedReconstructionObligationRequest,
|
||||
): Evidence => {
|
||||
const generated = deriveReconstructionObligationCandidates(request);
|
||||
const ledger = evaluateReconstructionObligationLedger({
|
||||
candidates: generated.candidates,
|
||||
sources: request.sources,
|
||||
manifest: request.manifest,
|
||||
generationLimitations: generated.limitations,
|
||||
});
|
||||
return createLedgerEvidence(ledger);
|
||||
};
|
||||
|
||||
const createLedgerEvidence = (
|
||||
input: ReconstructionObligationLedgerInput,
|
||||
ledger: ReconstructionObligationLedger,
|
||||
): Evidence => {
|
||||
const completeLedger = reconstructionObligationLedgerSchema.parse(ledger);
|
||||
return createEvidence(undefined, JAVASCRIPT_APPLICATION_WORKFLOW_PROVIDER, {
|
||||
): Evidence =>
|
||||
createEvidence(undefined, JAVASCRIPT_APPLICATION_WORKFLOW_PROVIDER, {
|
||||
predicateType: "rea.reconstruction-obligation-ledger",
|
||||
operation: OPERATION,
|
||||
parameters: jsonObjectSchema.parse({
|
||||
ledger_id: completeLedger.ledger_id,
|
||||
closure_digest: completeLedger.closure_digest,
|
||||
}),
|
||||
result: jsonValueSchema.parse(completeLedger),
|
||||
parameters: {
|
||||
ledger_id: ledger.ledger_id,
|
||||
closure_digest: ledger.closure_digest,
|
||||
},
|
||||
result: ledger,
|
||||
rawResult: null,
|
||||
confidence: "inferred",
|
||||
authority: "analyst-inference",
|
||||
environment: null,
|
||||
limitations: completeLedger.limitations,
|
||||
evidenceLinks: completeLedger.evidence_links,
|
||||
limitations: ledger.limitations,
|
||||
evidenceLinks: ledger.evidence_links,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { z } from "zod";
|
||||
import type { z } from "zod";
|
||||
|
||||
import { AnalysisInputError } from "../domain/analysisErrorCore.js";
|
||||
import type { AnalysisInputError } from "../domain/analysisErrorCore.js";
|
||||
import { analysisInputErrorFromIssues } from "../domain/inputIssueProjection.js";
|
||||
|
||||
/** Project schema issues of the raw request onto their request paths. */
|
||||
@@ -10,25 +10,3 @@ export const requestInputError = (
|
||||
input: unknown,
|
||||
): AnalysisInputError =>
|
||||
analysisInputErrorFromIssues(operation, cause.issues, input, { cause });
|
||||
|
||||
/**
|
||||
* Keep the failed constraint when a parsed request is rejected later. Nested
|
||||
* values such as Evidence results are parsed on their own, so their issue
|
||||
* paths are relative to that value and are reported in the message, not as
|
||||
* request paths.
|
||||
*/
|
||||
export const workflowInputError = (
|
||||
operation: string,
|
||||
cause: z.ZodError | TypeError,
|
||||
): AnalysisInputError =>
|
||||
new AnalysisInputError(
|
||||
operation,
|
||||
{ cause },
|
||||
cause instanceof z.ZodError
|
||||
? cause.issues.map((issue) => ({
|
||||
path: [],
|
||||
reason: "invalid_value" as const,
|
||||
message: `A nested value failed validation at ${issue.path.length === 0 ? "its root" : issue.path.map(String).join(".")}: ${issue.message}`,
|
||||
}))
|
||||
: [{ path: [], reason: "invalid_value", message: cause.message }],
|
||||
);
|
||||
|
||||
@@ -17,7 +17,6 @@ import { compareApplicationVersionsInputSchema } from "../domain/javascript/java
|
||||
import { registerSourceToBundleCommand } from "./sourceToBundleCommand.js";
|
||||
import { compareJavaScriptExportShapesInputSchema } from "../domain/javascript/javascriptExportShapeComparisonSchemas.js";
|
||||
import { analysisInputErrorFromIssues } from "../domain/inputIssueProjection.js";
|
||||
import { reconstructionObligationLedgerInputSchema } from "../domain/reconstructionObligationLedgerSchemas.js";
|
||||
|
||||
type CliInstance = ReturnType<typeof Cli.create>;
|
||||
|
||||
@@ -141,15 +140,11 @@ const registerObligationLedgerCommand = (
|
||||
name: CLI_COMMANDS.buildReconstructionObligationLedger,
|
||||
description:
|
||||
"Generate a deterministic Evidence-backed reconstruction obligation ledger page",
|
||||
inputSchema: reconstructionObligationLedgerInputSchema,
|
||||
inputSchema: z.unknown(),
|
||||
workflow: async (input) => {
|
||||
const { buildReconstructionObligationLedgerEvidenceValidated } =
|
||||
const { buildReconstructionObligationLedgerEvidence } =
|
||||
await import("../application/ReconstructionObligationLedgerService.js");
|
||||
const result =
|
||||
buildReconstructionObligationLedgerEvidenceValidated(input);
|
||||
return result.ok
|
||||
? { ok: true, value: jsonValueSchema.parse(result.value) }
|
||||
: result;
|
||||
return buildReconstructionObligationLedgerEvidence(input);
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -2,10 +2,9 @@ import type { EvidenceMcpServer } from "../EvidenceMcpServer.js";
|
||||
import { runAdmittedToolOperation } from "../admittedToolOperation.js";
|
||||
import { recordSessionEvidenceSources } from "../sessionEvidence.js";
|
||||
|
||||
import {
|
||||
buildReconstructionObligationLedgerEvidenceValidated,
|
||||
resolveReconstructionObligationLedgerRequest,
|
||||
} from "../../application/ReconstructionObligationLedgerService.js";
|
||||
import { buildResolvedReconstructionObligationLedgerEvidence } from "../../application/ReconstructionObligationLedgerService.js";
|
||||
import { resolveReconstructionObligationLedgerRequest } from "../../application/ReconstructionObligationLedgerRequest.js";
|
||||
import { ok } from "../../domain/result.js";
|
||||
import { applicationToolContract } from "../../contracts/applicationToolContracts.js";
|
||||
import { logToolExecution } from "../toolLogging.js";
|
||||
import { toolRegistrationOptions } from "../toolRegistrationOptions.js";
|
||||
@@ -39,8 +38,10 @@ export const registerReconstructionObligationLedgerTool = (
|
||||
contract.name,
|
||||
() =>
|
||||
Promise.resolve(
|
||||
buildReconstructionObligationLedgerEvidenceValidated(
|
||||
resolved.value,
|
||||
ok(
|
||||
buildResolvedReconstructionObligationLedgerEvidence(
|
||||
resolved.value,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -48,7 +49,7 @@ export const registerReconstructionObligationLedgerTool = (
|
||||
return server.delivery.toCallToolResult(result, contract);
|
||||
const recorded = recordSessionEvidenceSources(
|
||||
options.recordEvidence,
|
||||
resolved.value.evidence_bundle.records,
|
||||
resolved.value.sources.bundle.records,
|
||||
);
|
||||
if (!recorded.ok)
|
||||
return server.delivery.toCallToolResult(recorded, contract);
|
||||
|
||||
@@ -79,18 +79,13 @@ describe("reconstruction obligation ledger CLI parity", () => {
|
||||
stdout = cause.stdout;
|
||||
}
|
||||
expect(JSON.parse(stdout)).toMatchObject({
|
||||
code: "invalid_request",
|
||||
category: "invalid_input",
|
||||
code: "evidence_integrity_mismatch",
|
||||
category: "integrity_mismatch",
|
||||
retryable: false,
|
||||
details: {
|
||||
operation: "build_reconstruction_obligation_ledger",
|
||||
issues: [
|
||||
expect.objectContaining({
|
||||
message: expect.stringContaining(
|
||||
"Evidence bundle manifests are not canonical",
|
||||
),
|
||||
}),
|
||||
],
|
||||
reason: expect.stringContaining(
|
||||
"Evidence bundle manifests are not canonical",
|
||||
),
|
||||
},
|
||||
});
|
||||
}, 20_000);
|
||||
|
||||
@@ -55,16 +55,11 @@ const withLedgerClient = async (
|
||||
}
|
||||
};
|
||||
|
||||
const inputError = (message: string) => ({
|
||||
const integrityError = (message: string) => ({
|
||||
error: {
|
||||
code: "invalid_request",
|
||||
category: "invalid_input",
|
||||
details: {
|
||||
operation: "build_reconstruction_obligation_ledger",
|
||||
issues: [
|
||||
expect.objectContaining({ message: expect.stringContaining(message) }),
|
||||
],
|
||||
},
|
||||
code: "evidence_integrity_mismatch",
|
||||
category: "integrity_mismatch",
|
||||
details: { reason: expect.stringContaining(message) },
|
||||
},
|
||||
});
|
||||
|
||||
@@ -97,7 +92,7 @@ describe("reconstruction obligation ledger residual graph input", () => {
|
||||
const result = await request(createEvidenceBundle([], [unknown]));
|
||||
expect(result.isError).toBe(true);
|
||||
expect(parseMcpToolError(result)).toMatchObject(
|
||||
inputError(
|
||||
integrityError(
|
||||
`Residual unknown references missing evidence ${mutation.evidence_id}`,
|
||||
),
|
||||
);
|
||||
@@ -110,7 +105,7 @@ describe("reconstruction obligation ledger residual graph input", () => {
|
||||
});
|
||||
|
||||
it.each(["history", "relationship", "resolution"] as const)(
|
||||
"reports residual %s constraints as caller input",
|
||||
"reports residual %s source integrity constraints",
|
||||
async (constraint) => {
|
||||
const missingId = `unk_${"a".repeat(64)}`;
|
||||
const input = recordUnknownInputSchema.parse({
|
||||
@@ -168,14 +163,14 @@ describe("reconstruction obligation ledger residual graph input", () => {
|
||||
createEvidenceBundle([mutation], unknowns),
|
||||
);
|
||||
expect(result.isError).toBe(true);
|
||||
expect(parseMcpToolError(result)).toMatchObject(inputError(reason));
|
||||
expect(parseMcpToolError(result)).toMatchObject(integrityError(reason));
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe("reconstruction obligation ledger MCP parity", () => {
|
||||
it("returns canonical bundle failures as caller input errors", async () => {
|
||||
it("rejects noncanonical source bundles before ledger generation", async () => {
|
||||
const records = ["first", "second"].map((label) =>
|
||||
createEvidence(
|
||||
undefined,
|
||||
@@ -218,22 +213,9 @@ describe("reconstruction obligation ledger MCP parity", () => {
|
||||
records: [...canonical.records].reverse(),
|
||||
});
|
||||
expect(result.isError).toBe(true);
|
||||
expect(parseMcpToolError(result)).toMatchObject({
|
||||
error: {
|
||||
code: "invalid_request",
|
||||
category: "invalid_input",
|
||||
details: {
|
||||
operation: "build_reconstruction_obligation_ledger",
|
||||
issues: [
|
||||
expect.objectContaining({
|
||||
message: expect.stringContaining(
|
||||
"Evidence bundle manifests are not canonical",
|
||||
),
|
||||
}),
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(parseMcpToolError(result)).toMatchObject(
|
||||
integrityError("Evidence bundle manifests are not canonical"),
|
||||
);
|
||||
} finally {
|
||||
await client.close();
|
||||
await server.close();
|
||||
@@ -241,7 +223,7 @@ describe("reconstruction obligation ledger MCP parity", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("returns duplicate records and missing review Evidence as caller input errors", async () => {
|
||||
it("distinguishes corrupt source bundles from missing caller-selected review references", async () => {
|
||||
const record = createEvidence(
|
||||
undefined,
|
||||
{ id: "fixture", name: "Fixture", version: "1" },
|
||||
@@ -256,17 +238,25 @@ describe("reconstruction obligation ledger MCP parity", () => {
|
||||
});
|
||||
expect(duplicate.isError).toBe(true);
|
||||
expect(parseMcpToolError(duplicate)).toMatchObject(
|
||||
inputError("Evidence bundle contains duplicate record IDs"),
|
||||
integrityError("Evidence bundle contains duplicate record IDs"),
|
||||
);
|
||||
const missing = await request(canonical, [
|
||||
reviewedObligation("obl.reviewed", missingEvidenceId),
|
||||
]);
|
||||
expect(missing.isError).toBe(true);
|
||||
expect(parseMcpToolError(missing)).toMatchObject(
|
||||
inputError(
|
||||
`Reviewed obligation Evidence is missing: ${missingEvidenceId}`,
|
||||
),
|
||||
);
|
||||
expect(parseMcpToolError(missing)).toMatchObject({
|
||||
error: {
|
||||
code: "invalid_request",
|
||||
details: {
|
||||
issues: [
|
||||
expect.objectContaining({
|
||||
path: ["reviewed_obligations", 0, "review_evidence_ids", 0],
|
||||
message: `Reviewed obligation Evidence is missing: ${missingEvidenceId}`,
|
||||
}),
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -30,6 +30,25 @@ import { compareManagedMemberPaths } from "../../src/application/managed/Managed
|
||||
import { buildManagedPeFixture } from "../../src/dotnet/ManagedPe.fixture.js";
|
||||
import { createTestTempDirectory } from "../fixtures/temporaryDirectory.js";
|
||||
|
||||
import { reconstructionObligationLedgerSchema } from "../../src/domain/reconstructionObligationLedgerSchemas.js";
|
||||
import {
|
||||
request as ledgerRequest,
|
||||
processEvidence,
|
||||
} from "../../src/application/ReconstructionObligationLedger.fixture.js";
|
||||
import { processCaptureSchema } from "../../src/domain/process/processCapture.js";
|
||||
|
||||
/** Unexpected implementation failures retain the SDK text surface, without a typed caller error. */
|
||||
const expectUnexpectedToolFailure = (
|
||||
response: Awaited<ReturnType<Client["callTool"]>>,
|
||||
failure: Error,
|
||||
): void => {
|
||||
expect(response).toMatchObject({
|
||||
isError: true,
|
||||
content: [{ type: "text", text: failure.message }],
|
||||
});
|
||||
expect(response.structuredContent).toBeUndefined();
|
||||
};
|
||||
|
||||
const unexpectedResultFailures = [
|
||||
["TypeError", () => new TypeError("injected result parser TypeError")],
|
||||
["Error", () => new Error("injected result parser Error")],
|
||||
@@ -226,11 +245,7 @@ describe("derived operation failures through the public SDK", () => {
|
||||
},
|
||||
});
|
||||
|
||||
expect(response).toMatchObject({
|
||||
isError: true,
|
||||
content: [{ type: "text", text: failure.message }],
|
||||
});
|
||||
expect(response.structuredContent).toBeUndefined();
|
||||
expectUnexpectedToolFailure(response, failure);
|
||||
expect(session.exportEvidenceBundle().records).toEqual([]);
|
||||
});
|
||||
},
|
||||
@@ -251,11 +266,7 @@ describe("derived operation failures through the public SDK", () => {
|
||||
name: "evaluate_reconstruction_coverage",
|
||||
arguments: { coverage, boundary_id: "replacement.cli" },
|
||||
});
|
||||
expect(response).toMatchObject({
|
||||
isError: true,
|
||||
content: [{ type: "text", text: failure.message }],
|
||||
});
|
||||
expect(response.structuredContent).toBeUndefined();
|
||||
expectUnexpectedToolFailure(response, failure);
|
||||
expect(session.exportEvidenceBundle().records).toEqual([]);
|
||||
});
|
||||
},
|
||||
@@ -366,11 +377,7 @@ describe.each(managedWorkflows)(
|
||||
name: workflow.name,
|
||||
arguments: workflow.input,
|
||||
});
|
||||
expect(response).toMatchObject({
|
||||
isError: true,
|
||||
content: [{ type: "text", text: failure.message }],
|
||||
});
|
||||
expect(response.structuredContent).toBeUndefined();
|
||||
expectUnexpectedToolFailure(response, failure);
|
||||
expect(session.exportEvidenceBundle().records).toEqual([]);
|
||||
});
|
||||
},
|
||||
@@ -416,3 +423,48 @@ describe("managed comparison output failure with completed source reads", () =>
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("ledger generation failure ownership through the public SDK", () => {
|
||||
afterEach(() => vi.restoreAllMocks());
|
||||
|
||||
it.each(unexpectedResultFailures)(
|
||||
"preserves an unexpected %s from generated ledger validation",
|
||||
async (_label, createFailure) => {
|
||||
await withSdk(async ({ client, session }) => {
|
||||
const input = ledgerRequest([processEvidence()]);
|
||||
const failure = createFailure();
|
||||
vi.spyOn(
|
||||
reconstructionObligationLedgerSchema,
|
||||
"parse",
|
||||
).mockImplementation(() => {
|
||||
throw failure;
|
||||
});
|
||||
const response = await client.callTool({
|
||||
name: "build_reconstruction_obligation_ledger",
|
||||
arguments: input,
|
||||
});
|
||||
expectUnexpectedToolFailure(response, failure);
|
||||
expect(session.exportEvidenceBundle().records).toEqual([]);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it.each(unexpectedResultFailures.filter(([kind]) => kind !== "ZodError"))(
|
||||
"preserves an unexpected %s while deriving process candidates",
|
||||
async (_label, createFailure) => {
|
||||
await withSdk(async ({ client, session }) => {
|
||||
const input = ledgerRequest([processEvidence()]);
|
||||
const failure = createFailure();
|
||||
vi.spyOn(processCaptureSchema, "parse").mockImplementation(() => {
|
||||
throw failure;
|
||||
});
|
||||
const response = await client.callTool({
|
||||
name: "build_reconstruction_obligation_ledger",
|
||||
arguments: input,
|
||||
});
|
||||
expectUnexpectedToolFailure(response, failure);
|
||||
expect(session.exportEvidenceBundle().records).toEqual([]);
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user