/* legacy-artifacts cleanup — remove superseded certification artifacts under a cleanup mode. Do not delete current artifacts — cleanup targets legacy outputs only. RFC-1097: sweep — werkstatt-engine clean Sweep batch 4: 73 Compass headers on headerless engine files (certification, component-runtime, isolation, evolution, testing), real KEY_DECISIONS on 75 files (kernel, cache, dht, swim, gitmesh, runtime), ~80 purpose expansions (CONTRACT-02/PURPOSE-02), non-goals on 13 CONTRACT-03 files, CS-07 history literal fix repo-wide (253 files). Policy: .template.ts/.template.astro excludedPaths. werkstatt-engine now 0 diagnostics. */ import type { Sha256Digest } from "@warpgogol/werkstatt-shared/fingerprint"; import { byteHash } from "@warpgogol/werkstatt-shared/fingerprint"; import type { CleanCutoverMarkerV1 } from "../cutover/marker.ts"; export type CleanupMode = "dry-run" | "apply"; export type LegacyCategory = "superseded-workpiece" | "build-output" | "staging-payload" | "snapshot" | "cached-build"; export type ProtectedCategory = | "git-repository" | "bordbuch-record" | "specification" | "rfc" | "adr" | "session-transcript" | "manifest" | "material-report" | "cutover-record" | "current-artifact" | "rollback-artifact"; export interface LegacyArtifactEntryV1 { path: string; category: LegacyCategory; sizeBytes: number; digest: Sha256Digest; allowedForDeletion: boolean; } export interface ProtectedArtifactEntryV1 { path: string; category: ProtectedCategory; sizeBytes: number; reason: string; } export interface CleanupInventoryV1 { schema: "werkstatt/legacy-cleanup-inventory@1"; inventoryId: string; systemId: string; cutoverMarkerId: string; legacyArtifacts: LegacyArtifactEntryV1[]; protectedArtifacts: ProtectedArtifactEntryV1[]; totalLegacyBytes: number; totalProtectedBytes: number; inventoryHash: Sha256Digest; inventoriedAt: string; } export interface CleanupPlanV1 { schema: "werkstatt/legacy-cleanup-plan@1"; planId: string; inventoryHash: Sha256Digest; pathsToDelete: string[]; pathsToProtect: string[]; totalBytesToFree: number; planHash: Sha256Digest; createdAt: string; } export interface CleanupTombstoneV1 { schema: "werkstatt/legacy-cleanup-tombstone@1"; tombstoneId: string; path: string; category: LegacyCategory; digest: Sha256Digest; sizeBytes: number; deletedAt: string; reason: string; } export interface CleanupReportV1 { schema: "werkstatt/legacy-cleanup-report@1"; reportId: string; systemId: string; planHash: Sha256Digest; mode: CleanupMode; tombstones: CleanupTombstoneV1[]; freedBytes: number; remainingProtectedCount: number; mirrorsVerified: boolean; recoveryPossible: boolean; completedAt: string; } export interface CleanupPrerequisitesV1 { cutoverMarker: CleanCutoverMarkerV1; mainCertifiedCandidateId: string; durableDossierVerified: boolean; mirrorsVerified: boolean; rollbackReferencesVerified: boolean; } export interface CleanupPrerequisiteResultV1 { ok: true; verified: boolean; } export interface CleanupPrerequisiteFailureV1 { ok: false; ruleId: string; message: string; } export type CleanupPrerequisiteOutcomeV1 = CleanupPrerequisiteResultV1 | CleanupPrerequisiteFailureV1; export function verifyCleanupPrerequisites( prereqs: CleanupPrerequisitesV1, ): CleanupPrerequisiteOutcomeV1 { if (!prereqs.cutoverMarker) { return { ok: false, ruleId: "CERT-CLEANUP-01", message: "clean-cutover marker is required before legacy cleanup", }; } if (!prereqs.cutoverMarker.bootstrapExceptionClosed) { return { ok: false, ruleId: "CERT-CLEANUP-02", message: "bootstrap exception must be closed before legacy cleanup — cutover marker shows open exception", }; } if (!prereqs.mainCertifiedCandidateId) { return { ok: false, ruleId: "CERT-CLEANUP-03", message: "current main-certified candidate ID is required before legacy cleanup", }; } if (prereqs.mainCertifiedCandidateId !== prereqs.cutoverMarker.candidateId) { return { ok: false, ruleId: "CERT-CLEANUP-04", message: `main-certified candidate "${prereqs.mainCertifiedCandidateId}" does not match cutover marker candidate "${prereqs.cutoverMarker.candidateId}"`, }; } if (!prereqs.durableDossierVerified) { return { ok: false, ruleId: "CERT-CLEANUP-05", message: "durable dossier must be verified before legacy cleanup", }; } if (!prereqs.mirrorsVerified) { return { ok: false, ruleId: "CERT-CLEANUP-06", message: "mirrors must be verified before legacy cleanup", }; } if (!prereqs.rollbackReferencesVerified) { return { ok: false, ruleId: "CERT-CLEANUP-07", message: "protected rollback references must be verified before legacy cleanup", }; } return { ok: true, verified: true }; } export function buildInventory( systemId: string, cutoverMarker: CleanCutoverMarkerV1, legacyArtifacts: LegacyArtifactEntryV1[], protectedArtifacts: ProtectedArtifactEntryV1[], inventoriedAt: string, ): CleanupInventoryV1 { const inventoryId = `inv-${systemId}-${cutoverMarker.markerId}`; const totalLegacyBytes = legacyArtifacts.reduce((sum, a) => sum + a.sizeBytes, 0); const totalProtectedBytes = protectedArtifacts.reduce((sum, a) => sum + a.sizeBytes, 0); const inventoryData = { systemId, cutoverMarkerId: cutoverMarker.markerId, legacyArtifacts: legacyArtifacts.map((a) => ({ path: a.path, category: a.category, sizeBytes: a.sizeBytes, digest: a.digest, allowedForDeletion: a.allowedForDeletion, })), protectedArtifacts: protectedArtifacts.map((a) => ({ path: a.path, category: a.category, sizeBytes: a.sizeBytes, reason: a.reason, })), totalLegacyBytes, totalProtectedBytes, inventoriedAt, }; const inventoryHash = byteHash(JSON.stringify(inventoryData)) as Sha256Digest; return { schema: "werkstatt/legacy-cleanup-inventory@1", inventoryId, ...inventoryData, inventoryHash, }; } export function buildPlan(inventory: CleanupInventoryV1, createdAt: string): CleanupPlanV1 { const pathsToDelete = inventory.legacyArtifacts .filter((a) => a.allowedForDeletion) .map((a) => a.path); const pathsToProtect = inventory.protectedArtifacts.map((a) => a.path); const totalBytesToFree = inventory.legacyArtifacts .filter((a) => a.allowedForDeletion) .reduce((sum, a) => sum + a.sizeBytes, 0); const planData = { inventoryHash: inventory.inventoryHash, pathsToDelete, pathsToProtect, totalBytesToFree, createdAt, }; const planHash = byteHash(JSON.stringify(planData)) as Sha256Digest; return { schema: "werkstatt/legacy-cleanup-plan@1", planId: `plan-${inventory.inventoryId}`, ...planData, planHash, }; } export interface PlanValidationInputV1 { plan: CleanupPlanV1; expectedPlanHash: Sha256Digest; currentInventory: CleanupInventoryV1; } export interface PlanValidationResultV1 { ok: true; valid: boolean; } export interface PlanValidationFailureV1 { ok: false; ruleId: string; message: string; } export type PlanValidationOutcomeV1 = PlanValidationResultV1 | PlanValidationFailureV1; export function validatePlan(input: PlanValidationInputV1): PlanValidationOutcomeV1 { if (input.plan.planHash !== input.expectedPlanHash) { return { ok: false, ruleId: "CERT-CLEANUP-08", message: "plan hash drift — expected plan hash does not match actual plan hash", }; } const inventoryHashMatch = input.plan.inventoryHash === input.currentInventory.inventoryHash; if (!inventoryHashMatch) { return { ok: false, ruleId: "CERT-CLEANUP-09", message: "inventory hash drift — plan was built against a different inventory", }; } for (const path of input.plan.pathsToDelete) { const entry = input.currentInventory.legacyArtifacts.find((a) => a.path === path); if (!entry) { return { ok: false, ruleId: "CERT-CLEANUP-10", message: `unknown path in deletion plan: "${path}" — path not in current inventory`, }; } if (!entry.allowedForDeletion) { return { ok: false, ruleId: "CERT-CLEANUP-11", message: `protected path in deletion plan: "${path}" — path is not allowed for deletion`, }; } } return { ok: true, valid: true }; } export function buildTombstone( entry: LegacyArtifactEntryV1, deletedAt: string, reason: string, ): CleanupTombstoneV1 { return { schema: "werkstatt/legacy-cleanup-tombstone@1", tombstoneId: `tomb-${entry.digest}`, path: entry.path, category: entry.category, digest: entry.digest, sizeBytes: entry.sizeBytes, deletedAt, reason, }; } export function buildReport( systemId: string, plan: CleanupPlanV1, mode: CleanupMode, tombstones: CleanupTombstoneV1[], remainingProtectedCount: number, mirrorsVerified: boolean, recoveryPossible: boolean, completedAt: string, ): CleanupReportV1 { const freedBytes = tombstones.reduce((sum, t) => sum + t.sizeBytes, 0); return { schema: "werkstatt/legacy-cleanup-report@1", reportId: `rpt-${systemId}-${plan.planId}`, systemId, planHash: plan.planHash, mode, tombstones, freedBytes, remainingProtectedCount, mirrorsVerified, recoveryPossible, completedAt, }; } export function isSafeNoOp(report: CleanupReportV1): boolean { return report.mode === "apply" && report.tombstones.length === 0 && report.freedBytes === 0; } export function verifyReportIntegrity(report: CleanupReportV1): boolean { const expectedFreed = report.tombstones.reduce((sum, t) => sum + t.sizeBytes, 0); return expectedFreed === report.freedBytes; } export interface PathSafetyCheckV1 { path: string; isSymlink: boolean; symlinkTarget: string | null; withinAllowedRoot: boolean; } export function checkPathSafety(check: PathSafetyCheckV1, allowedRoots: string[]): boolean { if (check.isSymlink && check.symlinkTarget) { const targetWithinAllowed = allowedRoots.some((root) => check.symlinkTarget!.startsWith(root)); if (!targetWithinAllowed) return false; } return check.withinAllowedRoot; }