{"version":3,"file":"retention.d.ts","sourceRoot":"","sources":["../../../src/core/evaluation/retention.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,uBAAuB,EAAE,yBAAyB,EAAE,MAAM,YAAY,CAAC;AAErF,eAAO,MAAM,mCAAmC,EAAE,yBAyBjD,CAAC;AAEF,MAAM,WAAW,iBAAiB;IACjC,UAAU,EAAE,MAAM,CAAC;IACnB,cAAc,EAAE,MAAM,CAAC;IACvB,SAAS,EAAE,OAAO,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,cAAc,EAAE,MAAM,CAAC;CACvB;AAMD,wBAAsB,uBAAuB,CAC5C,IAAI,EAAE,MAAM,EACZ,MAAM,4BAAsC,GAC1C,OAAO,CAAC,iBAAiB,EAAE,CAAC,CAoD9B;AAED,wBAAsB,mBAAmB,CACxC,IAAI,EAAE,MAAM,EACZ,MAAM,4BAAsC,GAC1C,OAAO,CAAC,uBAAuB,CAAC,CAwBlC;AAED,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,yBAAyB,GAAG,IAAI,CAgB7E;AAED,wBAAsB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,yBAAyB,CAAC,CAI1F","sourcesContent":["import { randomUUID } from \"node:crypto\";\nimport { readdir, readFile, stat } from \"node:fs/promises\";\nimport { join, resolve } from \"node:path\";\nimport { listArtifacts } from \"./artifacts.js\";\nimport { listBaselines } from \"./baselines.js\";\nimport { hashJson } from \"./identity.js\";\nimport type { EvaluationPruneManifest, EvaluationRetentionPolicy } from \"./types.js\";\n\nexport const DEFAULT_EVALUATION_RETENTION_POLICY: EvaluationRetentionPolicy = {\n\tpolicyVersion: 1,\n\trules: [\n\t\t{ class: \"release_baseline\", preserveWhenReferenced: true, preserveForRelease: true },\n\t\t{ class: \"release_gate_evidence\", preserveWhenReferenced: true, preserveForRelease: true },\n\t\t{ class: \"active_comparison\", preserveWhenReferenced: true, preserveForRelease: true },\n\t\t{ class: \"failure_cluster_evidence\", preserveWhenReferenced: true, preserveForRelease: false },\n\t\t{ class: \"human_annotation\", preserveWhenReferenced: true, preserveForRelease: false },\n\t\t{\n\t\t\tclass: \"ordinary_completed_run\",\n\t\t\tmaximumRetentionDays: 30,\n\t\t\tpreserveWhenReferenced: true,\n\t\t\tpreserveForRelease: false,\n\t\t},\n\t\t{ class: \"failed_run\", maximumRetentionDays: 30, preserveWhenReferenced: true, preserveForRelease: false },\n\t\t{ class: \"cancelled_run\", maximumRetentionDays: 7, preserveWhenReferenced: true, preserveForRelease: false },\n\t\t{ class: \"temporary_sandbox\", maximumRetentionDays: 1, preserveWhenReferenced: false, preserveForRelease: false },\n\t\t{\n\t\t\tclass: \"live_provider_trace\",\n\t\t\tmaximumRetentionDays: 7,\n\t\t\tpreserveWhenReferenced: true,\n\t\t\tpreserveForRelease: false,\n\t\t},\n\t\t{ class: \"raw_log\", maximumRetentionDays: 7, preserveWhenReferenced: false, preserveForRelease: false },\n\t],\n};\n\nexport interface RetentionDecision {\n\tartifactId: string;\n\tretentionClass: string;\n\tprotected: boolean;\n\treasonCode: string;\n\testimatedBytes: number;\n}\n\nfunction ruleFor(policy: EvaluationRetentionPolicy, retentionClass: string) {\n\treturn policy.rules.find((rule) => rule.class === retentionClass);\n}\n\nexport async function classifyEvaluationStore(\n\troot: string,\n\tpolicy = DEFAULT_EVALUATION_RETENTION_POLICY,\n): Promise<RetentionDecision[]> {\n\tconst storeRoot = resolve(root);\n\tconst artifacts = await listArtifacts(storeRoot).catch(() => []);\n\tconst baselines = await listBaselines(join(storeRoot, \"baselines\"));\n\tconst protectedArtifactIds = new Set(baselines.flatMap((baseline) => baseline.artifactIds));\n\tconst decisions: RetentionDecision[] = [];\n\tfor (const artifact of artifacts) {\n\t\tconst retentionClass = protectedArtifactIds.has(artifact.artifactHash)\n\t\t\t? \"release_baseline\"\n\t\t\t: artifact.run.status === \"failed\"\n\t\t\t\t? \"failed_run\"\n\t\t\t\t: artifact.run.status === \"cancelled\"\n\t\t\t\t\t? \"cancelled_run\"\n\t\t\t\t\t: artifact.run.mode === \"live\"\n\t\t\t\t\t\t? \"live_provider_trace\"\n\t\t\t\t\t\t: \"ordinary_completed_run\";\n\t\tconst rule = ruleFor(policy, retentionClass);\n\t\tconst bytes = Buffer.byteLength(JSON.stringify(artifact));\n\t\tconst ageDays = Math.max(0, (Date.now() - Date.parse(artifact.provenance.createdAt)) / 86_400_000);\n\t\tconst withinRetentionWindow = rule?.maximumRetentionDays !== undefined && ageDays <= rule.maximumRetentionDays;\n\t\tdecisions.push({\n\t\t\tartifactId: artifact.artifactHash,\n\t\t\tretentionClass,\n\t\t\tprotected:\n\t\t\t\trule?.preserveForRelease === true ||\n\t\t\t\tprotectedArtifactIds.has(artifact.artifactHash) ||\n\t\t\t\twithinRetentionWindow,\n\t\t\treasonCode: protectedArtifactIds.has(artifact.artifactHash)\n\t\t\t\t? \"REFERENCED_BY_ACTIVE_BASELINE\"\n\t\t\t\t: withinRetentionWindow\n\t\t\t\t\t? \"RETENTION_WINDOW_ACTIVE\"\n\t\t\t\t\t: \"ARTIFACT_CLASS_POLICY\",\n\t\t\testimatedBytes: bytes,\n\t\t});\n\t}\n\tconst temporaryRoot = join(storeRoot, \"temporary\");\n\tconst entries = await readdir(temporaryRoot, { withFileTypes: true }).catch((error: unknown) => {\n\t\tif ((error as NodeJS.ErrnoException).code === \"ENOENT\") return [];\n\t\tthrow error;\n\t});\n\tfor (const entry of entries.sort((left, right) => left.name.localeCompare(right.name))) {\n\t\tconst path = join(temporaryRoot, entry.name);\n\t\tconst bytes = entry.isFile() ? (await stat(path)).size : 0;\n\t\tdecisions.push({\n\t\t\tartifactId: `temporary/${entry.name}`,\n\t\t\tretentionClass: \"temporary_sandbox\",\n\t\t\tprotected: false,\n\t\t\treasonCode: \"TEMPORARY_EXPIRED_OR_RECOVERABLE\",\n\t\t\testimatedBytes: bytes,\n\t\t});\n\t}\n\treturn decisions.sort((left, right) => left.artifactId.localeCompare(right.artifactId));\n}\n\nexport async function createPruneManifest(\n\troot: string,\n\tpolicy = DEFAULT_EVALUATION_RETENTION_POLICY,\n): Promise<EvaluationPruneManifest> {\n\tconst decisions = await classifyEvaluationStore(root, policy);\n\tconst unsigned = {\n\t\tpolicyVersion: policy.policyVersion,\n\t\tentries: decisions\n\t\t\t.filter((decision) => !decision.protected)\n\t\t\t.map((decision) => ({\n\t\t\t\tartifactId: decision.artifactId,\n\t\t\t\tretentionClass: decision.retentionClass,\n\t\t\t\treasonCode: decision.reasonCode,\n\t\t\t\testimatedBytes: decision.estimatedBytes,\n\t\t\t})),\n\t\tprotectedEntries: decisions\n\t\t\t.filter((decision) => decision.protected)\n\t\t\t.map((decision) => ({\n\t\t\t\tartifactId: decision.artifactId,\n\t\t\t\treasonCode: decision.reasonCode,\n\t\t\t})),\n\t};\n\treturn {\n\t\tmanifestId: `prune-${hashJson(unsigned).slice(0, 24)}-${randomUUID().slice(0, 8)}`,\n\t\tcreatedAt: new Date().toISOString(),\n\t\t...unsigned,\n\t};\n}\n\nexport function verifyRetentionPolicy(policy: EvaluationRetentionPolicy): void {\n\tif (!Number.isInteger(policy.policyVersion) || policy.policyVersion < 1)\n\t\tthrow new Error(\"retention policy version is invalid\");\n\tfor (const rule of policy.rules) {\n\t\tif (!rule.class.trim()) throw new Error(\"retention class is required\");\n\t\tif (rule.minimumRetentionDays !== undefined && rule.minimumRetentionDays < 0)\n\t\t\tthrow new Error(\"minimum retention cannot be negative\");\n\t\tif (rule.maximumRetentionDays !== undefined && rule.maximumRetentionDays < 0)\n\t\t\tthrow new Error(\"maximum retention cannot be negative\");\n\t\tif (\n\t\t\trule.minimumRetentionDays !== undefined &&\n\t\t\trule.maximumRetentionDays !== undefined &&\n\t\t\trule.minimumRetentionDays > rule.maximumRetentionDays\n\t\t)\n\t\t\tthrow new Error(`retention bounds are invalid for ${rule.class}`);\n\t}\n}\n\nexport async function readRetentionPolicy(path: string): Promise<EvaluationRetentionPolicy> {\n\tconst policy = JSON.parse(await readFile(path, \"utf8\")) as EvaluationRetentionPolicy;\n\tverifyRetentionPolicy(policy);\n\treturn policy;\n}\n"]}