feat(reconstruction): add obligation ledgers
This commit is contained in:
@@ -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();
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user