feat(reconstruction): add obligation ledgers

This commit is contained in:
morluto
2026-07-24 13:17:56 +00:00
parent 38c883bbc8
commit 9d48ca788c
14 changed files with 2581 additions and 0 deletions
@@ -0,0 +1,70 @@
import type { Evidence } from "../domain/evidence.js";
import type { ReconstructionObligation } from "../domain/reconstructionObligationLedgerSchemas.js";
type ProofAuthority = ReconstructionObligation["required_fixture_authority"];
type OriginalAuthority =
ReconstructionObligation["required_original_authority"];
/** Return whether an actual reconstruction proof is comparable to a requirement. */
export const proofAuthoritySatisfies = (
actual: ProofAuthority,
required: ProofAuthority,
): boolean => {
const accepted: Readonly<Record<ProofAuthority, readonly ProofAuthority[]>> =
{
unit: [
"unit",
"integration",
"protocol",
"renderer",
"packaged-process",
"native-abi",
"live-observation",
"external",
],
integration: [
"integration",
"protocol",
"renderer",
"packaged-process",
"native-abi",
"live-observation",
],
protocol: ["protocol", "packaged-process", "live-observation"],
renderer: ["renderer", "packaged-process", "live-observation"],
"packaged-process": ["packaged-process", "live-observation"],
"native-abi": ["native-abi"],
"live-observation": ["live-observation"],
external: ["external"],
};
return accepted[required].includes(actual);
};
/** Check that Evidence can authenticate the declared reconstruction authority. */
export const evidenceAuthoritySupportsProof = (
evidence: Evidence,
authority: ProofAuthority,
): boolean =>
evidence.confidence === "observed" &&
(authority === "external"
? evidence.authority === "external-service"
: ["shipped-artifact", "controlled-replay"].includes(evidence.authority));
/** Check that Evidence observes the original at the required boundary. */
export const evidenceSupportsOriginalAuthority = (
evidence: Evidence,
authority: OriginalAuthority,
): boolean => {
if (evidence.confidence !== "observed") return false;
if (authority === "static") return evidence.authority === "shipped-artifact";
if (authority === "runtime")
return ["controlled-replay", "external-service"].includes(
evidence.authority,
);
if (authority === "process")
return (
evidence.operation === "capture_process_scenario" &&
evidence.predicate_type === "rea.process-capture/v4"
);
return evidence.authority === "external-service";
};
@@ -0,0 +1,416 @@
import { createHash } from "node:crypto";
import canonicalize from "canonicalize";
import type { Evidence } from "../domain/evidence.js";
import {
parseEvidenceBundle,
type EvidenceBundle,
} from "../domain/evidenceBundle.js";
import { parseProcessCapture } from "../domain/processCapture.js";
import type {
ReconstructionObligation,
ReviewedReconstructionObligation,
} from "../domain/reconstructionObligationLedgerSchemas.js";
import { parseApplicationGraphEvidence } from "./JavaScriptApplicationEvidenceGraph.js";
import {
applicationObligationPolicy,
semanticObligationPolicy,
type ReconstructionObligationCandidatePolicy,
} from "./ReconstructionObligationPolicies.js";
type CandidateFields = Omit<
ReconstructionObligation,
"binding" | "status" | "diagnostics"
>;
/** Internal generated or reviewed obligation before manifest closure evaluation. */
export interface ReconstructionObligationCandidate extends CandidateFields {
readonly definitionCount: number;
readonly disposition: "active" | "blocked" | "out-of-scope";
}
export interface ReconstructionObligationCandidateSet {
readonly candidates: readonly ReconstructionObligationCandidate[];
readonly limitations: readonly string[];
}
/** Derive conservative candidates from graph, process, and reviewed inputs. */
export const deriveReconstructionObligationCandidates = (
bundleInput: EvidenceBundle,
reviewed: readonly ReviewedReconstructionObligation[],
): ReconstructionObligationCandidateSet => {
const bundle = parseEvidenceBundle(bundleInput);
const records = new Map(
bundle.records.map((record) => [record.evidence_id, record]),
);
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)
addCandidate(candidates, reviewedCandidate(obligation, records));
return {
candidates: [...candidates.values()].sort((left, right) =>
left.obligation_id.localeCompare(right.obligation_id),
),
limitations: [...limitations].sort(),
};
};
const deriveApplicationCandidates = (
evidence: Evidence,
records: ReadonlyMap<string, Evidence>,
candidates: Map<string, ReconstructionObligationCandidate>,
limitations: Set<string>,
): void => {
if (
![
"analyze_javascript_application",
"reconcile_javascript_runtime",
"project_managed_application_graph",
].includes(evidence.operation)
)
return;
try {
const source = parseApplicationGraphEvidence(evidence);
for (const node of source.graph.nodes) {
const candidatePolicy = applicationObligationPolicy(node.kind);
if (candidatePolicy === undefined) 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 ?? `${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)
for (const node of source.semanticGraph.nodes) {
const candidatePolicy = semanticObligationPolicy(node.kind);
if (candidatePolicy === undefined) continue;
addCandidate(
candidates,
generatedCandidate({
key: `semantic:${node.node_id}:${candidatePolicy.family}`,
title: node.label ?? `${node.kind} semantic boundary`,
policy: candidatePolicy,
artifactSha256: node.identity.artifact_sha256,
applicationNodeId: node.application_node_ids[0] ?? null,
semanticNodeId: node.node_id,
location: `semantic-graph:${source.semanticGraph.graph_id}/nodes/${node.node_id}`,
authorityReferences: [
graphReference(
evidence,
records,
node.evidence,
`${source.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: ${errorMessage(cause)}`,
);
}
};
const deriveProcessCandidate = (
evidence: Evidence,
candidates: Map<string, ReconstructionObligationCandidate>,
limitations: Set<string>,
): void => {
if (
evidence.operation !== "capture_process_scenario" ||
evidence.predicate_type !== "rea.process-capture/v4"
)
return;
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",
},
]
: []),
];
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.`,
);
} catch (cause: unknown) {
limitations.add(
`Process Evidence ${evidence.evidence_id} could not generate candidates: ${errorMessage(cause)}`,
);
}
};
interface GeneratedCandidateInput {
readonly key: string;
readonly title: string;
readonly policy: ReconstructionObligationCandidatePolicy;
readonly artifactSha256: string;
readonly applicationNodeId: string | null;
readonly semanticNodeId: string | null;
readonly location: string;
readonly authorityReferences: ReconstructionObligation["authority_references"];
}
const generatedCandidate = ({
key,
title,
policy: candidatePolicy,
artifactSha256,
applicationNodeId,
semanticNodeId,
location,
authorityReferences,
}: GeneratedCandidateInput): ReconstructionObligationCandidate => ({
obligation_id: obligationId(key),
obligation_version: 1,
title: title.slice(0, 500),
origin: "generated",
application_layer: candidatePolicy.applicationLayer,
family: candidatePolicy.family,
target: {
artifact_sha256: artifactSha256,
application_node_id: applicationNodeId,
semantic_node_id: semanticNodeId,
location,
},
authority_references: authorityReferences,
source_state: sourceState(authorityReferences),
observed_cases: [],
required: true,
required_case_kinds: candidatePolicy.cases,
required_original_authority: candidatePolicy.originalAuthority,
required_fixture_authority: candidatePolicy.fixtureAuthority,
required_verifier_authority: candidatePolicy.verifierAuthority,
requires_parser_type: candidatePolicy.requiresParserType,
dependency_obligation_ids: [],
residual_unknown_ids: [],
unavailable_authority: [],
required_next_evidence: [
`Obtain ${candidatePolicy.originalAuthority} authority for this finite ${candidatePolicy.family} claim.`,
],
definitionCount: 1,
disposition: "active",
});
const reviewedCandidate = (
obligation: ReviewedReconstructionObligation,
records: ReadonlyMap<string, Evidence>,
): ReconstructionObligationCandidate => {
const {
review_evidence_ids: reviewEvidenceIds,
disposition,
...definition
} = obligation;
return {
...definition,
origin: "reviewed",
authority_references: reviewEvidenceIds.map((evidenceId) => {
const evidence = records.get(evidenceId);
if (evidence === undefined)
throw new TypeError(
`Reviewed obligation Evidence is missing: ${evidenceId}`,
);
return {
evidence_id: evidenceId,
authority: evidence.authority,
state: reviewedEvidenceState(evidence),
location: `/evidence/${evidenceId}`,
};
}),
source_state: "reviewed",
observed_cases: [],
definitionCount: 1,
disposition,
};
};
const graphReference = (
envelope: Evidence,
records: ReadonlyMap<string, Evidence>,
graphEvidence: {
readonly authority: string;
readonly state: string;
readonly evidence_ids: readonly string[];
},
location: string,
): ReconstructionObligation["authority_references"][number] => {
const linkedId =
graphEvidence.evidence_ids.find((evidenceId) => records.has(evidenceId)) ??
envelope.evidence_id;
const linked = records.get(linkedId) ?? envelope;
const runtimeAuthority = [
"passive-cdp-runtime",
"controlled-replay",
].includes(graphEvidence.authority);
return {
evidence_id: linked.evidence_id,
authority: linked.authority,
state:
runtimeAuthority && graphEvidence.state === "observed"
? "observed"
: graphEvidence.state === "unknown" ||
graphEvidence.state === "unavailable"
? "unknown"
: "candidate",
location,
};
};
const reviewedEvidenceState = (
evidence: Evidence,
): ReconstructionObligation["authority_references"][number]["state"] => {
if (
evidence.operation === "capture_process_scenario" &&
evidence.predicate_type === "rea.process-capture/v4" &&
evidence.confidence === "observed"
)
return "observed";
if (
evidence.authority === "external-service" &&
evidence.confidence === "observed"
)
return "observed";
if (evidence.authority === "shipped-artifact") return "candidate";
return "reviewed";
};
const addCandidate = (
candidates: Map<string, ReconstructionObligationCandidate>,
candidate: ReconstructionObligationCandidate,
): void => {
const current = candidates.get(candidate.obligation_id);
if (current === undefined) {
candidates.set(candidate.obligation_id, candidate);
return;
}
candidates.set(candidate.obligation_id, {
...current,
authority_references: uniqueBy(
[...current.authority_references, ...candidate.authority_references],
(reference) =>
`${reference.evidence_id}:${reference.state}:${reference.location}`,
),
observed_cases: uniqueBy(
[...current.observed_cases, ...candidate.observed_cases],
(item) => `${item.kind}:${item.evidence_id}:${item.location}`,
),
source_state: sourceState([
...current.authority_references,
...candidate.authority_references,
]),
definitionCount: current.definitionCount + candidate.definitionCount,
});
};
const sourceState = (
references: ReconstructionObligation["authority_references"],
): ReconstructionObligation["source_state"] => {
if (references.some(({ state }) => state === "observed")) return "observed";
if (references.some(({ state }) => state === "candidate")) return "candidate";
if (references.some(({ state }) => state === "reviewed")) return "reviewed";
return "unknown";
};
const uniqueBy = <Value>(
values: readonly Value[],
key: (value: Value) => string,
): Value[] =>
[...new Map(values.map((value) => [key(value), value])).values()].sort(
(left, right) => key(left).localeCompare(key(right)),
);
const obligationId = (key: string): string =>
`obl_${digest({ schema: "rea.reconstruction-obligation/v1", key })}`;
const digest = (value: unknown): string => {
const encoded = canonicalize(value);
if (encoded === undefined)
throw new TypeError("Reconstruction obligation key is not canonical JSON");
return createHash("sha256").update(encoded).digest("hex");
};
const errorMessage = (cause: unknown): string =>
cause instanceof Error ? cause.message : "unknown validation failure";
@@ -0,0 +1,87 @@
import type {
ReconstructionObligation,
ReconstructionObligationManifest,
} from "../domain/reconstructionObligationLedgerSchemas.js";
import type { Evidence } from "../domain/evidence.js";
type Diagnostic = ReconstructionObligation["diagnostics"][number];
type Binding = NonNullable<ReconstructionObligation["binding"]>;
export const groupManifestBindings = (
manifest: ReconstructionObligationManifest,
): ReadonlyMap<string, readonly Binding[]> => {
const grouped = new Map<string, Binding[]>();
for (const binding of manifest.bindings)
grouped.set(binding.obligation_id, [
...(grouped.get(binding.obligation_id) ?? []),
binding,
]);
return grouped;
};
export const groupManifestContradictions = (
manifest: ReconstructionObligationManifest,
): ReadonlyMap<string, readonly string[]> => {
const grouped = new Map<string, string[]>();
for (const contradiction of manifest.contradictions)
grouped.set(contradiction.obligation_id, [
...(grouped.get(contradiction.obligation_id) ?? []),
...contradiction.evidence_ids,
]);
return grouped;
};
export const addObligationDiagnostic = (
diagnostics: Diagnostic[],
code: Diagnostic["code"],
detail: string,
): void => {
diagnostics.push({ code, detail });
};
export const uniqueObligationDiagnostics = (
diagnostics: readonly Diagnostic[],
): Diagnostic[] =>
[
...new Map(
diagnostics.map((diagnostic) => [
`${diagnostic.code}:${diagnostic.detail}`,
diagnostic,
]),
).values(),
].sort((left, right) =>
`${left.code}:${left.detail}`.localeCompare(
`${right.code}:${right.detail}`,
),
);
export const hasObligationDiagnostic = (
diagnostics: readonly Diagnostic[],
code: Diagnostic["code"],
): boolean => diagnostics.some((diagnostic) => diagnostic.code === code);
export const contradictionDiagnostics = (
obligationId: string,
contradictions: ReadonlyMap<string, readonly string[]>,
evidenceById: ReadonlyMap<string, Evidence>,
): Diagnostic[] => {
const evidenceIds = contradictions.get(obligationId) ?? [];
if (evidenceIds.length === 0) return [];
const missing = evidenceIds.filter(
(evidenceId) => !evidenceById.has(evidenceId),
);
return missing.length > 0
? [
{
code: "contradiction-evidence-missing",
detail: `Contradiction references missing Evidence: ${missing.join(", ")}.`,
},
]
: [
{
code: "contradiction",
detail:
"Authenticated contradictory Evidence reopens this obligation.",
},
];
};
@@ -0,0 +1,515 @@
import type { Evidence } from "../domain/evidence.js";
import {
parseEvidenceBundle,
type EvidenceBundle,
} from "../domain/evidenceBundle.js";
import {
reconstructionObligationLedgerSchema,
type ReconstructionObligation,
type ReconstructionObligationLedger,
type ReconstructionObligationManifest,
} from "../domain/reconstructionObligationLedgerSchemas.js";
import type { ReconstructionObligationCandidate } from "./ReconstructionObligationCandidates.js";
import {
evidenceAuthoritySupportsProof,
evidenceSupportsOriginalAuthority,
proofAuthoritySatisfies,
} from "./ReconstructionObligationAuthority.js";
import {
addObligationDiagnostic as addDiagnostic,
contradictionDiagnostics,
groupManifestBindings,
groupManifestContradictions,
hasObligationDiagnostic as hasDiagnostic,
uniqueObligationDiagnostics as uniqueDiagnostics,
} from "./ReconstructionObligationLedgerDiagnostics.js";
import {
originalCaseDiagnostics,
originalCasesFor,
} from "./ReconstructionObligationOriginalCases.js";
import {
digestObligationLedgerValue,
obligationEdgeOrder,
obligationLedgerEvidenceLinks,
obligationLedgerStatus,
obligationReports,
summarizeObligations,
} from "./ReconstructionObligationLedgerProjection.js";
type Diagnostic = ReconstructionObligation["diagnostics"][number];
type Binding = NonNullable<ReconstructionObligation["binding"]>;
interface EvaluationContext {
readonly candidates: ReadonlyMap<string, ReconstructionObligationCandidate>;
readonly evidence: ReadonlyMap<string, Evidence>;
readonly bindings: ReadonlyMap<string, readonly Binding[]>;
readonly contradictions: ReadonlyMap<string, readonly string[]>;
}
/** Evaluate candidates and manifest bindings into one canonical fail-closed ledger. */
export const evaluateReconstructionObligationLedger = ({
candidates: candidatesInput,
bundle: bundleInput,
manifest,
maxObligations,
generationLimitations,
}: {
readonly candidates: readonly ReconstructionObligationCandidate[];
readonly bundle: EvidenceBundle;
readonly manifest: ReconstructionObligationManifest;
readonly maxObligations: number;
readonly generationLimitations: readonly string[];
}): ReconstructionObligationLedger => {
const bundle = parseEvidenceBundle(bundleInput);
const allCandidates = [...candidatesInput].sort((left, right) =>
left.obligation_id.localeCompare(right.obligation_id),
);
const orphanManifestIds = findOrphanManifestIds(allCandidates, manifest);
const candidates = allCandidates.slice(0, maxObligations);
const omittedCount = allCandidates.length - candidates.length;
const context = createEvaluationContext(candidates, bundle, manifest);
const preliminary = candidates.map((candidate) =>
evaluateObligation(candidate, context),
);
const obligations = applyDependencyDiagnostics(preliminary, context);
const summary = summarizeObligations(obligations);
const reports = obligationReports(obligations);
const ownershipGraph = obligations
.flatMap((obligation) =>
obligation.binding === null
? []
: [
{
obligation_id: obligation.obligation_id,
module_path: obligation.binding.owner.module_path,
symbol: obligation.binding.owner.symbol,
},
],
)
.sort(obligationEdgeOrder);
const dependencyGraph = obligations
.flatMap((obligation) =>
obligation.dependency_obligation_ids.map((dependencyId) => ({
obligation_id: obligation.obligation_id,
depends_on_obligation_id: dependencyId,
})),
)
.sort(obligationEdgeOrder);
const evidenceLinks = obligationLedgerEvidenceLinks(obligations, manifest);
const limitations = ledgerLimitations(
generationLimitations,
allCandidates.length,
omittedCount,
orphanManifestIds,
);
const coverage = coverageFor(
omittedCount,
maxObligations,
limitations.length > 0,
);
const semantic = {
coverage,
summary,
reports,
ownership_graph: ownershipGraph,
dependency_graph: dependencyGraph,
obligations,
evidence_links: evidenceLinks,
limitations,
};
const evaluatedStatus = obligationLedgerStatus(obligations, omittedCount);
const status =
evaluatedStatus === "failed"
? evaluatedStatus
: limitations.length > 0
? "unknown"
: evaluatedStatus;
const closureDigest = digestObligationLedgerValue({
schema: "rea.reconstruction-obligation-closure/v1",
status,
...semantic,
});
return reconstructionObligationLedgerSchema.parse({
schema: "ReconstructionObligationLedger",
schema_version: 1,
ledger_id: `rol_${digestObligationLedgerValue({
schema: "rea.reconstruction-obligation-ledger/v1",
evidence_ids: bundle.records.map(({ evidence_id: id }) => id),
obligation_ids: obligations.map(({ obligation_id: id }) => id),
closure_digest: closureDigest,
})}`,
closure_digest: closureDigest,
status,
...semantic,
});
};
const createEvaluationContext = (
candidates: readonly ReconstructionObligationCandidate[],
bundle: EvidenceBundle,
manifest: ReconstructionObligationManifest,
): EvaluationContext => ({
candidates: new Map(
candidates.map((candidate) => [candidate.obligation_id, candidate]),
),
evidence: new Map(
bundle.records.map((evidence) => [evidence.evidence_id, evidence]),
),
bindings: groupManifestBindings(manifest),
contradictions: groupManifestContradictions(manifest),
});
const findOrphanManifestIds = (
candidates: readonly ReconstructionObligationCandidate[],
manifest: ReconstructionObligationManifest,
): string[] => {
const candidateIds = new Set(
candidates.map(({ obligation_id: obligationId }) => obligationId),
);
return [
...new Set(
[
...manifest.bindings.map(
({ obligation_id: obligationId }) => obligationId,
),
...manifest.contradictions.map(
({ obligation_id: obligationId }) => obligationId,
),
].filter((obligationId) => !candidateIds.has(obligationId)),
),
].sort();
};
const ledgerLimitations = (
generationLimitations: readonly string[],
candidateCount: number,
omittedCount: number,
orphanManifestIds: readonly string[],
): string[] =>
[
...new Set([
...generationLimitations,
...(candidateCount === 0
? [
"No reconstruction obligations were derived; closure cannot be claimed.",
]
: []),
...(omittedCount > 0
? [
`${String(omittedCount)} obligation candidates were omitted by max_obligations; closure is unknown.`,
]
: []),
...(orphanManifestIds.length > 0
? [
`Manifest entries reference unknown obligations: ${orphanManifestIds.join(", ")}.`,
]
: []),
]),
].sort();
const coverageFor = (
omittedCount: number,
maxObligations: number,
incomplete: boolean,
): ReconstructionObligationLedger["coverage"] => ({
status: incomplete ? "partial" : "complete",
truncated: omittedCount > 0,
omitted_count: omittedCount,
max_obligations: maxObligations,
});
const evaluateObligation = (
candidate: ReconstructionObligationCandidate,
context: EvaluationContext,
): ReconstructionObligation => {
const bindings = context.bindings.get(candidate.obligation_id) ?? [];
const binding = bindings.length === 1 ? (bindings[0] ?? null) : null;
const diagnostics: Diagnostic[] = [];
if (candidate.definitionCount > 1)
addDiagnostic(
diagnostics,
"duplicate-definition",
"Multiple candidate definitions share this obligation ID.",
);
if (bindings.length === 0)
addDiagnostic(
diagnostics,
"missing-owner",
"No reconstruction manifest binding owns this obligation.",
);
if (bindings.length > 1)
addDiagnostic(
diagnostics,
"ambiguous-owner",
"Multiple reconstruction manifest bindings claim this obligation.",
);
if (binding !== null)
evaluateBinding(candidate, binding, context, diagnostics);
diagnostics.push(
...originalCaseDiagnostics(candidate, binding, context.evidence),
);
evaluateAuthority(candidate, binding, context, diagnostics);
evaluateDisposition(candidate, diagnostics);
diagnostics.push(
...contradictionDiagnostics(
candidate.obligation_id,
context.contradictions,
context.evidence,
),
);
for (const unknownId of candidate.residual_unknown_ids)
addDiagnostic(
diagnostics,
"residual-unknown",
`Residual unknown remains visible: ${unknownId}.`,
);
for (const unavailable of candidate.unavailable_authority)
addDiagnostic(diagnostics, "unavailable-authority", unavailable);
return {
obligation_id: candidate.obligation_id,
obligation_version: candidate.obligation_version,
title: candidate.title,
origin: candidate.origin,
application_layer: candidate.application_layer,
family: candidate.family,
target: candidate.target,
authority_references: candidate.authority_references,
source_state: candidate.source_state,
observed_cases: originalCasesFor(candidate, binding),
required: candidate.required,
required_case_kinds: candidate.required_case_kinds,
required_original_authority: candidate.required_original_authority,
required_fixture_authority: candidate.required_fixture_authority,
required_verifier_authority: candidate.required_verifier_authority,
requires_parser_type: candidate.requires_parser_type,
dependency_obligation_ids: candidate.dependency_obligation_ids,
residual_unknown_ids: candidate.residual_unknown_ids,
unavailable_authority: candidate.unavailable_authority,
required_next_evidence: candidate.required_next_evidence,
binding,
status: obligationStatus(candidate, binding, diagnostics),
diagnostics: uniqueDiagnostics(diagnostics),
};
};
const evaluateBinding = (
candidate: ReconstructionObligationCandidate,
binding: Binding,
context: EvaluationContext,
diagnostics: Diagnostic[],
): void => {
if (candidate.requires_parser_type && binding.parser_type === null)
addDiagnostic(
diagnostics,
"missing-parser-type",
"This boundary requires an explicit parser, schema, and domain type.",
);
for (const caseKind of candidate.required_case_kinds) {
const fixtures = binding.fixtures.filter(
({ case_kind: fixtureCase }) => fixtureCase === caseKind,
);
if (fixtures.length === 0) {
addDiagnostic(
diagnostics,
"missing-case",
`Required ${caseKind} fixture is missing.`,
);
continue;
}
if (
!fixtures.some(
(fixture) =>
proofAuthoritySatisfies(
fixture.authority,
candidate.required_fixture_authority,
) &&
fixture.evidence_ids.every((id) => {
const evidence = context.evidence.get(id);
return (
evidence !== undefined &&
evidenceAuthoritySupportsProof(evidence, fixture.authority)
);
}),
)
)
addDiagnostic(
diagnostics,
"weak-fixture-authority",
`${caseKind} fixture lacks ${candidate.required_fixture_authority} authority or authenticated Evidence.`,
);
}
evaluateVerifier(candidate, binding, context, diagnostics);
};
const evaluateVerifier = (
candidate: ReconstructionObligationCandidate,
binding: Binding,
context: EvaluationContext,
diagnostics: Diagnostic[],
): void => {
const verifier = binding.verifier;
if (verifier === null) {
addDiagnostic(
diagnostics,
"missing-verifier",
"No verifier claim is bound to this obligation.",
);
return;
}
if (!verifier.enumerated_obligation_ids.includes(candidate.obligation_id))
addDiagnostic(
diagnostics,
"verifier-does-not-enumerate",
"The verifier claim does not enumerate this obligation ID.",
);
const resultEvidence = context.evidence.get(verifier.result_evidence_id);
if (resultEvidence === undefined)
addDiagnostic(
diagnostics,
"missing-verifier-result",
"The latest verifier result Evidence is absent from the bundle.",
);
if (
!proofAuthoritySatisfies(
verifier.authority,
candidate.required_verifier_authority,
) ||
(resultEvidence !== undefined &&
!evidenceAuthoritySupportsProof(resultEvidence, verifier.authority))
)
addDiagnostic(
diagnostics,
"weak-verifier-authority",
`Verifier authority does not satisfy ${candidate.required_verifier_authority}.`,
);
if (verifier.status !== "pass")
addDiagnostic(
diagnostics,
"verifier-failed",
`Latest verifier status is ${verifier.status}.`,
);
};
const evaluateAuthority = (
candidate: ReconstructionObligationCandidate,
binding: Binding | null,
context: EvaluationContext,
diagnostics: Diagnostic[],
): void => {
const referenceSatisfied = candidate.authority_references.some(
(reference) => {
const evidence = context.evidence.get(reference.evidence_id);
return (
evidence !== undefined &&
(candidate.required_original_authority === "static"
? ["candidate", "observed"].includes(reference.state)
: reference.state === "observed") &&
evidenceSupportsOriginalAuthority(
evidence,
candidate.required_original_authority,
)
);
},
);
const caseSatisfied = originalCasesFor(candidate, binding).some(
({ evidence_id: evidenceId }) => {
const evidence = context.evidence.get(evidenceId);
return (
evidence !== undefined &&
evidenceSupportsOriginalAuthority(
evidence,
candidate.required_original_authority,
)
);
},
);
const satisfied = referenceSatisfied || caseSatisfied;
if (!satisfied)
addDiagnostic(
diagnostics,
"static-only",
`No ${candidate.required_original_authority} authority characterizes the original finite behavior.`,
);
};
const evaluateDisposition = (
candidate: ReconstructionObligationCandidate,
diagnostics: Diagnostic[],
): void => {
if (candidate.disposition === "blocked")
addDiagnostic(
diagnostics,
"blocked",
"Reviewed disposition marks this obligation blocked.",
);
if (candidate.disposition === "out-of-scope")
addDiagnostic(
diagnostics,
"out-of-scope",
"Reviewed disposition marks this obligation out of scope; it remains visible.",
);
};
const applyDependencyDiagnostics = (
obligations: readonly ReconstructionObligation[],
context: EvaluationContext,
): ReconstructionObligation[] => {
const statuses = new Map(
obligations.map((obligation) => [
obligation.obligation_id,
obligation.status,
]),
);
return obligations.map((obligation) => {
const diagnostics = [...obligation.diagnostics];
for (const dependencyId of obligation.dependency_obligation_ids) {
if (!context.candidates.has(dependencyId))
addDiagnostic(
diagnostics,
"dependency-missing",
`Dependency obligation is missing: ${dependencyId}.`,
);
else if (statuses.get(dependencyId) !== "verified")
addDiagnostic(
diagnostics,
"dependency-open",
`Dependency obligation is not verified: ${dependencyId}.`,
);
}
const normalized = uniqueDiagnostics(diagnostics);
return {
...obligation,
status: dependencyStatus(obligation.status, normalized),
diagnostics: normalized,
};
});
};
const obligationStatus = (
candidate: ReconstructionObligationCandidate,
binding: Binding | null,
diagnostics: readonly Diagnostic[],
): ReconstructionObligation["status"] => {
if (hasDiagnostic(diagnostics, "out-of-scope")) return "out-of-scope";
if (hasDiagnostic(diagnostics, "blocked")) return "blocked";
if (hasDiagnostic(diagnostics, "contradiction")) return "contradicted";
if (
hasDiagnostic(diagnostics, "duplicate-definition") ||
hasDiagnostic(diagnostics, "residual-unknown") ||
hasDiagnostic(diagnostics, "unavailable-authority")
)
return "unknown";
if (binding === null) return "unowned";
if (diagnostics.length === 0) return "verified";
return "implemented";
};
const dependencyStatus = (
status: ReconstructionObligation["status"],
diagnostics: readonly Diagnostic[],
): ReconstructionObligation["status"] =>
(hasDiagnostic(diagnostics, "dependency-missing") ||
hasDiagnostic(diagnostics, "dependency-open")) &&
!["contradicted", "out-of-scope"].includes(status)
? "blocked"
: status;
@@ -0,0 +1,153 @@
import { createHash } from "node:crypto";
import canonicalize from "canonicalize";
import type {
ReconstructionObligation,
ReconstructionObligationLedger,
ReconstructionObligationManifest,
} from "../domain/reconstructionObligationLedgerSchemas.js";
type DiagnosticCode = ReconstructionObligation["diagnostics"][number]["code"];
export const summarizeObligations = (
obligations: readonly ReconstructionObligation[],
): ReconstructionObligationLedger["summary"] => ({
total: obligations.length,
required: obligations.filter(({ required }) => required).length,
verified: obligations.filter(({ status }) => status === "verified").length,
required_open: obligations.filter(
({ required, status }) => required && status !== "verified",
).length,
by_status: countBy(
obligations,
[
"unowned",
"characterized",
"implemented",
"verified",
"contradicted",
"blocked",
"out-of-scope",
"unknown",
],
({ status }) => status,
),
by_application_layer: countBy(
obligations,
[
"cli",
"protocol",
"electron",
"persistence",
"process",
"runtime",
"packaging",
"native-abi",
"application",
"other",
],
({ application_layer: layer }) => layer,
),
by_evidence_authority: countBy(
obligations,
["candidate", "observed", "reviewed", "unknown"],
({ source_state: state }) => state,
),
});
export const obligationReports = (
obligations: readonly ReconstructionObligation[],
): ReconstructionObligationLedger["reports"] => ({
missing_owner_obligation_ids: obligationIdsWith(obligations, "missing-owner"),
missing_verifier_obligation_ids: obligationIdsWith(
obligations,
"missing-verifier",
),
contradicted_obligation_ids: obligationIdsWith(obligations, "contradiction"),
residual_unknown_ids: [
...new Set(obligations.flatMap(({ residual_unknown_ids: ids }) => ids)),
].sort(),
});
export const obligationLedgerEvidenceLinks = (
obligations: readonly ReconstructionObligation[],
manifest: ReconstructionObligationManifest,
): string[] =>
[
...new Set([
...obligations.flatMap(({ authority_references: references }) =>
references.map(({ evidence_id: id }) => id),
),
...manifest.bindings.flatMap((binding) => [
...binding.original_cases.map(({ evidence_id: id }) => id),
...binding.fixtures.flatMap(({ evidence_ids: ids }) => ids),
...(binding.verifier === null
? []
: [binding.verifier.result_evidence_id]),
]),
...manifest.contradictions.flatMap(({ evidence_ids: ids }) => ids),
]),
].sort();
export const obligationLedgerStatus = (
obligations: readonly ReconstructionObligation[],
omittedCount: number,
): ReconstructionObligationLedger["status"] => {
if (obligations.length === 0) return "unknown";
const requiredOpen = obligations.filter(
({ required, status }) => required && status !== "verified",
);
if (
requiredOpen.some(
({ status, diagnostics }) =>
status === "contradicted" ||
hasDiagnostic(diagnostics, "verifier-failed"),
)
)
return "failed";
if (
omittedCount > 0 ||
requiredOpen.some(({ status }) => status === "unknown")
)
return "unknown";
return requiredOpen.length === 0 ? "ready" : "open";
};
export const obligationEdgeOrder = (
left: { readonly obligation_id: string },
right: { readonly obligation_id: string },
): number => JSON.stringify(left).localeCompare(JSON.stringify(right));
export const digestObligationLedgerValue = (value: unknown): string => {
const encoded = canonicalize(value);
if (encoded === undefined)
throw new TypeError(
"Reconstruction obligation ledger is not canonical JSON",
);
return createHash("sha256").update(encoded).digest("hex");
};
const countBy = <Value extends string, Item>(
items: readonly Item[],
values: readonly Value[],
valueFor: (item: Item) => Value,
): { key: Value; count: number }[] =>
values.map((value) => ({
key: value,
count: items.filter((item) => valueFor(item) === value).length,
}));
const obligationIdsWith = (
obligations: readonly ReconstructionObligation[],
code: DiagnosticCode,
): string[] =>
obligations
.filter(({ diagnostics }) => hasDiagnostic(diagnostics, code))
.map(({ obligation_id: id }) => id)
.sort();
const hasDiagnostic = (
diagnostics: ReconstructionObligation["diagnostics"],
code: DiagnosticCode,
): boolean => diagnostics.some((diagnostic) => diagnostic.code === code);
@@ -0,0 +1,107 @@
import { z } from "zod";
import {
AnalysisInputError,
AnalysisProtocolError,
type AnalysisError,
} from "../domain/errors.js";
import { createEvidence, type Evidence } from "../domain/evidence.js";
import { jsonObjectSchema, jsonValueSchema } from "../domain/jsonValue.js";
import {
reconstructionObligationLedgerInputSchema,
reconstructionObligationLedgerPageSchema,
type ReconstructionObligationLedgerInput,
type ReconstructionObligationLedgerPage,
} from "../domain/reconstructionObligationLedgerSchemas.js";
import { err, ok, type Result } from "../domain/result.js";
import { JAVASCRIPT_APPLICATION_WORKFLOW_PROVIDER } from "./InvestigationProviders.js";
import { deriveReconstructionObligationCandidates } from "./ReconstructionObligationCandidates.js";
import { evaluateReconstructionObligationLedger } from "./ReconstructionObligationLedgerEvaluation.js";
const OPERATION = "build_reconstruction_obligation_ledger" as const;
/** Parse one portable ledger request for both CLI and MCP adapters. */
export const resolveReconstructionObligationLedgerRequest = (
input: unknown,
): Result<ReconstructionObligationLedgerInput, AnalysisError> => {
const parsed = reconstructionObligationLedgerInputSchema.safeParse(input);
return parsed.success
? ok(parsed.data)
: err(new AnalysisInputError(OPERATION, { cause: parsed.error }));
};
/** Build one deterministic ledger page and wrap it in portable Evidence v2. */
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,
maxObligations: input.limits.max_obligations,
generationLimitations: generated.limitations,
});
const page = pageLedger(ledger, input.page.offset, input.page.limit);
return ok(createLedgerEvidence(input, page));
} catch (cause: unknown) {
if (cause instanceof z.ZodError)
return err(new AnalysisInputError(OPERATION, { cause }));
return err(
new AnalysisProtocolError(
"Reconstruction obligation ledger generation failed",
{ cause },
),
);
}
};
const pageLedger = (
ledger: ReturnType<typeof evaluateReconstructionObligationLedger>,
offset: number,
limit: number,
): ReconstructionObligationLedgerPage => {
const obligations = ledger.obligations.slice(offset, offset + limit);
const nextOffset =
offset + obligations.length < ledger.obligations.length
? offset + obligations.length
: null;
const { obligations: _allObligations, ...summary } = ledger;
return reconstructionObligationLedgerPageSchema.parse({
...summary,
page: {
offset,
limit,
total: ledger.obligations.length,
returned: obligations.length,
next_offset: nextOffset,
},
obligations,
});
};
const createLedgerEvidence = (
input: ReconstructionObligationLedgerInput,
page: ReconstructionObligationLedgerPage,
): Evidence =>
createEvidence(undefined, JAVASCRIPT_APPLICATION_WORKFLOW_PROVIDER, {
predicateType: "rea.reconstruction-obligation-ledger/v1",
operation: OPERATION,
parameters: jsonObjectSchema.parse({
ledger_id: page.ledger_id,
closure_digest: page.closure_digest,
limits: input.limits,
page: input.page,
}),
result: jsonValueSchema.parse(page),
rawResult: null,
confidence: "inferred",
authority: "analyst-inference",
environment: null,
limitations: page.limitations,
evidenceLinks: page.evidence_links,
});
@@ -0,0 +1,64 @@
import type { Evidence } from "../domain/evidence.js";
import type { ReconstructionObligation } from "../domain/reconstructionObligationLedgerSchemas.js";
import { evidenceSupportsOriginalAuthority } from "./ReconstructionObligationAuthority.js";
import type { ReconstructionObligationCandidate } from "./ReconstructionObligationCandidates.js";
type Binding = NonNullable<ReconstructionObligation["binding"]>;
type Diagnostic = ReconstructionObligation["diagnostics"][number];
/** Merge generated and manifest-supplied original observations canonically. */
export const originalCasesFor = (
candidate: ReconstructionObligationCandidate,
binding: Binding | null,
): ReconstructionObligation["observed_cases"] =>
[
...new Map(
[...candidate.observed_cases, ...(binding?.original_cases ?? [])].map(
(observed) => [
`${observed.kind}:${observed.evidence_id}:${observed.location}`,
observed,
],
),
).values(),
].sort((left, right) =>
`${left.kind}:${left.evidence_id}:${left.location}`.localeCompare(
`${right.kind}:${right.evidence_id}:${right.location}`,
),
);
/** Report original cases that are absent or lack comparable Evidence authority. */
export const originalCaseDiagnostics = (
candidate: ReconstructionObligationCandidate,
binding: Binding | null,
evidenceById: ReadonlyMap<string, Evidence>,
): Diagnostic[] => {
const diagnostics: Diagnostic[] = [];
const originalCases = originalCasesFor(candidate, binding);
for (const caseKind of candidate.required_case_kinds) {
const cases = originalCases.filter(({ kind }) => kind === caseKind);
if (cases.length === 0) {
diagnostics.push({
code: "missing-original-case",
detail: `Original ${caseKind} behavior has not been observed.`,
});
continue;
}
if (
!cases.some(({ evidence_id: evidenceId }) => {
const evidence = evidenceById.get(evidenceId);
return (
evidence !== undefined &&
evidenceSupportsOriginalAuthority(
evidence,
candidate.required_original_authority,
)
);
})
)
diagnostics.push({
code: "weak-original-case-authority",
detail: `Original ${caseKind} behavior lacks ${candidate.required_original_authority} Evidence.`,
});
}
return diagnostics;
};
@@ -0,0 +1,169 @@
import type { ReconstructionObligation } from "../domain/reconstructionObligationLedgerSchemas.js";
export interface ReconstructionObligationCandidatePolicy {
readonly applicationLayer: ReconstructionObligation["application_layer"];
readonly family: string;
readonly fixtureAuthority: ReconstructionObligation["required_fixture_authority"];
readonly verifierAuthority: ReconstructionObligation["required_verifier_authority"];
readonly originalAuthority: ReconstructionObligation["required_original_authority"];
readonly cases: ReconstructionObligation["required_case_kinds"];
readonly requiresParserType: boolean;
}
const APPLICATION_NODE_POLICIES: Readonly<
Partial<Record<string, ReconstructionObligationCandidatePolicy>>
> = {
package: policy("packaging", "package-lifecycle", "packaged-process", [
"positive",
"negative",
"teardown",
]),
installer: policy("packaging", "installer", "packaged-process", [
"positive",
"negative",
"teardown",
]),
"context-bridge-api": policy("electron", "context-bridge", "renderer", [
"positive",
"negative",
"malformed",
]),
"ipc-channel": policy("electron", "ipc", "renderer", [
"positive",
"negative",
"malformed",
]),
"ipc-handler": policy("electron", "ipc", "renderer", [
"positive",
"negative",
"malformed",
]),
endpoint: policy("protocol", "endpoint", "protocol", [
"positive",
"negative",
"malformed",
]),
storage: policy("persistence", "persistence", "integration", [
"positive",
"negative",
"malformed",
]),
worker: policy("process", "worker-lifecycle", "packaged-process", [
"positive",
"cancellation",
"teardown",
]),
"service-worker": policy("process", "worker-lifecycle", "packaged-process", [
"positive",
"cancellation",
"teardown",
]),
"native-addon": nativePolicy("native-addon"),
"native-export": nativePolicy("native-export"),
"managed-pinvoke-import": nativePolicy("managed-pinvoke"),
"managed-native-implementation": nativePolicy("managed-native"),
};
const SEMANTIC_NODE_POLICIES: Readonly<
Partial<Record<string, ReconstructionObligationCandidatePolicy>>
> = {
"child-process": policy("process", "child-process", "packaged-process", [
"positive",
"negative",
"cancellation",
"teardown",
]),
stdio: policy("process", "stdio", "packaged-process", [
"positive",
"negative",
"teardown",
]),
signal: policy("process", "signal", "packaged-process", [
"positive",
"cancellation",
"teardown",
]),
"config-source": parserPolicy("cli", "configuration", "integration"),
request: parserPolicy("protocol", "request", "protocol"),
response: parserPolicy("protocol", "response", "protocol"),
boundary: parserPolicy("application", "parser-boundary", "integration"),
resource: policy("persistence", "resource-lifecycle", "integration", [
"positive",
"negative",
"teardown",
]),
event: policy("runtime", "event", "packaged-process", [
"positive",
"negative",
"cancellation",
]),
timer: policy("runtime", "timer", "packaged-process", [
"positive",
"cancellation",
"teardown",
]),
promise: policy("runtime", "promise-ownership", "packaged-process", [
"positive",
"negative",
"cancellation",
]),
task: policy("runtime", "task-lifecycle", "packaged-process", [
"positive",
"cancellation",
"teardown",
]),
};
export const applicationObligationPolicy = (
nodeKind: string,
): ReconstructionObligationCandidatePolicy | undefined =>
APPLICATION_NODE_POLICIES[nodeKind];
export const semanticObligationPolicy = (
nodeKind: string,
): ReconstructionObligationCandidatePolicy | undefined =>
SEMANTIC_NODE_POLICIES[nodeKind];
function policy(
applicationLayer: ReconstructionObligationCandidatePolicy["applicationLayer"],
family: string,
authority: ReconstructionObligationCandidatePolicy["fixtureAuthority"],
cases: ReconstructionObligationCandidatePolicy["cases"],
): ReconstructionObligationCandidatePolicy {
return {
applicationLayer,
family,
fixtureAuthority: authority,
verifierAuthority: authority,
originalAuthority: "runtime",
cases,
requiresParserType: false,
};
}
function parserPolicy(
applicationLayer: ReconstructionObligationCandidatePolicy["applicationLayer"],
family: string,
authority: ReconstructionObligationCandidatePolicy["fixtureAuthority"],
): ReconstructionObligationCandidatePolicy {
return {
...policy(applicationLayer, family, authority, [
"positive",
"negative",
"malformed",
]),
requiresParserType: true,
};
}
function nativePolicy(family: string): ReconstructionObligationCandidatePolicy {
return {
applicationLayer: "native-abi",
family,
fixtureAuthority: "native-abi",
verifierAuthority: "native-abi",
originalAuthority: "static",
cases: ["positive", "negative"],
requiresParserType: false,
};
}
@@ -0,0 +1,344 @@
import canonicalize from "canonicalize";
import { z } from "zod";
import { evidenceBundleSchema } from "./evidenceBundle.js";
const digestSchema = z.string().regex(/^[a-f0-9]{64}$/u);
const evidenceIdSchema = z.string().regex(/^ev_[a-f0-9]{64}$/u);
const stableIdSchema = z.string().regex(/^[A-Za-z][A-Za-z0-9._:/-]{0,199}$/u);
const boundedTextSchema = z.string().trim().min(1).max(4_096);
export const obligationStatusSchema = z.enum([
"unowned",
"characterized",
"implemented",
"verified",
"contradicted",
"blocked",
"out-of-scope",
"unknown",
]);
export const obligationLayerSchema = z.enum([
"cli",
"protocol",
"electron",
"persistence",
"process",
"runtime",
"packaging",
"native-abi",
"application",
"other",
]);
export const obligationCaseKindSchema = z.enum([
"positive",
"negative",
"malformed",
"cancellation",
"teardown",
]);
export const obligationProofAuthoritySchema = z.enum([
"unit",
"integration",
"protocol",
"renderer",
"packaged-process",
"native-abi",
"live-observation",
"external",
]);
export const obligationOriginalAuthoritySchema = z.enum([
"static",
"runtime",
"process",
"external",
]);
const obligationTargetSchema = z.strictObject({
artifact_sha256: digestSchema.nullable(),
application_node_id: z
.string()
.regex(/^jag_node_[a-f0-9]{64}$/u)
.nullable(),
semantic_node_id: z
.string()
.regex(/^jsrg_node_[a-f0-9]{64}$/u)
.nullable(),
location: boundedTextSchema,
});
const authorityReferenceSchema = z.strictObject({
evidence_id: evidenceIdSchema,
authority: z.enum([
"shipped-artifact",
"controlled-replay",
"historical-reference",
"external-service",
"analyst-inference",
]),
state: z.enum(["candidate", "observed", "reviewed", "unknown"]),
location: boundedTextSchema,
});
const observedCaseSchema = z.strictObject({
kind: obligationCaseKindSchema,
evidence_id: evidenceIdSchema,
location: boundedTextSchema,
});
const ownerBindingSchema = z.strictObject({
module_path: boundedTextSchema,
symbol: z.string().trim().min(1).max(1_024),
owner_sha256: digestSchema,
});
const parserTypeBindingSchema = z.strictObject({
parser: boundedTextSchema,
schema: boundedTextSchema,
domain_type: boundedTextSchema,
});
const fixtureBindingSchema = z.strictObject({
fixture_id: stableIdSchema,
case_kind: obligationCaseKindSchema,
authority: obligationProofAuthoritySchema,
evidence_ids: z.array(evidenceIdSchema).min(1).max(100),
});
const nondeterminismSchema = z.strictObject({
mode: z.enum(["total-order", "partial-order", "finite-traces"]),
specification: boundedTextSchema,
});
const verifierBindingSchema = z.strictObject({
verifier_id: stableIdSchema,
claim_id: stableIdSchema,
command: boundedTextSchema,
authority: obligationProofAuthoritySchema,
status: z.enum(["pass", "fail", "unknown", "unsupported", "skipped"]),
result_evidence_id: evidenceIdSchema,
enumerated_obligation_ids: z.array(stableIdSchema).min(1).max(10_000),
nondeterminism: nondeterminismSchema,
});
export const reconstructionObligationManifestBindingSchema = z.strictObject({
obligation_id: stableIdSchema,
owner: ownerBindingSchema,
parser_type: parserTypeBindingSchema.nullable(),
original_cases: z.array(observedCaseSchema).max(100),
fixtures: z.array(fixtureBindingSchema).max(100),
verifier: verifierBindingSchema.nullable(),
});
export const reconstructionObligationManifestSchema = z.strictObject({
schema_version: z.literal(1),
bindings: z.array(reconstructionObligationManifestBindingSchema).max(10_000),
contradictions: z
.array(
z.strictObject({
obligation_id: stableIdSchema,
evidence_ids: z.array(evidenceIdSchema).min(2).max(100),
}),
)
.max(10_000),
});
export const reviewedReconstructionObligationSchema = z.strictObject({
obligation_id: stableIdSchema,
obligation_version: z.number().int().min(1),
title: z.string().trim().min(1).max(500),
application_layer: obligationLayerSchema,
family: z.string().trim().min(1).max(100),
target: obligationTargetSchema,
required: z.boolean(),
required_case_kinds: z.array(obligationCaseKindSchema).min(1).max(5),
required_original_authority: obligationOriginalAuthoritySchema,
required_fixture_authority: obligationProofAuthoritySchema,
required_verifier_authority: obligationProofAuthoritySchema,
requires_parser_type: z.boolean(),
dependency_obligation_ids: z.array(stableIdSchema).max(1_000),
residual_unknown_ids: z.array(stableIdSchema).max(1_000),
unavailable_authority: z.array(boundedTextSchema).max(100),
required_next_evidence: z.array(boundedTextSchema).max(100),
disposition: z.enum(["active", "blocked", "out-of-scope"]),
review_evidence_ids: z.array(evidenceIdSchema).min(1).max(100),
});
const diagnosticSchema = z.strictObject({
code: z.enum([
"duplicate-definition",
"missing-owner",
"ambiguous-owner",
"missing-parser-type",
"missing-original-case",
"weak-original-case-authority",
"missing-case",
"weak-fixture-authority",
"missing-verifier",
"verifier-does-not-enumerate",
"missing-verifier-result",
"weak-verifier-authority",
"verifier-failed",
"static-only",
"contradiction",
"contradiction-evidence-missing",
"residual-unknown",
"dependency-missing",
"dependency-open",
"unavailable-authority",
"blocked",
"out-of-scope",
]),
detail: boundedTextSchema,
});
export const reconstructionObligationSchema = z.strictObject({
obligation_id: stableIdSchema,
obligation_version: z.number().int().min(1),
title: z.string().trim().min(1).max(500),
origin: z.enum(["generated", "reviewed"]),
application_layer: obligationLayerSchema,
family: z.string().trim().min(1).max(100),
target: obligationTargetSchema,
authority_references: z.array(authorityReferenceSchema).min(1).max(1_000),
source_state: z.enum(["candidate", "observed", "reviewed", "unknown"]),
observed_cases: z.array(observedCaseSchema).max(100),
required: z.boolean(),
required_case_kinds: z.array(obligationCaseKindSchema).min(1).max(5),
required_original_authority: obligationOriginalAuthoritySchema,
required_fixture_authority: obligationProofAuthoritySchema,
required_verifier_authority: obligationProofAuthoritySchema,
requires_parser_type: z.boolean(),
dependency_obligation_ids: z.array(stableIdSchema).max(1_000),
residual_unknown_ids: z.array(stableIdSchema).max(1_000),
unavailable_authority: z.array(boundedTextSchema).max(100),
required_next_evidence: z.array(boundedTextSchema).max(100),
binding: reconstructionObligationManifestBindingSchema.nullable(),
status: obligationStatusSchema,
diagnostics: z.array(diagnosticSchema).max(1_000),
});
const countBySchema = <Schema extends z.ZodType>(
key: Schema,
): z.ZodArray<
z.ZodObject<{
key: Schema;
count: z.ZodNumber;
}>
> =>
z.array(
z.strictObject({
key,
count: z.number().int().min(0),
}),
);
export const reconstructionObligationLedgerSchema = z.strictObject({
schema: z.literal("ReconstructionObligationLedger"),
schema_version: z.literal(1),
ledger_id: z.string().regex(/^rol_[a-f0-9]{64}$/u),
closure_digest: digestSchema,
status: z.enum(["ready", "open", "failed", "unknown"]),
coverage: z.strictObject({
status: z.enum(["complete", "partial"]),
truncated: z.boolean(),
omitted_count: z.number().int().min(0),
max_obligations: z.number().int().min(1),
}),
summary: z.strictObject({
total: z.number().int().min(0),
required: z.number().int().min(0),
verified: z.number().int().min(0),
required_open: z.number().int().min(0),
by_status: countBySchema(obligationStatusSchema),
by_application_layer: countBySchema(obligationLayerSchema),
by_evidence_authority: countBySchema(
z.enum(["candidate", "observed", "reviewed", "unknown"]),
),
}),
reports: z.strictObject({
missing_owner_obligation_ids: z.array(stableIdSchema),
missing_verifier_obligation_ids: z.array(stableIdSchema),
contradicted_obligation_ids: z.array(stableIdSchema),
residual_unknown_ids: z.array(stableIdSchema),
}),
ownership_graph: z.array(
z.strictObject({
obligation_id: stableIdSchema,
module_path: boundedTextSchema,
symbol: z.string().trim().min(1).max(1_024),
}),
),
dependency_graph: z.array(
z.strictObject({
obligation_id: stableIdSchema,
depends_on_obligation_id: stableIdSchema,
}),
),
obligations: z.array(reconstructionObligationSchema).max(10_000),
evidence_links: z.array(evidenceIdSchema).max(100_000),
limitations: z.array(boundedTextSchema).max(1_000),
});
export const reconstructionObligationLedgerInputSchema = z.strictObject({
evidence_bundle: evidenceBundleSchema,
reviewed_obligations: z
.array(reviewedReconstructionObligationSchema)
.max(10_000),
manifest: reconstructionObligationManifestSchema,
limits: z.strictObject({
max_obligations: z.number().int().min(1).max(10_000),
}),
page: z.strictObject({
offset: z.number().int().min(0).max(10_000),
limit: z.number().int().min(1).max(100),
}),
});
export const reconstructionObligationLedgerPageSchema = z.strictObject({
...reconstructionObligationLedgerSchema.omit({ obligations: true }).shape,
page: z.strictObject({
offset: z.number().int().min(0),
limit: z.number().int().min(1).max(100),
total: z.number().int().min(0),
returned: z.number().int().min(0),
next_offset: z.number().int().min(0).nullable(),
}),
obligations: z.array(reconstructionObligationSchema).max(100),
});
export type ReconstructionObligation = z.infer<
typeof reconstructionObligationSchema
>;
export type ReconstructionObligationLedger = z.infer<
typeof reconstructionObligationLedgerSchema
>;
export type ReconstructionObligationLedgerInput = z.infer<
typeof reconstructionObligationLedgerInputSchema
>;
export type ReconstructionObligationLedgerPage = z.infer<
typeof reconstructionObligationLedgerPageSchema
>;
export type ReviewedReconstructionObligation = z.infer<
typeof reviewedReconstructionObligationSchema
>;
export type ReconstructionObligationManifest = z.infer<
typeof reconstructionObligationManifestSchema
>;
/** Parse and canonically serialize one complete reconstruction obligation ledger. */
export const serializeReconstructionObligationLedger = (
input: unknown,
): string => {
const ledger = reconstructionObligationLedgerSchema.parse(input);
const encoded = canonicalize(ledger);
if (encoded === undefined)
throw new TypeError(
"Reconstruction obligation ledger is not canonical JSON",
);
return encoded;
};
@@ -0,0 +1,53 @@
import type { McpServer } from "@modelcontextprotocol/server";
import {
buildReconstructionObligationLedgerEvidenceValidated,
resolveReconstructionObligationLedgerRequest,
} from "../../application/ReconstructionObligationLedgerService.js";
import { applicationToolContract } from "../../contracts/applicationToolContracts.js";
import { reconstructionObligationLedgerInputSchema } from "../../domain/reconstructionObligationLedgerSchemas.js";
import { logToolExecution } from "../toolLogging.js";
import { safeParseToolInput } from "../toolInputValidation.js";
import { toolRegistrationOptions } from "../toolRegistrationOptions.js";
import { toCallToolResult } from "../toolResult.js";
import { recordResult, recordSources } from "./helpers.js";
import type { ApplicationToolRegistration } from "./types.js";
const contract = applicationToolContract(
"build_reconstruction_obligation_ledger",
);
/** Register conservative reconstruction-obligation generation and closure. */
export const registerReconstructionObligationLedgerTool = (
server: McpServer,
options: ApplicationToolRegistration,
): void => {
server.registerTool(
contract.name,
toolRegistrationOptions(contract),
async (input) => {
const parsed = safeParseToolInput(
reconstructionObligationLedgerInputSchema,
input,
contract.name,
);
if (!parsed.ok) return toCallToolResult(parsed, contract);
const resolved = resolveReconstructionObligationLedgerRequest(
parsed.value,
);
if (!resolved.ok) return toCallToolResult(resolved, contract);
const result = await logToolExecution(options.logger, contract.name, () =>
Promise.resolve(
buildReconstructionObligationLedgerEvidenceValidated(resolved.value),
),
);
if (!result.ok) return toCallToolResult(result, contract);
const recorded = recordSources(
options.recordEvidence,
resolved.value.evidence_bundle.records,
);
if (!recorded.ok) return toCallToolResult(recorded, contract);
return recordResult(options, contract, result.value);
},
);
};
@@ -0,0 +1,69 @@
import {
ResourceNotFoundError,
ResourceTemplate,
type McpServer,
} from "@modelcontextprotocol/server";
import type { BinarySessionPort } from "../application/BinarySessionPort.js";
import { reconstructionObligationLedgerPageSchema } from "../domain/reconstructionObligationLedgerSchemas.js";
/** Expose retained obligation-ledger pages as typed Evidence resources. */
export const registerReconstructionObligationLedgerResource = (
server: McpServer,
session: BinarySessionPort,
): void => {
server.registerResource(
"reconstruction-obligation-ledger-page",
new ResourceTemplate(
"rea://evidence/{evidenceId}/reconstruction-obligation-ledger",
{
list: undefined,
complete: {
evidenceId: (prefix) =>
session
.exportEvidenceBundle()
.records.filter(
({ predicate_type: predicateType }) =>
predicateType === "rea.reconstruction-obligation-ledger/v1",
)
.map(({ evidence_id: evidenceId }) => evidenceId)
.filter((evidenceId) => evidenceId.startsWith(prefix)),
},
},
),
{
title: "Reconstruction obligation ledger page",
description:
"One deterministic Evidence v2-backed obligation page with full-ledger closure digest and summaries.",
mimeType: "application/json",
},
(uri, variables) => {
const evidenceId = variables.evidenceId;
if (typeof evidenceId !== "string")
throw new ResourceNotFoundError(uri.href);
const evidence = session.evidenceById(evidenceId);
if (
evidence === undefined ||
evidence.predicate_type !== "rea.reconstruction-obligation-ledger/v1"
)
throw new ResourceNotFoundError(uri.href);
const page = reconstructionObligationLedgerPageSchema.safeParse(
evidence.normalized_result,
);
if (!page.success) throw new ResourceNotFoundError(uri.href);
return {
contents: [
{
uri: uri.href,
mimeType: "application/json",
text: JSON.stringify(
{ evidence_id: evidence.evidence_id, page: page.data },
null,
2,
),
},
],
};
},
);
};
@@ -0,0 +1,414 @@
import { describe, expect, it } from "vitest";
import { PROCESS_PROVIDER } from "../src/application/ProcessEvidence.js";
import {
buildReconstructionObligationLedgerEvidenceValidated,
resolveReconstructionObligationLedgerRequest,
} from "../src/application/ReconstructionObligationLedgerService.js";
import { EMPTY_PROCESS_CAPTURE_EXAMPLE } from "../src/contracts/processCaptureExample.js";
import { createEvidence, type Evidence } from "../src/domain/evidence.js";
import { createEvidenceBundle } from "../src/domain/evidenceBundle.js";
import { jsonValueSchema } from "../src/domain/jsonValue.js";
import { processCaptureSchema } from "../src/domain/processCapture.js";
import {
reconstructionObligationLedgerPageSchema,
serializeReconstructionObligationLedger,
type ReconstructionObligationLedgerInput,
type ReconstructionObligationLedgerPage,
type ReviewedReconstructionObligation,
} from "../src/domain/reconstructionObligationLedgerSchemas.js";
const proofEvidence = (id: string) =>
createEvidence(
undefined,
{ id: `fixture-${id}`, name: "Fixture verifier", version: "1" },
{
predicateType: "rea.fixture-verification/v1",
operation: "run_fixture_verifier",
parameters: { id },
result: { passed: true },
confidence: "observed",
authority: "controlled-replay",
},
);
const processEvidence = () =>
createEvidence(undefined, PROCESS_PROVIDER, {
predicateType: "rea.process-capture/v4",
operation: "capture_process_scenario",
parameters: {},
result: jsonValueSchema.parse(
processCaptureSchema.parse(EMPTY_PROCESS_CAPTURE_EXAMPLE),
),
confidence: "observed",
authority: "controlled-replay",
});
const request = (
records: readonly Evidence[],
overrides: Partial<ReconstructionObligationLedgerInput> = {},
): ReconstructionObligationLedgerInput => ({
evidence_bundle: createEvidenceBundle(records),
reviewed_obligations: [],
manifest: { schema_version: 1, bindings: [], contradictions: [] },
limits: { max_obligations: 100 },
page: { offset: 0, limit: 50 },
...overrides,
});
const build = (
input: ReconstructionObligationLedgerInput,
): ReconstructionObligationLedgerPage => {
const parsed = resolveReconstructionObligationLedgerRequest(input);
if (!parsed.ok) throw parsed.error;
const result = buildReconstructionObligationLedgerEvidenceValidated(
parsed.value,
);
if (!result.ok) throw result.error;
return reconstructionObligationLedgerPageSchema.parse(
result.value.normalized_result,
);
};
const completeLedger = (
page: ReconstructionObligationLedgerPage,
obligations: ReconstructionObligationLedgerPage["obligations"],
) => {
const { page: pagination, ...ledger } = page;
if (pagination.total !== obligations.length)
throw new TypeError("Incomplete obligation page set");
return { ...ledger, obligations };
};
const originalCases = (
obligation: ReconstructionObligationLedgerPage["obligations"][number],
evidenceId: string,
) =>
obligation.required_case_kinds.map((caseKind) => ({
kind: caseKind,
evidence_id: evidenceId,
location: `/normalized_result/cases/${caseKind}`,
}));
describe("reconstruction obligation ledger", () => {
it("emits deterministic closure identity and paging", () => {
const evidence = proofEvidence("review");
const reviewed = [
reviewedObligation("obl.review.one", evidence.evidence_id),
reviewedObligation("obl.review.two", evidence.evidence_id),
];
const first = build(
request([evidence], {
reviewed_obligations: reviewed,
page: { offset: 0, limit: 1 },
}),
);
const second = build(
request([evidence], {
reviewed_obligations: reviewed,
page: { offset: 1, limit: 1 },
}),
);
expect(first.page).toEqual({
offset: 0,
limit: 1,
total: 2,
returned: 1,
next_offset: 1,
});
expect(second.page.next_offset).toBeNull();
expect(second.ledger_id).toBe(first.ledger_id);
expect(second.closure_digest).toBe(first.closure_digest);
expect(first.obligations[0]?.source_state).toBe("reviewed");
expect(first.status).toBe("open");
expect(
serializeReconstructionObligationLedger(
completeLedger(first, [...first.obligations, ...second.obligations]),
),
).toBe(
serializeReconstructionObligationLedger(
completeLedger(second, [...first.obligations, ...second.obligations]),
),
);
});
it("does not let green unit evidence close a packaged-process obligation", () => {
const capture = processEvidence();
const proof = proofEvidence("green-unit");
const initial = build(request([capture, proof]));
const obligation = initial.obligations[0];
if (obligation === undefined)
throw new Error("Expected process obligation");
const unitBinding = {
obligation_id: obligation.obligation_id,
owner: {
module_path: "src/processOwner.ts",
symbol: "runProcess",
owner_sha256: "1".repeat(64),
},
parser_type: null,
original_cases: originalCases(obligation, capture.evidence_id),
fixtures: obligation.required_case_kinds.map((caseKind) => ({
fixture_id: `fixture.${caseKind}`,
case_kind: caseKind,
authority: "unit" as const,
evidence_ids: [proof.evidence_id],
})),
verifier: {
verifier_id: "verifier.unit",
claim_id: "claim.process",
command: "npm test -- processOwner",
authority: "unit" as const,
status: "pass" as const,
result_evidence_id: proof.evidence_id,
enumerated_obligation_ids: [obligation.obligation_id],
nondeterminism: {
mode: "total-order" as const,
specification: "Exact unit-call order.",
},
},
};
const result = build(
request([capture, proof], {
manifest: {
schema_version: 1,
bindings: [unitBinding],
contradictions: [],
},
}),
);
const evaluated = result.obligations[0];
expect(evaluated?.status).toBe("implemented");
expect(evaluated?.diagnostics.map(({ code }) => code)).toEqual(
expect.arrayContaining([
"weak-fixture-authority",
"weak-verifier-authority",
]),
);
expect(result.status).toBe("open");
});
it("keeps unobserved original cases open", () => {
const capture = processEvidence();
const proof = proofEvidence("packaged-original-gap");
const initial = build(request([capture, proof]));
const obligation = initial.obligations[0];
if (obligation === undefined)
throw new Error("Expected process obligation");
const result = build(
request([capture, proof], {
manifest: {
schema_version: 1,
bindings: [
{
obligation_id: obligation.obligation_id,
owner: {
module_path: "src/processOwner.ts",
symbol: "runProcess",
owner_sha256: "4".repeat(64),
},
parser_type: null,
original_cases: obligation.observed_cases,
fixtures: obligation.required_case_kinds.map((caseKind) => ({
fixture_id: `packaged.${caseKind}`,
case_kind: caseKind,
authority: "packaged-process",
evidence_ids: [proof.evidence_id],
})),
verifier: {
verifier_id: "verifier.packaged",
claim_id: "claim.process",
command: "npm run verify:package -- process",
authority: "packaged-process",
status: "pass",
result_evidence_id: proof.evidence_id,
enumerated_obligation_ids: [obligation.obligation_id],
nondeterminism: {
mode: "partial-order",
specification:
"Startup precedes teardown; worker completion is unordered.",
},
},
},
],
contradictions: [],
},
}),
);
expect(result.obligations[0]?.status).toBe("implemented");
expect(result.obligations[0]?.diagnostics).toEqual(
expect.arrayContaining([
expect.objectContaining({
code: "missing-original-case",
detail: expect.stringContaining("negative"),
}),
expect.objectContaining({
code: "missing-original-case",
detail: expect.stringContaining("cancellation"),
}),
]),
);
expect(result.status).toBe("open");
});
it("closes only when every required case and verifier has comparable authority", () => {
const capture = processEvidence();
const proof = proofEvidence("packaged");
const initial = build(request([capture, proof]));
const obligation = initial.obligations[0];
if (obligation === undefined)
throw new Error("Expected process obligation");
const result = build(
request([capture, proof], {
manifest: {
schema_version: 1,
bindings: [
{
obligation_id: obligation.obligation_id,
owner: {
module_path: "src/processOwner.ts",
symbol: "runProcess",
owner_sha256: "2".repeat(64),
},
parser_type: null,
original_cases: originalCases(obligation, capture.evidence_id),
fixtures: obligation.required_case_kinds.map((caseKind) => ({
fixture_id: `packaged.${caseKind}`,
case_kind: caseKind,
authority: "packaged-process",
evidence_ids: [proof.evidence_id],
})),
verifier: {
verifier_id: "verifier.packaged",
claim_id: "claim.process",
command: "npm run verify:package -- process",
authority: "packaged-process",
status: "pass",
result_evidence_id: proof.evidence_id,
enumerated_obligation_ids: [obligation.obligation_id],
nondeterminism: {
mode: "partial-order",
specification:
"Startup precedes teardown; worker completion is unordered.",
},
},
},
],
contradictions: [],
},
}),
);
expect(result.obligations[0]).toMatchObject({
status: "verified",
diagnostics: [],
});
expect(result.status).toBe("ready");
expect(result.summary.required_open).toBe(0);
});
it("fails closed on parser gaps, ambiguous ownership, and contradiction", () => {
const original = createEvidence(
undefined,
{ id: "external", name: "External protocol observer", version: "1" },
{
predicateType: "rea.protocol-observation/v1",
operation: "observe_protocol",
parameters: {},
result: { status: 200 },
confidence: "observed",
authority: "external-service",
},
);
const proof = proofEvidence("protocol");
const reviewed: ReviewedReconstructionObligation = {
...reviewedObligation("obl.protocol", original.evidence_id),
application_layer: "protocol" as const,
family: "json-rpc",
required_original_authority: "external" as const,
required_fixture_authority: "protocol" as const,
required_verifier_authority: "protocol" as const,
requires_parser_type: true,
required_case_kinds: ["positive", "negative", "malformed"],
};
const binding = {
obligation_id: reviewed.obligation_id,
owner: {
module_path: "src/protocol.ts",
symbol: "handleRequest",
owner_sha256: "3".repeat(64),
},
parser_type: null,
original_cases: reviewed.required_case_kinds.map((caseKind) => ({
kind: caseKind,
evidence_id: original.evidence_id,
location: `/protocol/${caseKind}`,
})),
fixtures: [
{
fixture_id: "fixture.positive",
case_kind: "positive" as const,
authority: "unit" as const,
evidence_ids: [proof.evidence_id],
},
],
verifier: null,
};
const result = build(
request([original, proof], {
reviewed_obligations: [reviewed],
manifest: {
schema_version: 1,
bindings: [binding, binding],
contradictions: [
{
obligation_id: reviewed.obligation_id,
evidence_ids: [original.evidence_id, proof.evidence_id],
},
],
},
}),
);
expect(result.status).toBe("failed");
expect(result.obligations[0]?.status).toBe("contradicted");
expect(result.obligations[0]?.diagnostics.map(({ code }) => code)).toEqual(
expect.arrayContaining(["ambiguous-owner", "contradiction"]),
);
expect(result.reports.contradicted_obligation_ids).toEqual([
reviewed.obligation_id,
]);
});
});
const reviewedObligation = (
obligationId: string,
evidenceId: string,
): ReviewedReconstructionObligation => ({
obligation_id: obligationId,
obligation_version: 1,
title: `Reviewed obligation ${obligationId}`,
application_layer: "other",
family: "reviewed",
target: {
artifact_sha256: null,
application_node_id: null,
semantic_node_id: null,
location: `/reviewed/${obligationId}`,
},
required: true,
required_case_kinds: ["positive"],
required_original_authority: "static",
required_fixture_authority: "unit",
required_verifier_authority: "unit",
requires_parser_type: false,
dependency_obligation_ids: [],
residual_unknown_ids: [],
unavailable_authority: [],
required_next_evidence: ["Bind an owner and verifier."],
disposition: "active",
review_evidence_ids: [evidenceId],
});
@@ -0,0 +1,47 @@
import { execFile } from "node:child_process";
import { promisify } from "node:util";
import { describe, expect, it } from "vitest";
import { createEvidenceBundle } from "../src/domain/evidenceBundle.js";
const execute = promisify(execFile);
describe("reconstruction obligation ledger CLI parity", () => {
it("builds the same Evidence-backed empty ledger through the CLI", async () => {
const input = {
evidence_bundle: createEvidenceBundle([]),
reviewed_obligations: [],
manifest: {
schema_version: 1,
bindings: [],
contradictions: [],
},
limits: { max_obligations: 100 },
page: { offset: 0, limit: 50 },
};
const { stdout } = await execute(
process.execPath,
[
"scripts/rea.mjs",
"build-reconstruction-obligation-ledger",
JSON.stringify(input),
"--json",
],
{
cwd: process.cwd(),
env: process.env,
maxBuffer: 16 * 1_024 * 1_024,
},
);
expect(JSON.parse(stdout)).toMatchObject({
operation: "build_reconstruction_obligation_ledger",
predicate_type: "rea.reconstruction-obligation-ledger/v1",
normalized_result: {
schema: "ReconstructionObligationLedger",
status: "unknown",
summary: { total: 0 },
},
});
}, 20_000);
});
@@ -0,0 +1,73 @@
import { Client, InMemoryTransport } from "@modelcontextprotocol/client";
import { describe, expect, it } from "vitest";
import { BinarySession } from "../src/application/BinarySession.js";
import { createEvidenceBundle } from "../src/domain/evidenceBundle.js";
import { createServer } from "../src/server/createServer.js";
import { observed } from "./fixtures/analysisExecution.js";
describe("reconstruction obligation ledger MCP parity", () => {
it("builds an Evidence-backed page and exposes the retained resource", async () => {
const session = new BinarySession(() => ({
execute: () => Promise.resolve(observed(null)),
close: () => Promise.resolve(),
}));
const server = createServer(session, session);
const client = new Client({ name: "obligation-ledger-test", version: "1" });
const [clientTransport, serverTransport] =
InMemoryTransport.createLinkedPair();
try {
await server.connect(serverTransport);
await client.connect(clientTransport);
const result = await client.callTool({
name: "build_reconstruction_obligation_ledger",
arguments: {
evidence_bundle: createEvidenceBundle([]),
reviewed_obligations: [],
manifest: {
schema_version: 1,
bindings: [],
contradictions: [],
},
limits: { max_obligations: 100 },
page: { offset: 0, limit: 50 },
},
});
expect(result.isError).not.toBe(true);
expect(result.structuredContent).toMatchObject({
evidence_id: expect.stringMatching(/^ev_[a-f0-9]{64}$/u),
evidence_uri: expect.stringMatching(
/^rea:\/\/evidence\/ev_[a-f0-9]{64}$/u,
),
result: {
schema: "ReconstructionObligationLedger",
status: "unknown",
summary: { total: 0 },
},
});
const evidence = result.structuredContent;
if (
evidence === undefined ||
evidence === null ||
typeof evidence !== "object" ||
!("evidence_id" in evidence) ||
typeof evidence.evidence_id !== "string"
)
throw new TypeError("Missing obligation ledger Evidence ID");
const resource = await client.readResource({
uri: `rea://evidence/${evidence.evidence_id}/reconstruction-obligation-ledger`,
});
expect(resource.contents[0]).toEqual(
expect.objectContaining({
text: expect.stringContaining(
'"schema": "ReconstructionObligationLedger"',
),
}),
);
} finally {
await client.close();
await server.close();
await session.close();
}
});
});