/* RFC-0216: Content Maintenance Plan — shared contracts and task-id hashing. The planner is a pure consolidator over RFC-0213 (freshness ledger), RFC-0214 (source outbox), and RFC-0215 (derivation states). Each signal becomes a deduplicated MaintenanceTask with a pre-deadline dueAt, owner, criticality, and the originating Diagnostic rule ids. Framework-free; no filesystem access. RFC-0216, CKL, maintenance, plan, task, criticality, calendar Define MaintenanceTask, MaintenancePlan, Criticality, MaintenanceTrigger. Stable task id = sha256 of (subject + trigger) so re-runs are idempotent. Compute pre-deadline dueAt from validUntil − leadTime or reviewDueAt. Do not read the filesystem or fetch sources — the kernel command does that. Do not emit Diagnostics — tasks are the plan's output; the gate reads plan.gate. Core plan contracts. Idempotent hash for (subject, trigger). Pre-deadline task date from policy + signal. RFC-0216: initial maintenance plan contracts. */ import { createHash } from "node:crypto"; export type MaintenanceTrigger = "review-due" | "expiring-soon" | "expired" | "source-diverged" | "derived-outdated"; export type Criticality = "advisory" | "important" | "blocking"; export interface MaintenanceTask { /** Stable hash of (subject + trigger) — idempotent across plan builds. */ id: string; subject: string; trigger: MaintenanceTrigger; /** Pre-deadline: validUntil − leadTime, or reviewDueAt, or today for already-expired/diverged. */ dueAt: string; criticality: Criticality; owner: string; /** Originating Diagnostic rule ids (e.g. "CKL-FRESH-02 apps/…/offer.claims.yaml:3"). */ diagnostics: string[]; status: "open" | "routed" | "done"; } export interface MaintenancePlan { generatedAt: string | null; site: string; tasks: MaintenanceTask[]; gate: { /** Amber: important + expired, advisory + any, or important + expiring. Ships with flag. */ amber: number; /** Red: blocking + expired/diverged. Blocks build. */ red: number; }; } /** Deterministic task id — hash of (subject, trigger). */ export function stableTaskId(subject: string, trigger: MaintenanceTrigger): string { return `t_${createHash("sha256").update(`${subject}:${trigger}`, "utf8").digest("hex").slice(0, 16)}`; } /** * Compute a pre-deadline dueAt for a task. * - For time-based signals: validUntil − leadTimeDays (clamped to today if already past). * - For already-expired or event-driven signals: today (already overdue). */ export function computeDueAt(opts: { validUntil?: string; reviewDueAt?: string; leadTimeDays: number; today: string; }): string { const { validUntil, reviewDueAt, leadTimeDays, today } = opts; if (validUntil) { // Subtract leadTimeDays by finding (validUntil in ms) - (leadTimeDays * ms/day). const vu = Date.parse(`${validUntil}T00:00:00Z`); if (!Number.isNaN(vu)) { const early = new Date(vu - leadTimeDays * 86_400_000).toISOString().slice(0, 10); if (early > today) return early; } return today; } if (reviewDueAt) return reviewDueAt < today ? today : reviewDueAt; return today; } /** Default gate policy for a trigger × criticality pair. */ export function defaultCriticality(trigger: MaintenanceTrigger): Criticality { if (trigger === "expired") return "important"; if (trigger === "source-diverged") return "important"; return "advisory"; } /** * The single source of truth for the red (build-blocking) gate verdict, shared by * content.plan.build and (future) APPS_CHECK so the two never diverge. A task is * red iff it is `blocking` criticality AND its claim no longer matches reality, * has drifted from its source/derivation, or its required comparative-commercial * source review is due. */ export function isRedTask( task: Pick, ): boolean { if (task.status === "done") return false; if (task.criticality !== "blocking") return false; return ( task.trigger === "review-due" || task.trigger === "expired" || task.trigger === "source-diverged" || task.trigger === "derived-outdated" ); } export const GENERATED_MARKER = "GENERATED. Do not change this line unless the file contains project specific changes.";