diff --git a/packages/daemon/src/domain/proof/judgments.ts b/packages/daemon/src/domain/proof/judgments.ts index 790c193e5..eaf9d10e2 100644 --- a/packages/daemon/src/domain/proof/judgments.ts +++ b/packages/daemon/src/domain/proof/judgments.ts @@ -211,6 +211,15 @@ export interface JudgeInput { subject?: Judgment["subject"]; expectedEvidence?: Evidence[]; reason: string; expectedRevision: string; expectedPrevious: string | null; operationId?: string; replace?: boolean; } +/** + * Records a verifiable proof judgment for a slice item into the judgments ledger. + * + * @param missionsRoot - Workspace root path for resolving scopes. + * @param input - Judgment details including scope, item, verdict, and evidence. + * @param actor - Identifier of the judging actor authorized under the proof policy. + * @param provenance - Attestation provenance string recorded in the ledger. + * @returns Recorded judgment receipt, current slice readiness, and replay status. + */ export function recordJudgment(missionsRoot: string, input: JudgeInput, actor: string, provenance: string): { judgment: Judgment; readiness: ScopeReadiness; replayed: boolean } { const dir = resolveProofScope(missionsRoot, input.scope), root = workspaceOf(dir, proofFs); if (path.basename(path.dirname(dir)) !== "slices") throw new JudgmentError("slice_required", "Item judgments belong to a slice; higher outcome judgments remain workflow decisions"); @@ -264,13 +273,20 @@ export function recordJudgment(missionsRoot: string, input: JudgeInput, actor: s return { judgment, readiness: readSliceReadiness(dir), replayed: false }; } +/** + * Evaluates readiness for a mission by reading its composition and resolving member slices. + * + * @param missionDir - Path to the mission directory containing mission.yaml. + * @param readPolicy - Reader function for determining applicable proof policy. + * @returns Evaluated mission readiness, including revision, state, slices, and any issues. + */ export function readMissionReadiness(missionDir: string, readPolicy: ProofPolicyRead = policyOf): MissionReadiness { const issues: string[] = [], slices: MissionReadiness["slices"] = []; let historicalStatus: string | null = null; try { const doc = manifest(proofFs, path.join(missionDir, "mission.yaml")); if (!doc) return { revision: hash("legacy"), state: "legacy", slices, issues, historicalStatus }; - const metadata = mapping(doc.metadata, "mission metadata"); + const metadata = doc.metadata != null ? mapping(doc.metadata, "mission metadata") : {}; historicalStatus = typeof metadata.status === "string" ? metadata.status : null; const members = validateMissionComposition(doc, path.join(missionDir, "mission.yaml")).filter(m => m.active); for (const member of members) { diff --git a/packages/daemon/test/proof-judgments.test.ts b/packages/daemon/test/proof-judgments.test.ts index 9d3b937f8..e4e061d3a 100644 --- a/packages/daemon/test/proof-judgments.test.ts +++ b/packages/daemon/test/proof-judgments.test.ts @@ -205,6 +205,22 @@ describe("retained authority negative controls", () => { f.write(join(f.beta, "proof/judgments/00000001.md"), fs.readFileSync(join(f.alpha, "proof/judgments/00000001.md"), "utf8")); expect(readSliceReadiness(f.beta).items[0]!.state).toBe("unknown"); }); + /** + * Verifies that mission readiness resolves composition slices when metadata is omitted (#72), + * while rejecting malformed metadata values. + */ + it("tolerates mission without metadata and resolves composition slices (#72)", () => { + const f = fixture(); + const members = ["01-alpha", "02-beta"].map((s, i) => ({ ref: `slices/${s}/slice.yaml`, order: i + 1, active: true })); + f.write(join(f.mission, "mission.yaml"), { kind: "mission", composition: { slices: members } }); + const readiness = readMissionReadiness(f.mission); + expect(readiness.issues).toEqual([]); + expect(readiness.slices.map(s => s.id)).toEqual(["01-alpha", "02-beta"]); + expect(readiness.historicalStatus).toBeNull(); + f.write(join(f.mission, "mission.yaml"), { kind: "mission", metadata: "not-a-mapping", composition: { slices: members } }); + const invalid = readMissionReadiness(f.mission); + expect(invalid.issues).toContain("mission metadata: expected a mapping"); + }); it("pushes changed source truth, keeps sibling basis, and ignores unchanged source bytes", async () => { const f = fixture(); f.judge(); const sibling = readSliceReadiness(f.beta).revision; const events: Array<{ type: string; revision: string }> = []; let invalidated = 0;