From 3cbcbbc6a7a02dc655b052507fed2ba2ac9bae1e Mon Sep 17 00:00:00 2001 From: tuanadr Date: Wed, 20 May 2026 18:18:54 +0700 Subject: [PATCH] Add artifact retention tombstone ledger --- artifact-retention-tombstone-ledger/README.md | 13 ++ .../acceptance-notes.md | 7 + .../demo-output/demo.mp4 | Bin 0 -> 53494 bytes .../demo-output/demo.svg | 13 ++ .../demo-output/disposal-review.md | 13 ++ .../demo-output/tombstone-ledger-packet.json | 141 +++++++++++++ artifact-retention-tombstone-ledger/demo.js | 75 +++++++ artifact-retention-tombstone-ledger/index.js | 189 ++++++++++++++++++ .../requirements-map.md | 12 ++ .../sample-data.js | 108 ++++++++++ artifact-retention-tombstone-ledger/test.js | 59 ++++++ 11 files changed, 630 insertions(+) create mode 100644 artifact-retention-tombstone-ledger/README.md create mode 100644 artifact-retention-tombstone-ledger/acceptance-notes.md create mode 100644 artifact-retention-tombstone-ledger/demo-output/demo.mp4 create mode 100644 artifact-retention-tombstone-ledger/demo-output/demo.svg create mode 100644 artifact-retention-tombstone-ledger/demo-output/disposal-review.md create mode 100644 artifact-retention-tombstone-ledger/demo-output/tombstone-ledger-packet.json create mode 100644 artifact-retention-tombstone-ledger/demo.js create mode 100644 artifact-retention-tombstone-ledger/index.js create mode 100644 artifact-retention-tombstone-ledger/requirements-map.md create mode 100644 artifact-retention-tombstone-ledger/sample-data.js create mode 100644 artifact-retention-tombstone-ledger/test.js diff --git a/artifact-retention-tombstone-ledger/README.md b/artifact-retention-tombstone-ledger/README.md new file mode 100644 index 0000000..1281dc3 --- /dev/null +++ b/artifact-retention-tombstone-ledger/README.md @@ -0,0 +1,13 @@ +# Artifact Retention Tombstone Ledger + +This self-contained slice reviews hosted scientific artifacts before deletion or disposal. +It preserves citation continuity and checksum evidence by deciding whether an artifact must be retained, tombstoned, or safely disposed. + +## Run locally + +```bash +node artifact-retention-tombstone-ledger/test.js +node artifact-retention-tombstone-ledger/demo.js +``` + +Demo outputs are written to `artifact-retention-tombstone-ledger/demo-output/`. diff --git a/artifact-retention-tombstone-ledger/acceptance-notes.md b/artifact-retention-tombstone-ledger/acceptance-notes.md new file mode 100644 index 0000000..5d70171 --- /dev/null +++ b/artifact-retention-tombstone-ledger/acceptance-notes.md @@ -0,0 +1,7 @@ +# Acceptance Notes + +- The module is dependency-free and uses synthetic data only. +- Tests cover active holds, unelapsed retention, missing checksums, DOI/citation tombstone requirements, and destructive delete prevention. +- The ready-path test proves an eligible artifact can be disposed with a tombstone packet. +- Demo artifacts include JSON, Markdown, SVG, and MP4 output. +- The slice is distinct from prior #14 submissions around FAIR manifests, compute governance, provenance, quarantine, quota/deduplication, preview cache, raw instrument preview, and notebook preview. diff --git a/artifact-retention-tombstone-ledger/demo-output/demo.mp4 b/artifact-retention-tombstone-ledger/demo-output/demo.mp4 new file mode 100644 index 0000000000000000000000000000000000000000..841336540c8fc9d3228658e27269995b7014a47a GIT binary patch literal 53494 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 + + + Artifact Retention Tombstone Ledger + hosting-cardio-fluid-dynamics + 5 disposal blockers + 1. raw-trials-2024: active_policy_hold + 2. simulation-cache-v3: checksum_manifest_missing + 3. derived-public-csv: citation_tombstone_required + 4. notebook-results-v1: destructive_delete_not_allowed + 5. raw-trials-2024: retention_window_not_elapsed + audit digest: fa494d5362739c320e1bdcc1... + diff --git a/artifact-retention-tombstone-ledger/demo-output/disposal-review.md b/artifact-retention-tombstone-ledger/demo-output/disposal-review.md new file mode 100644 index 0000000..e703c72 --- /dev/null +++ b/artifact-retention-tombstone-ledger/demo-output/disposal-review.md @@ -0,0 +1,13 @@ +# Artifact retention and tombstone review + +Project: hosting-cardio-fluid-dynamics +Status: hold + +## Holds +- raw-trials-2024: Clear or document the legal, IRB, funder, or citation hold before disposal. +- simulation-cache-v3: Generate and preserve SHA-256 evidence before any deletion or tombstone export. +- derived-public-csv: Record DOI, title, type, checksum, license, and replacement URI before disposal. +- notebook-results-v1: Use a tombstone-preserving disposal mode for cited scientific artifacts. +- raw-trials-2024: Wait until the retention window elapses or obtain an explicit retention override. + +Audit digest: fa494d5362739c320e1bdcc1764ea662c55944682a87715de046142c29f13568 diff --git a/artifact-retention-tombstone-ledger/demo-output/tombstone-ledger-packet.json b/artifact-retention-tombstone-ledger/demo-output/tombstone-ledger-packet.json new file mode 100644 index 0000000..4896795 --- /dev/null +++ b/artifact-retention-tombstone-ledger/demo-output/tombstone-ledger-packet.json @@ -0,0 +1,141 @@ +{ + "packetType": "artifact-retention-tombstone-ledger", + "projectId": "hosting-cardio-fluid-dynamics", + "generatedAt": "2026-05-20T12:00:00Z", + "overallStatus": "hold", + "decisions": [ + { + "artifactId": "raw-trials-2024", + "decision": "hold", + "tombstoneRequired": false, + "retentionUntil": "2027-01-01T00:00:00Z", + "disposalMode": "tombstone" + }, + { + "artifactId": "simulation-cache-v3", + "decision": "hold", + "tombstoneRequired": false, + "retentionUntil": "2025-01-01T00:00:00Z", + "disposalMode": "delete" + }, + { + "artifactId": "derived-public-csv", + "decision": "tombstone_required", + "tombstoneRequired": true, + "retentionUntil": "2025-01-01T00:00:00Z", + "disposalMode": "tombstone" + }, + { + "artifactId": "notebook-results-v1", + "decision": "hold", + "tombstoneRequired": true, + "retentionUntil": "2025-01-01T00:00:00Z", + "disposalMode": "hard_delete" + } + ], + "holds": [ + { + "artifactId": "raw-trials-2024", + "code": "active_policy_hold", + "owner": "data-steward", + "severity": "blocker", + "evidence": "raw-trials-2024 has active hold(s): irb-hold.", + "requiredAction": "Clear or document the legal, IRB, funder, or citation hold before disposal." + }, + { + "artifactId": "simulation-cache-v3", + "code": "checksum_manifest_missing", + "owner": "compute-admin", + "severity": "blocker", + "evidence": "simulation-cache-v3 is missing a checksum manifest.", + "requiredAction": "Generate and preserve SHA-256 evidence before any deletion or tombstone export." + }, + { + "artifactId": "derived-public-csv", + "code": "citation_tombstone_required", + "owner": "repository-owner", + "severity": "blocker", + "evidence": "derived-public-csv is cited or DOI-backed but lacks complete tombstone metadata.", + "requiredAction": "Record DOI, title, type, checksum, license, and replacement URI before disposal." + }, + { + "artifactId": "notebook-results-v1", + "code": "destructive_delete_not_allowed", + "owner": "repository-owner", + "severity": "blocker", + "evidence": "notebook-results-v1 requested hard delete despite citation dependencies.", + "requiredAction": "Use a tombstone-preserving disposal mode for cited scientific artifacts." + }, + { + "artifactId": "raw-trials-2024", + "code": "retention_window_not_elapsed", + "owner": "data-steward", + "severity": "blocker", + "evidence": "raw-trials-2024 must be retained until 2027-01-01T00:00:00Z.", + "requiredAction": "Wait until the retention window elapses or obtain an explicit retention override." + } + ], + "tombstonePlan": [ + { + "artifactId": "raw-trials-2024", + "decision": "hold", + "requiredMetadata": { + "doi": null, + "title": "Raw protected waveform trials", + "artifactType": "dataset", + "sha256": "4f8a6c0f6a3e7c85d236d2dd7251c2a6d4c6f5974f4e1a17c95bbf58b9fd0041", + "license": "restricted-dua", + "replacementUri": null, + "disposalReason": "superseded protected dataset" + }, + "preserveChecksum": true, + "publicLandingPage": false + }, + { + "artifactId": "simulation-cache-v3", + "decision": "hold", + "requiredMetadata": { + "doi": null, + "title": "Simulation cache v3", + "artifactType": "model-cache", + "sha256": null, + "license": "internal", + "replacementUri": null, + "disposalReason": "cache eviction" + }, + "preserveChecksum": false, + "publicLandingPage": false + }, + { + "artifactId": "derived-public-csv", + "decision": "tombstone_required", + "requiredMetadata": { + "doi": "10.5555/scibase.derived-pressure", + "title": "Derived public pressure table", + "artifactType": "dataset", + "sha256": "9b340ad03e9c4d2fdc1768c374fd898d021a7e11a62f1c812f12792b3e65cf3d", + "license": "cc-by-4.0", + "replacementUri": "https://doi.org/10.5555/scibase.derived-pressure-v2", + "disposalReason": "replaced by corrected table" + }, + "preserveChecksum": true, + "publicLandingPage": true + }, + { + "artifactId": "notebook-results-v1", + "decision": "hold", + "requiredMetadata": { + "doi": "10.5555/scibase.notebook-results-v1", + "title": "Notebook results v1", + "artifactType": "notebook", + "sha256": "ce8a3b1337fb61e3a0b273a7f085d45f943fda03f852e37caad06cc8215998e6", + "license": "cc-by-4.0", + "replacementUri": null, + "disposalReason": "author requested cleanup" + }, + "preserveChecksum": true, + "publicLandingPage": true + } + ], + "auditDigest": "fa494d5362739c320e1bdcc1764ea662c55944682a87715de046142c29f13568" +} diff --git a/artifact-retention-tombstone-ledger/demo.js b/artifact-retention-tombstone-ledger/demo.js new file mode 100644 index 0000000..245243b --- /dev/null +++ b/artifact-retention-tombstone-ledger/demo.js @@ -0,0 +1,75 @@ +const fs = require('fs'); +const path = require('path'); +const { execFileSync } = require('child_process'); +const { buildTombstoneLedgerPacket } = require('./index'); +const { sampleHostingProject } = require('./sample-data'); + +const outputDir = path.join(__dirname, 'demo-output'); +fs.mkdirSync(outputDir, { recursive: true }); + +const packet = buildTombstoneLedgerPacket(sampleHostingProject, { + asOf: '2026-05-20T12:00:00Z', +}); + +fs.writeFileSync(path.join(outputDir, 'tombstone-ledger-packet.json'), `${JSON.stringify(packet, null, 2)}\n`); + +const rows = packet.holds + .map((hold, index) => `${index + 1}. ${hold.artifactId}: ${hold.code}`) + .join('\n '); + +fs.writeFileSync(path.join(outputDir, 'demo.svg'), ` + + + Artifact Retention Tombstone Ledger + ${packet.projectId} + ${packet.holds.length} disposal blockers + ${rows} + audit digest: ${packet.auditDigest.slice(0, 24)}... + +`); + +fs.writeFileSync(path.join(outputDir, 'disposal-review.md'), [ + '# Artifact retention and tombstone review', + '', + `Project: ${packet.projectId}`, + `Status: ${packet.overallStatus}`, + '', + '## Holds', + ...packet.holds.map((hold) => `- ${hold.artifactId}: ${hold.requiredAction}`), + '', + `Audit digest: ${packet.auditDigest}`, + '', +].join('\n')); + +function renderMp4() { + const videoPath = path.join(outputDir, 'demo.mp4'); + const font = 'C\\:/Windows/Fonts/arial.ttf'; + const escapeText = (value) => String(value).replace(/\\/g, '\\\\').replace(/:/g, '\\:').replace(/'/g, "\\'"); + const filters = [ + `drawtext=fontfile='${font}':text='${escapeText('Artifact Retention Tombstone Ledger')}':x=70:y=80:fontsize=41:fontcolor=white`, + `drawtext=fontfile='${font}':text='${escapeText(`${packet.holds.length} disposal blockers before deletion`)}':x=70:y=155:fontsize=32:fontcolor=0xffd166`, + ...packet.holds.map((hold, index) => + `drawtext=fontfile='${font}':text='${escapeText(`${hold.artifactId}: ${hold.code}`)}':x=90:y=${230 + index * 55}:fontsize=26:fontcolor=white`, + ), + `drawtext=fontfile='${font}':text='${escapeText(`audit ${packet.auditDigest.slice(0, 20)}...`)}':x=70:y=630:fontsize=24:fontcolor=0x93c5fd`, + ].join(','); + + execFileSync('ffmpeg', [ + '-y', + '-f', + 'lavfi', + '-i', + 'color=c=0x172554:s=1280x720:d=7', + '-vf', + filters, + '-c:v', + 'libx264', + '-pix_fmt', + 'yuv420p', + videoPath, + ], { stdio: 'inherit' }); +} + +renderMp4(); + +console.log(`Wrote demo artifacts to ${outputDir}`); diff --git a/artifact-retention-tombstone-ledger/index.js b/artifact-retention-tombstone-ledger/index.js new file mode 100644 index 0000000..5eef4ce --- /dev/null +++ b/artifact-retention-tombstone-ledger/index.js @@ -0,0 +1,189 @@ +const crypto = require('crypto'); + +function stableStringify(value) { + if (Array.isArray(value)) { + return `[${value.map(stableStringify).join(',')}]`; + } + if (value && typeof value === 'object') { + return `{${Object.keys(value) + .sort() + .map((key) => `${JSON.stringify(key)}:${stableStringify(value[key])}`) + .join(',')}}`; + } + return JSON.stringify(value); +} + +function digest(value) { + return crypto.createHash('sha256').update(stableStringify(value)).digest('hex'); +} + +function toTime(value) { + return new Date(value).getTime(); +} + +function addHold(holds, artifact, code, owner, evidence, requiredAction) { + holds.push({ + artifactId: artifact.artifactId, + code, + owner, + severity: 'blocker', + evidence, + requiredAction, + }); +} + +function tombstoneMetadataFor(artifact) { + return { + doi: artifact.doi || null, + title: artifact.title, + artifactType: artifact.artifactType, + sha256: artifact.sha256 || null, + license: artifact.license, + replacementUri: artifact.replacementUri || null, + disposalReason: artifact.disposalRequest.reason, + }; +} + +function evaluateArtifact(artifact, options) { + const holds = []; + + if (artifact.policyHolds.length > 0) { + addHold( + holds, + artifact, + 'active_policy_hold', + artifact.owner, + `${artifact.artifactId} has active hold(s): ${artifact.policyHolds.join(', ')}.`, + 'Clear or document the legal, IRB, funder, or citation hold before disposal.', + ); + } + + if (toTime(artifact.retentionUntil) > toTime(options.asOf)) { + addHold( + holds, + artifact, + 'retention_window_not_elapsed', + artifact.owner, + `${artifact.artifactId} must be retained until ${artifact.retentionUntil}.`, + 'Wait until the retention window elapses or obtain an explicit retention override.', + ); + } + + if (!artifact.sha256) { + addHold( + holds, + artifact, + 'checksum_manifest_missing', + artifact.owner, + `${artifact.artifactId} is missing a checksum manifest.`, + 'Generate and preserve SHA-256 evidence before any deletion or tombstone export.', + ); + } + + const hasCitationDependency = artifact.doi || artifact.citationCount > 0; + if (hasCitationDependency && !artifact.tombstoneMetadataComplete) { + addHold( + holds, + artifact, + 'citation_tombstone_required', + artifact.owner, + `${artifact.artifactId} is cited or DOI-backed but lacks complete tombstone metadata.`, + 'Record DOI, title, type, checksum, license, and replacement URI before disposal.', + ); + } + + if (artifact.disposalRequest.mode === 'hard_delete' && hasCitationDependency) { + addHold( + holds, + artifact, + 'destructive_delete_not_allowed', + artifact.owner, + `${artifact.artifactId} requested hard delete despite citation dependencies.`, + 'Use a tombstone-preserving disposal mode for cited scientific artifacts.', + ); + } + + let decision = 'retain'; + if (holds.length > 0) { + decision = holds.some((hold) => hold.code === 'citation_tombstone_required') && holds.length === 1 + ? 'tombstone_required' + : 'hold'; + } else if (artifact.disposalRequest.requested) { + decision = artifact.doi || artifact.citationCount > 0 ? 'dispose_with_tombstone' : 'dispose'; + } + + return { + decision: { + artifactId: artifact.artifactId, + decision, + tombstoneRequired: Boolean(artifact.doi || artifact.citationCount > 0), + retentionUntil: artifact.retentionUntil, + disposalMode: artifact.disposalRequest.mode, + }, + holds, + tombstonePlan: { + artifactId: artifact.artifactId, + decision, + requiredMetadata: tombstoneMetadataFor(artifact), + preserveChecksum: Boolean(artifact.sha256), + publicLandingPage: Boolean(artifact.doi || artifact.citationCount > 0), + }, + }; +} + +function evaluateArtifactRetention(project, options = {}) { + const evaluationOptions = { + asOf: options.asOf || new Date().toISOString(), + }; + const decisions = []; + const holds = []; + const tombstonePlan = []; + + for (const artifact of project.artifacts) { + const result = evaluateArtifact(artifact, evaluationOptions); + decisions.push(result.decision); + holds.push(...result.holds); + tombstonePlan.push(result.tombstonePlan); + } + + const sortedHolds = holds.sort((left, right) => left.code.localeCompare(right.code)); + + return { + projectId: project.projectId, + overallStatus: sortedHolds.length > 0 ? 'hold' : 'ready', + summary: { + artifactsReviewed: project.artifacts.length, + blockingHolds: sortedHolds.length, + disposalReady: decisions.filter((decision) => decision.decision.startsWith('dispose')).length, + tombstonesRequired: decisions.filter((decision) => decision.tombstoneRequired).length, + }, + decisions, + holds: sortedHolds, + tombstonePlan, + }; +} + +function buildTombstoneLedgerPacket(project, options = {}) { + const review = evaluateArtifactRetention(project, options); + const packet = { + packetType: 'artifact-retention-tombstone-ledger', + projectId: review.projectId, + generatedAt: options.asOf || new Date().toISOString(), + overallStatus: review.overallStatus, + decisions: review.decisions, + holds: review.holds, + tombstonePlan: review.tombstonePlan, + }; + + return { + ...packet, + auditDigest: digest(packet), + }; +} + +module.exports = { + evaluateArtifactRetention, + buildTombstoneLedgerPacket, + stableStringify, + digest, +}; diff --git a/artifact-retention-tombstone-ledger/requirements-map.md b/artifact-retention-tombstone-ledger/requirements-map.md new file mode 100644 index 0000000..4bbf9c7 --- /dev/null +++ b/artifact-retention-tombstone-ledger/requirements-map.md @@ -0,0 +1,12 @@ +# Requirements Map + +Issue #14 asks for scientific and engineering data/code hosting, including metadata, access, versioning, reproducibility, storage, and export workflows. + +| Requirement area | Coverage in this slice | +| --- | --- | +| Hosted artifact lifecycle | Reviews deletion requests for datasets, model caches, notebooks, and derived tables. | +| Metadata continuity | Requires DOI, title, type, checksum, license, and replacement URI in tombstone records. | +| Integrity | Blocks disposal when checksum manifests are missing. | +| Retention and compliance | Enforces IRB, legal, funder, citation, and retention-window holds. | +| Public repository trust | Prevents destructive deletion of DOI-backed or cited artifacts. | +| Export readiness | Emits deterministic JSON/Markdown/SVG/MP4 evidence packets for reviewers. | diff --git a/artifact-retention-tombstone-ledger/sample-data.js b/artifact-retention-tombstone-ledger/sample-data.js new file mode 100644 index 0000000..f871aeb --- /dev/null +++ b/artifact-retention-tombstone-ledger/sample-data.js @@ -0,0 +1,108 @@ +const sampleHostingProject = { + projectId: 'hosting-cardio-fluid-dynamics', + artifacts: [ + { + artifactId: 'raw-trials-2024', + title: 'Raw protected waveform trials', + artifactType: 'dataset', + owner: 'data-steward', + license: 'restricted-dua', + doi: null, + citationCount: 0, + retentionUntil: '2027-01-01T00:00:00Z', + policyHolds: ['irb-hold'], + sha256: '4f8a6c0f6a3e7c85d236d2dd7251c2a6d4c6f5974f4e1a17c95bbf58b9fd0041', + tombstoneMetadataComplete: true, + disposalRequest: { + requested: true, + mode: 'tombstone', + reason: 'superseded protected dataset', + }, + }, + { + artifactId: 'simulation-cache-v3', + title: 'Simulation cache v3', + artifactType: 'model-cache', + owner: 'compute-admin', + license: 'internal', + doi: null, + citationCount: 0, + retentionUntil: '2025-01-01T00:00:00Z', + policyHolds: [], + sha256: null, + tombstoneMetadataComplete: true, + disposalRequest: { + requested: true, + mode: 'delete', + reason: 'cache eviction', + }, + }, + { + artifactId: 'derived-public-csv', + title: 'Derived public pressure table', + artifactType: 'dataset', + owner: 'repository-owner', + license: 'cc-by-4.0', + doi: '10.5555/scibase.derived-pressure', + citationCount: 8, + retentionUntil: '2025-01-01T00:00:00Z', + policyHolds: [], + sha256: '9b340ad03e9c4d2fdc1768c374fd898d021a7e11a62f1c812f12792b3e65cf3d', + tombstoneMetadataComplete: false, + replacementUri: 'https://doi.org/10.5555/scibase.derived-pressure-v2', + disposalRequest: { + requested: true, + mode: 'tombstone', + reason: 'replaced by corrected table', + }, + }, + { + artifactId: 'notebook-results-v1', + title: 'Notebook results v1', + artifactType: 'notebook', + owner: 'repository-owner', + license: 'cc-by-4.0', + doi: '10.5555/scibase.notebook-results-v1', + citationCount: 3, + retentionUntil: '2025-01-01T00:00:00Z', + policyHolds: [], + sha256: 'ce8a3b1337fb61e3a0b273a7f085d45f943fda03f852e37caad06cc8215998e6', + tombstoneMetadataComplete: true, + disposalRequest: { + requested: true, + mode: 'hard_delete', + reason: 'author requested cleanup', + }, + }, + ], +}; + +const disposalReadyProject = { + projectId: 'hosting-open-microscopy', + artifacts: [ + { + artifactId: 'microscopy-derived-v1', + title: 'Microscopy derived masks v1', + artifactType: 'dataset', + owner: 'repository-owner', + license: 'cc-by-4.0', + doi: '10.5555/scibase.microscopy-derived-v1', + citationCount: 2, + retentionUntil: '2025-01-01T00:00:00Z', + policyHolds: [], + sha256: '01ab8d9e2f7c6f7bd88f32ce2a758aa9f26adbc5bc47d2cece9a9d9ea4b76411', + tombstoneMetadataComplete: true, + replacementUri: 'https://doi.org/10.5555/scibase.microscopy-derived-v2', + disposalRequest: { + requested: true, + mode: 'tombstone', + reason: 'superseded by v2 segmentation masks', + }, + }, + ], +}; + +module.exports = { + sampleHostingProject, + disposalReadyProject, +}; diff --git a/artifact-retention-tombstone-ledger/test.js b/artifact-retention-tombstone-ledger/test.js new file mode 100644 index 0000000..b1a2869 --- /dev/null +++ b/artifact-retention-tombstone-ledger/test.js @@ -0,0 +1,59 @@ +const assert = require('assert'); +const { + evaluateArtifactRetention, + buildTombstoneLedgerPacket, +} = require('./index'); +const { sampleHostingProject, disposalReadyProject } = require('./sample-data'); + +function testRetentionAndHoldBlockers() { + const review = evaluateArtifactRetention(sampleHostingProject, { + asOf: '2026-05-20T12:00:00Z', + }); + + assert.equal(review.overallStatus, 'hold'); + assert.equal(review.summary.blockingHolds, 5); + assert.ok(review.holds.some((hold) => hold.code === 'active_policy_hold')); + assert.ok(review.holds.some((hold) => hold.code === 'retention_window_not_elapsed')); + assert.ok(review.holds.some((hold) => hold.code === 'checksum_manifest_missing')); + assert.ok(review.holds.some((hold) => hold.code === 'citation_tombstone_required')); + assert.ok(review.holds.some((hold) => hold.code === 'destructive_delete_not_allowed')); + assert.equal(review.decisions.find((decision) => decision.artifactId === 'raw-trials-2024').decision, 'hold'); + assert.equal(review.decisions.find((decision) => decision.artifactId === 'derived-public-csv').decision, 'tombstone_required'); +} + +function testLedgerPacketPreservesAuditEvidence() { + const packet = buildTombstoneLedgerPacket(sampleHostingProject, { + asOf: '2026-05-20T12:00:00Z', + }); + + assert.match(packet.auditDigest, /^[a-f0-9]{64}$/); + assert.equal(packet.tombstonePlan.length, 4); + assert.ok(packet.tombstonePlan.some((item) => item.artifactId === 'derived-public-csv' && item.requiredMetadata.doi)); + assert.deepEqual( + packet.holds.map((hold) => hold.code), + [ + 'active_policy_hold', + 'checksum_manifest_missing', + 'citation_tombstone_required', + 'destructive_delete_not_allowed', + 'retention_window_not_elapsed', + ], + ); +} + +function testEligibleDisposalProducesTombstonePacket() { + const review = evaluateArtifactRetention(disposalReadyProject, { + asOf: '2026-05-20T12:00:00Z', + }); + + assert.equal(review.overallStatus, 'ready'); + assert.equal(review.summary.blockingHolds, 0); + assert.equal(review.decisions[0].decision, 'dispose_with_tombstone'); + assert.equal(review.decisions[0].tombstoneRequired, true); +} + +testRetentionAndHoldBlockers(); +testLedgerPacketPreservesAuditEvidence(); +testEligibleDisposalProducesTombstonePacket(); + +console.log('artifact-retention-tombstone-ledger tests passed');