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 0000000..8413365 Binary files /dev/null and b/artifact-retention-tombstone-ledger/demo-output/demo.mp4 differ diff --git a/artifact-retention-tombstone-ledger/demo-output/demo.svg b/artifact-retention-tombstone-ledger/demo-output/demo.svg new file mode 100644 index 0000000..0c97909 --- /dev/null +++ b/artifact-retention-tombstone-ledger/demo-output/demo.svg @@ -0,0 +1,13 @@ + + + + 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');