refactor(readiness): share canonical stage validation

This commit is contained in:
morluto committed 2026-10-22 08:52:56 +08:00
1 parent d8948efb81
commit adfde012ec
5 files changed
+55 -46

No files matched your search

@@ -1,15 +1,15 @@
import { createEvidence } from "../domain/evidence.js";
import { createEvidenceBundle } from "../domain/evidenceBundle.js";
import {
READINESS_REQUIRED_CHECKS,
READINESS_STAGE_IDS,
} from "../domain/reconstructionReadinessJourneyFindings.js";
import { READINESS_REQUIRED_CHECKS } from "../domain/reconstructionReadinessJourneyFindings.js";
import {
reconstructionObligationLedgerClosureDigest,
summarizeReconstructionObligations,
} from "../domain/reconstructionObligationLedgerIntegrity.js";
import type { ReconstructionObligationLedger } from "../domain/reconstructionObligationLedgerSchemas.js";
import type { ReconstructionReadinessInput } from "../domain/reconstructionReadinessSchemas.js";
import {
readinessStageIdSchema,
type ReconstructionReadinessInput,
} from "../domain/reconstructionReadinessSchemas.js";
const HASH = "a".repeat(64);
const sourceEvidence = (variant: string) =>
@@ -397,7 +397,7 @@ export const RECONSTRUCTION_READINESS_EXAMPLE: ReconstructionReadinessInput = {
stale_input_detected: true,
evidence_ids: [EVIDENCE_ID],
},
stages: READINESS_STAGE_IDS.map((stageId) => ({
stages: readinessStageIdSchema.options.map((stageId) => ({
stage_id: stageId,
required: true,
status: "pass",
@@ -9,6 +9,7 @@ import {
} from "./reconstructionObligationLedgerIntegrity.js";
import { reconstructionObligationLedgerSchema } from "./reconstructionObligationLedgerSchemas.js";
import {
reconstructionReadinessReportSchema,
reconstructionReadinessInputSchema,
type ReconstructionReadinessInput,
} from "./reconstructionReadinessSchemas.js";
@@ -181,6 +182,31 @@ describe("readiness obligation ledger integrity", () => {
);
});
describe("readiness stage identity", () => {
it.each(["input", "report"] as const)(
"rejects repeated stage IDs in the %s contract",
(kind) => {
const value =
kind === "input"
? input()
: createReconstructionReadinessReport(input());
value.stages[1] = { ...value.stages[0]! };
const schema =
kind === "input"
? reconstructionReadinessInputSchema
: reconstructionReadinessReportSchema;
const parsed = schema.safeParse(value);
expect(parsed.success).toBe(false);
if (parsed.success) throw new Error("Duplicate stage ID was accepted");
expect(parsed.error.issues).toContainEqual({
code: "custom",
path: ["stages", 1, "stage_id"],
message: "Readiness stage IDs must be unique",
});
},
);
});
describe("readiness missing Evidence references", () => {
it("reports each absent Evidence ID once per stage across stage and check references", () => {
const value = input();
+3 -5
View File
@@ -1,12 +1,10 @@
import { digestCanonicalValue } from "./canonicalDigest.js";
import { authenticateParsedEvidenceBundle } from "./evidenceBundle.js";
import { readinessFindingOrder } from "./reconstructionReadinessFindingHelpers.js";
import {
READINESS_STAGE_IDS,
readinessJourneyFindings,
} from "./reconstructionReadinessJourneyFindings.js";
import { readinessJourneyFindings } from "./reconstructionReadinessJourneyFindings.js";
import { readinessIntegrityFindings } from "./reconstructionReadinessIntegrityFindings.js";
import {
readinessStageIdSchema,
reconstructionReadinessInputSchema,
reconstructionReadinessReportSchema,
type ReadinessFinding,
@@ -71,7 +69,7 @@ const effectiveStages = (
input: ReconstructionReadinessInput,
findings: readonly ReadinessFinding[],
): ReconstructionReadinessReport["stages"] =>
READINESS_STAGE_IDS.map((stageId) => {
readinessStageIdSchema.options.map((stageId) => {
const stage = input.stages.find(({ stage_id: id }) => id === stageId);
const stageFindings = findings.filter(({ stage_id: id }) => id === stageId);
const status = stageFindings.some(
@@ -64,8 +64,6 @@ export const READINESS_REQUIRED_CHECKS: Readonly<
],
};
export const READINESS_STAGE_IDS = readinessStageIdSchema.options;
export const readinessJourneyFindings = (
input: ReconstructionReadinessInput,
evidenceIds: ReadonlySet<string>,
@@ -142,7 +140,7 @@ const stageFindings = (
evidenceIds: ReadonlySet<string>,
): ReadinessFinding[] => {
const findings: ReadinessFinding[] = [];
for (const stageId of READINESS_STAGE_IDS) {
for (const stageId of readinessStageIdSchema.options) {
const matching = input.stages.filter(({ stage_id: id }) => id === stageId);
if (matching.length !== 1) {
findings.push(
+19 -32
View File
@@ -4,8 +4,7 @@ import { STABLE_IDENTIFIER_PATTERN } from "./stringPatterns.js";
import { evidenceBundleSchema } from "./evidenceBundle.js";
import { reconstructionObligationLedgerSchema } from "./reconstructionObligationLedgerSchemas.js";
import { digestSchema } from "./../domain/digests.js";
import { prefixedDigestSchema } from "./../domain/digests.js";
import { digestSchema, prefixedDigestSchema } from "./digests.js";
const evidenceIdSchema = prefixedDigestSchema("ev");
const stableIdSchema = z.string().regex(STABLE_IDENTIFIER_PATTERN);
@@ -185,6 +184,22 @@ const stageSchema = z.strictObject({
checks: z.array(stageCheckSchema),
});
const readinessStagesSchema = z
.array(stageSchema)
.length(readinessStageIdSchema.options.length)
.superRefine((stages, context) => {
const seen = new Set<string>();
stages.forEach((stage, index) => {
if (seen.has(stage.stage_id))
context.addIssue({
code: "custom",
path: [index, "stage_id"],
message: "Readiness stage IDs must be unique",
});
seen.add(stage.stage_id);
});
});
const replaySchema = z.strictObject({
expected_source_digest: digestSchema.nullable(),
deterministic: z.boolean(),
@@ -220,21 +235,7 @@ export const reconstructionReadinessInputSchema = z.strictObject({
closure_history: z.array(closureObservationSchema).min(2),
delegation_checks: z.array(delegationCheckSchema).min(1),
replay: replaySchema,
stages: z
.array(stageSchema)
.length(9)
.superRefine((stages, context) => {
const seen = new Set<string>();
stages.forEach((stage, index) => {
if (seen.has(stage.stage_id))
context.addIssue({
code: "custom",
path: [index, "stage_id"],
message: "Readiness stage IDs must be unique",
});
seen.add(stage.stage_id);
});
}),
stages: readinessStagesSchema,
});
const findingSchema = z.strictObject({
@@ -271,21 +272,7 @@ export const reconstructionReadinessReportSchema = z.strictObject({
cleanup_leak_count: z.number().int().min(0),
nondeterministic_replay_count: z.number().int().min(0),
}),
stages: z
.array(stageSchema)
.length(9)
.superRefine((stages, context) => {
const seen = new Set<string>();
stages.forEach((stage, index) => {
if (seen.has(stage.stage_id))
context.addIssue({
code: "custom",
path: [index, "stage_id"],
message: "Readiness stage IDs must be unique",
});
seen.add(stage.stage_id);
});
}),
stages: readinessStagesSchema,
findings: z.array(findingSchema),
evidence_links: z.array(evidenceIdSchema),
snapshot: reconstructionReadinessInputSchema,