From dab8e70d2a7ab72c43a83446e9e3f60a566904bb Mon Sep 17 00:00:00 2001 From: "tho.nguyen" <91511523+haki203@users.noreply.github.com> Date: Fri, 22 May 2026 22:55:21 +0700 Subject: [PATCH] Add artifact replica consistency guard --- artifact-replica-consistency-guard/README.md | 32 +++ artifact-replica-consistency-guard/demo.js | 147 ++++++++++ artifact-replica-consistency-guard/index.js | 262 ++++++++++++++++++ .../reports/demo.mp4 | Bin 0 -> 6609 bytes .../reports/replica-consistency-packet.json | 185 +++++++++++++ .../reports/replica-consistency-review.md | 72 +++++ .../reports/summary.svg | 23 ++ .../requirements-map.md | 19 ++ .../sample-data.js | 76 +++++ artifact-replica-consistency-guard/test.js | 115 ++++++++ 10 files changed, 931 insertions(+) create mode 100644 artifact-replica-consistency-guard/README.md create mode 100644 artifact-replica-consistency-guard/demo.js create mode 100644 artifact-replica-consistency-guard/index.js create mode 100644 artifact-replica-consistency-guard/reports/demo.mp4 create mode 100644 artifact-replica-consistency-guard/reports/replica-consistency-packet.json create mode 100644 artifact-replica-consistency-guard/reports/replica-consistency-review.md create mode 100644 artifact-replica-consistency-guard/reports/summary.svg create mode 100644 artifact-replica-consistency-guard/requirements-map.md create mode 100644 artifact-replica-consistency-guard/sample-data.js create mode 100644 artifact-replica-consistency-guard/test.js diff --git a/artifact-replica-consistency-guard/README.md b/artifact-replica-consistency-guard/README.md new file mode 100644 index 00000000..42e4ae45 --- /dev/null +++ b/artifact-replica-consistency-guard/README.md @@ -0,0 +1,32 @@ +# Artifact Replica Consistency Guard + +Self-contained reviewer module for issue #14, Scientific/Engineering Data & Code Hosting. + +The guard validates hosted artifact replicas before persistent links, previews, export bundles, or reproduce buttons are enabled. It focuses on storage-mirror consistency for datasets, notebooks, model weights, and other scientific artifacts. + +## What It Does + +- Compares replica checksums against the canonical artifact manifest. +- Verifies manifest version alignment across primary storage, archive copies, institutional mirrors, and public landing pages. +- Detects access-policy mismatches that could expose controlled data or hide open artifacts. +- Flags stale mirror verification and routes safe preview-only cases to repair review. +- Checks DataCite DOI and schema.org landing-page consistency before export/publication. +- Generates deterministic JSON, Markdown, SVG, and MP4 artifacts from synthetic fixtures only. + +## Files + +- `index.js` - dependency-free replica evaluator and Markdown packet builder. +- `sample-data.js` - synthetic artifacts for held, repair-review, and publish decisions. +- `test.js` - Node tests for blocking, review-only, and publication-ready replica states. +- `demo.js` - report generator and optional MP4 artifact writer. +- `requirements-map.md` - issue requirement mapping. +- `reports/` - generated reviewer artifacts. + +## Run + +```bash +node --test artifact-replica-consistency-guard/test.js +FFMPEG_PATH=/path/to/ffmpeg node artifact-replica-consistency-guard/demo.js +``` + +No storage provider, DOI registry, external mirror, credential, private artifact, or network call is used. diff --git a/artifact-replica-consistency-guard/demo.js b/artifact-replica-consistency-guard/demo.js new file mode 100644 index 00000000..a3ec4ed3 --- /dev/null +++ b/artifact-replica-consistency-guard/demo.js @@ -0,0 +1,147 @@ +const fs = require('node:fs'); +const path = require('node:path'); +const {spawnSync} = require('node:child_process'); + +const { + evaluateArtifactReplicaConsistency, + buildReplicaConsistencyPacket, +} = require('./index'); +const {scenarios} = require('./sample-data'); + +const reportsDir = path.join(__dirname, 'reports'); +const framesDir = path.join(reportsDir, 'frames'); +fs.mkdirSync(reportsDir, {recursive: true}); + +const evaluations = scenarios.map((scenario) => ({ + scenario: scenario.name, + ...evaluateArtifactReplicaConsistency(scenario), +})); + +const decisionCounts = evaluations.reduce((counts, item) => { + counts[item.decision] = (counts[item.decision] || 0) + 1; + return counts; +}, {}); +const totals = evaluations.reduce( + (sum, item) => { + sum.findings += item.summary.findingCount; + sum.blocking += item.summary.blockingFindingCount; + sum.review += item.summary.reviewFindingCount; + sum.gated += item.gatedActions.length; + return sum; + }, + {findings: 0, blocking: 0, review: 0, gated: 0} +); + +const packetJson = JSON.stringify(evaluations, null, 2); +const reviewerPacket = evaluations.map(buildReplicaConsistencyPacket).join('\n---\n'); +const svg = ` + + Artifact Replica Consistency Guard + Checks hosted artifact mirrors before previews, exports, persistent links, and reproduce buttons are enabled + + + Held + ${decisionCounts['hold-artifact'] || 0} + + + + Repair Review + ${decisionCounts['repair-review'] || 0} + + + + Publish + ${decisionCounts['publish-artifact'] || 0} + + Findings: ${totals.findings} | Blocking: ${totals.blocking} | Review: ${totals.review} | Gated actions: ${totals.gated} + Checks: checksums, manifest versions, access policy parity, mirror freshness, DataCite and schema.org landing-page consistency + Synthetic artifact manifests only. No storage provider, DOI registry, external mirror, credential, or network calls. + +`; + +fs.writeFileSync(path.join(reportsDir, 'replica-consistency-packet.json'), `${packetJson}\n`); +fs.writeFileSync(path.join(reportsDir, 'replica-consistency-review.md'), reviewerPacket); +fs.writeFileSync(path.join(reportsDir, 'summary.svg'), svg); + +function fillRect(buffer, width, x0, y0, rectWidth, rectHeight, color) { + const [r, g, b] = color; + const height = Math.floor(buffer.length / (width * 3)); + const x1 = Math.min(width, x0 + rectWidth); + const y1 = Math.min(height, y0 + rectHeight); + for (let y = Math.max(0, y0); y < y1; y += 1) { + for (let x = Math.max(0, x0); x < x1; x += 1) { + const offset = (y * width + x) * 3; + buffer[offset] = r; + buffer[offset + 1] = g; + buffer[offset + 2] = b; + } + } +} + +function writePpmFrame(filePath, frameIndex, frameCount) { + const width = 640; + const height = 360; + const buffer = Buffer.alloc(width * height * 3); + for (let i = 0; i < width * height; i += 1) { + buffer[i * 3] = 23; + buffer[i * 3 + 1] = 32; + buffer[i * 3 + 2] = 42; + } + + const progress = frameIndex / Math.max(1, frameCount - 1); + fillRect(buffer, width, 42, 42, 556, 42, [248, 250, 252]); + fillRect(buffer, width, 42, 112, 150, 118, [127, 29, 29]); + fillRect(buffer, width, 245, 112, 150, 118, [133, 77, 14]); + fillRect(buffer, width, 448, 112, 150, 118, [22, 101, 52]); + fillRect(buffer, width, 78, 260, 112, 30, [248, 113, 113]); + fillRect(buffer, width, 264, 260, 112, 30, [251, 191, 36]); + fillRect(buffer, width, 450, 260, 112, 30, [74, 222, 128]); + fillRect(buffer, width, 42, 318, Math.round(556 * progress), 18, [191, 219, 254]); + fillRect(buffer, width, 42 + Math.round(508 * progress), 309, 48, 36, [241, 245, 249]); + + const header = Buffer.from(`P6\n${width} ${height}\n255\n`); + fs.writeFileSync(filePath, Buffer.concat([header, buffer])); +} + +function createDemoVideo() { + const ffmpegPath = process.env.FFMPEG_PATH; + if (!ffmpegPath) { + console.log('FFMPEG_PATH not set; skipped MP4 generation.'); + return; + } + + fs.rmSync(framesDir, {recursive: true, force: true}); + fs.mkdirSync(framesDir, {recursive: true}); + const frameCount = 72; + for (let index = 0; index < frameCount; index += 1) { + writePpmFrame(path.join(framesDir, `frame-${String(index).padStart(3, '0')}.ppm`), index, frameCount); + } + + const output = path.join(reportsDir, 'demo.mp4'); + const result = spawnSync(ffmpegPath, [ + '-y', + '-framerate', + '24', + '-i', + path.join(framesDir, 'frame-%03d.ppm'), + '-c:v', + 'libx264', + '-pix_fmt', + 'yuv420p', + output, + ], {encoding: 'utf8'}); + + fs.rmSync(framesDir, {recursive: true, force: true}); + if (result.status !== 0) { + throw new Error(result.stderr || 'ffmpeg failed'); + } +} + +createDemoVideo(); + +console.log(JSON.stringify({ + scenarios: evaluations.length, + reportsDir, + decisions: decisionCounts, + totals, +}, null, 2)); diff --git a/artifact-replica-consistency-guard/index.js b/artifact-replica-consistency-guard/index.js new file mode 100644 index 00000000..d060b75d --- /dev/null +++ b/artifact-replica-consistency-guard/index.js @@ -0,0 +1,262 @@ +function list(value) { + return Array.isArray(value) ? value : []; +} + +function clean(value) { + return String(value || '').trim(); +} + +function normalize(value) { + return clean(value).toLowerCase(); +} + +function hoursBetween(older, newer) { + const olderTime = Date.parse(older); + const newerTime = Date.parse(newer); + if (!Number.isFinite(olderTime) || !Number.isFinite(newerTime)) { + return Infinity; + } + return Math.floor((newerTime - olderTime) / 3600000); +} + +function finding(type, severity, target, message) { + return {type, severity, target, message}; +} + +function action(type, target, reason) { + return {type, target, reason}; +} + +function compareField(input, findings, repairPlan, field, type, label) { + const canonicalValue = normalize(input.canonical?.[field]); + for (const replica of list(input.replicas)) { + const replicaValue = normalize(replica[field]); + if (canonicalValue && replicaValue && canonicalValue !== replicaValue) { + findings.push( + finding( + type, + 'block', + replica.id || 'unknown-replica', + `${replica.id || 'Replica'} ${label} ${replica[field]} does not match canonical ${input.canonical[field]}.` + ) + ); + repairPlan.push( + action( + `repair-${field}`, + replica.id || 'unknown-replica', + `Reconcile ${replica.id || 'replica'} ${label} against canonical manifest before exposing durable artifact actions.` + ) + ); + } + } +} + +function validateReplicaFreshness(input, findings, repairPlan) { + const maxLag = Number.isFinite(input.maxMirrorLagHours) ? input.maxMirrorLagHours : 168; + for (const replica of list(input.replicas)) { + const age = hoursBetween(replica.lastVerifiedAt, input.generatedAt); + if (age > maxLag) { + const severity = normalize(replica.tier) === 'mirror' ? 'review' : 'block'; + findings.push( + finding( + 'replica-verification-stale', + severity, + replica.id || 'unknown-replica', + `${replica.id || 'Replica'} was last verified ${age} hours ago, above the ${maxLag}-hour freshness threshold.` + ) + ); + repairPlan.push( + action( + 'refresh-replica-verification', + replica.id || 'unknown-replica', + `Refresh checksum and manifest verification for ${replica.id || 'replica'} before export or reproduce actions continue.` + ) + ); + } + } +} + +function validateLandingPage(input, findings, repairPlan) { + const landing = input.landingPage || {}; + const canonical = input.canonical || {}; + if (normalize(landing.dataciteDoi) !== normalize(canonical.dataciteDoi)) { + findings.push( + finding( + 'landing-page-datacite-mismatch', + 'block', + 'landing-page', + `Landing page DataCite DOI ${landing.dataciteDoi || 'missing'} does not match canonical DOI ${canonical.dataciteDoi || 'missing'}.` + ) + ); + repairPlan.push( + action( + 'repair-landing-page-datacite', + 'landing-page', + 'Update landing page DataCite metadata before persistent links or exports are enabled.' + ) + ); + } + if (normalize(landing.schemaOrgUrl) !== normalize(canonical.schemaOrgUrl)) { + findings.push( + finding( + 'landing-page-schemaorg-mismatch', + 'block', + 'landing-page', + `Landing page schema.org URL ${landing.schemaOrgUrl || 'missing'} does not match canonical URL ${canonical.schemaOrgUrl || 'missing'}.` + ) + ); + repairPlan.push( + action( + 'repair-landing-page-schemaorg', + 'landing-page', + 'Update schema.org URL metadata before discovery crawlers see this artifact.' + ) + ); + } + if (normalize(landing.accessPolicy) !== normalize(canonical.accessPolicy)) { + findings.push( + finding( + 'landing-page-access-policy-mismatch', + 'block', + 'landing-page', + `Landing page access policy ${landing.accessPolicy || 'missing'} does not match canonical policy ${canonical.accessPolicy || 'missing'}.` + ) + ); + repairPlan.push( + action( + 'repair-landing-page-access-policy', + 'landing-page', + 'Align landing page access policy with canonical storage policy before public previews are trusted.' + ) + ); + } +} + +function summarize(findings) { + const blockingFindingCount = findings.filter((item) => item.severity === 'block').length; + const reviewFindingCount = findings.filter((item) => item.severity !== 'block').length; + return { + findingCount: findings.length, + blockingFindingCount, + reviewFindingCount, + }; +} + +function decideVisibleActions(input, decision, findings) { + const requested = list(input.actionsRequested); + if (decision === 'publish-artifact') { + return {visibleActions: requested, gatedActions: []}; + } + if (decision === 'hold-artifact') { + return {visibleActions: [], gatedActions: requested}; + } + const hasOnlyFreshnessReview = findings.every((item) => item.type === 'replica-verification-stale' && item.severity === 'review'); + if (hasOnlyFreshnessReview) { + return { + visibleActions: requested.filter((item) => item === 'enable-preview'), + gatedActions: requested.filter((item) => item !== 'enable-preview'), + }; + } + return { + visibleActions: [], + gatedActions: requested, + }; +} + +function evaluateArtifactReplicaConsistency(input = {}) { + const generatedAt = input.generatedAt || new Date(0).toISOString(); + const normalizedInput = {...input, generatedAt}; + const findings = []; + const repairPlan = []; + + compareField(normalizedInput, findings, repairPlan, 'checksum', 'replica-checksum-mismatch', 'checksum'); + compareField(normalizedInput, findings, repairPlan, 'manifestVersion', 'manifest-version-mismatch', 'manifest version'); + compareField(normalizedInput, findings, repairPlan, 'accessPolicy', 'access-policy-mismatch', 'access policy'); + validateReplicaFreshness(normalizedInput, findings, repairPlan); + validateLandingPage(normalizedInput, findings, repairPlan); + + const summary = summarize(findings); + const decision = summary.blockingFindingCount > 0 + ? 'hold-artifact' + : summary.reviewFindingCount > 0 + ? 'repair-review' + : 'publish-artifact'; + const {visibleActions, gatedActions} = decideVisibleActions(normalizedInput, decision, findings); + + return { + generatedAt, + artifactId: input.artifactId || 'unknown-artifact', + artifactType: input.artifactType || 'unknown-type', + decision, + summary, + findings, + repairPlan, + visibleActions, + gatedActions, + publicationPacket: { + artifactId: input.artifactId || 'unknown-artifact', + artifactType: input.artifactType || 'unknown-type', + decision, + dataciteDoi: input.canonical?.dataciteDoi || '', + schemaOrgUrl: input.canonical?.schemaOrgUrl || '', + replicaCount: list(input.replicas).length, + gatedActions, + visibleActions, + }, + }; +} + +function buildReplicaConsistencyPacket(result) { + const lines = [ + `# Artifact Replica Consistency Guard: ${result.artifactId}`, + '', + `Artifact type: ${result.artifactType}`, + `Decision: ${result.decision}`, + `Generated: ${result.generatedAt}`, + `Replicas: ${result.publicationPacket.replicaCount}`, + '', + '## Visible Actions', + ]; + + if (result.visibleActions.length === 0) { + lines.push('- None'); + } else { + for (const item of result.visibleActions) { + lines.push(`- ${item}`); + } + } + + lines.push('', '## Gated Actions'); + if (result.gatedActions.length === 0) { + lines.push('- None'); + } else { + for (const item of result.gatedActions) { + lines.push(`- ${item}`); + } + } + + lines.push('', '## Findings'); + if (result.findings.length === 0) { + lines.push('- None'); + } else { + for (const item of result.findings) { + lines.push(`- ${item.type}: ${item.target} - ${item.message}`); + } + } + + lines.push('', '## Repair Plan'); + if (result.repairPlan.length === 0) { + lines.push('- No repair action required'); + } else { + for (const item of result.repairPlan) { + lines.push(`- ${item.type}: ${item.target} - ${item.reason}`); + } + } + + return `${lines.join('\n')}\n`; +} + +module.exports = { + evaluateArtifactReplicaConsistency, + buildReplicaConsistencyPacket, +}; diff --git a/artifact-replica-consistency-guard/reports/demo.mp4 b/artifact-replica-consistency-guard/reports/demo.mp4 new file mode 100644 index 0000000000000000000000000000000000000000..d8d699586726e31b73deab140867ea82c0c9c5c5 GIT binary patch literal 6609 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 Tqy#C&pfZF)Wgt32Wgz?;p_pb- literal 0 HcmV?d00001 diff --git a/artifact-replica-consistency-guard/reports/replica-consistency-packet.json b/artifact-replica-consistency-guard/reports/replica-consistency-packet.json new file mode 100644 index 00000000..4019dd48 --- /dev/null +++ b/artifact-replica-consistency-guard/reports/replica-consistency-packet.json @@ -0,0 +1,185 @@ +[ + { + "scenario": "Controlled dataset has drifted archive and public landing-page policy", + "generatedAt": "2026-05-22T18:00:00Z", + "artifactId": "dataset-cellatlas-v4", + "artifactType": "dataset", + "decision": "hold-artifact", + "summary": { + "findingCount": 6, + "blockingFindingCount": 5, + "reviewFindingCount": 1 + }, + "findings": [ + { + "type": "replica-checksum-mismatch", + "severity": "block", + "target": "cold-archive", + "message": "cold-archive checksum sha256:archive-222 does not match canonical sha256:canonical-111." + }, + { + "type": "manifest-version-mismatch", + "severity": "block", + "target": "cold-archive", + "message": "cold-archive manifest version 4.1.0 does not match canonical 4.2.0." + }, + { + "type": "access-policy-mismatch", + "severity": "block", + "target": "institutional-mirror", + "message": "institutional-mirror access policy public does not match canonical controlled." + }, + { + "type": "replica-verification-stale", + "severity": "review", + "target": "institutional-mirror", + "message": "institutional-mirror was last verified 288 hours ago, above the 168-hour freshness threshold." + }, + { + "type": "landing-page-datacite-mismatch", + "severity": "block", + "target": "landing-page", + "message": "Landing page DataCite DOI 10.1234/cellatlas.v3 does not match canonical DOI 10.1234/cellatlas.v4." + }, + { + "type": "landing-page-access-policy-mismatch", + "severity": "block", + "target": "landing-page", + "message": "Landing page access policy public does not match canonical policy controlled." + } + ], + "repairPlan": [ + { + "type": "repair-checksum", + "target": "cold-archive", + "reason": "Reconcile cold-archive checksum against canonical manifest before exposing durable artifact actions." + }, + { + "type": "repair-manifestVersion", + "target": "cold-archive", + "reason": "Reconcile cold-archive manifest version against canonical manifest before exposing durable artifact actions." + }, + { + "type": "repair-accessPolicy", + "target": "institutional-mirror", + "reason": "Reconcile institutional-mirror access policy against canonical manifest before exposing durable artifact actions." + }, + { + "type": "refresh-replica-verification", + "target": "institutional-mirror", + "reason": "Refresh checksum and manifest verification for institutional-mirror before export or reproduce actions continue." + }, + { + "type": "repair-landing-page-datacite", + "target": "landing-page", + "reason": "Update landing page DataCite metadata before persistent links or exports are enabled." + }, + { + "type": "repair-landing-page-access-policy", + "target": "landing-page", + "reason": "Align landing page access policy with canonical storage policy before public previews are trusted." + } + ], + "visibleActions": [], + "gatedActions": [ + "enable-preview", + "enable-export", + "enable-reproduce" + ], + "publicationPacket": { + "artifactId": "dataset-cellatlas-v4", + "artifactType": "dataset", + "decision": "hold-artifact", + "dataciteDoi": "10.1234/cellatlas.v4", + "schemaOrgUrl": "https://scibase.example/artifacts/dataset-cellatlas-v4", + "replicaCount": 3, + "gatedActions": [ + "enable-preview", + "enable-export", + "enable-reproduce" + ], + "visibleActions": [] + } + }, + { + "scenario": "Notebook mirror lag needs repair review", + "generatedAt": "2026-05-22T18:10:00Z", + "artifactId": "notebook-rerun-pack", + "artifactType": "notebook", + "decision": "repair-review", + "summary": { + "findingCount": 1, + "blockingFindingCount": 0, + "reviewFindingCount": 1 + }, + "findings": [ + { + "type": "replica-verification-stale", + "severity": "review", + "target": "public-mirror", + "message": "public-mirror was last verified 73 hours ago, above the 48-hour freshness threshold." + } + ], + "repairPlan": [ + { + "type": "refresh-replica-verification", + "target": "public-mirror", + "reason": "Refresh checksum and manifest verification for public-mirror before export or reproduce actions continue." + } + ], + "visibleActions": [ + "enable-preview" + ], + "gatedActions": [ + "enable-export" + ], + "publicationPacket": { + "artifactId": "notebook-rerun-pack", + "artifactType": "notebook", + "decision": "repair-review", + "dataciteDoi": "10.1234/notebook.rerun", + "schemaOrgUrl": "https://scibase.example/artifacts/notebook-rerun-pack", + "replicaCount": 2, + "gatedActions": [ + "enable-export" + ], + "visibleActions": [ + "enable-preview" + ] + } + }, + { + "scenario": "Model capsule replicas are ready for publication", + "generatedAt": "2026-05-22T18:20:00Z", + "artifactId": "model-weight-capsule", + "artifactType": "model", + "decision": "publish-artifact", + "summary": { + "findingCount": 0, + "blockingFindingCount": 0, + "reviewFindingCount": 0 + }, + "findings": [], + "repairPlan": [], + "visibleActions": [ + "enable-preview", + "enable-export", + "enable-reproduce" + ], + "gatedActions": [], + "publicationPacket": { + "artifactId": "model-weight-capsule", + "artifactType": "model", + "decision": "publish-artifact", + "dataciteDoi": "10.1234/model.weight", + "schemaOrgUrl": "https://scibase.example/artifacts/model-weight-capsule", + "replicaCount": 3, + "gatedActions": [], + "visibleActions": [ + "enable-preview", + "enable-export", + "enable-reproduce" + ] + } + } +] diff --git a/artifact-replica-consistency-guard/reports/replica-consistency-review.md b/artifact-replica-consistency-guard/reports/replica-consistency-review.md new file mode 100644 index 00000000..5330e6d0 --- /dev/null +++ b/artifact-replica-consistency-guard/reports/replica-consistency-review.md @@ -0,0 +1,72 @@ +# Artifact Replica Consistency Guard: dataset-cellatlas-v4 + +Artifact type: dataset +Decision: hold-artifact +Generated: 2026-05-22T18:00:00Z +Replicas: 3 + +## Visible Actions +- None + +## Gated Actions +- enable-preview +- enable-export +- enable-reproduce + +## Findings +- replica-checksum-mismatch: cold-archive - cold-archive checksum sha256:archive-222 does not match canonical sha256:canonical-111. +- manifest-version-mismatch: cold-archive - cold-archive manifest version 4.1.0 does not match canonical 4.2.0. +- access-policy-mismatch: institutional-mirror - institutional-mirror access policy public does not match canonical controlled. +- replica-verification-stale: institutional-mirror - institutional-mirror was last verified 288 hours ago, above the 168-hour freshness threshold. +- landing-page-datacite-mismatch: landing-page - Landing page DataCite DOI 10.1234/cellatlas.v3 does not match canonical DOI 10.1234/cellatlas.v4. +- landing-page-access-policy-mismatch: landing-page - Landing page access policy public does not match canonical policy controlled. + +## Repair Plan +- repair-checksum: cold-archive - Reconcile cold-archive checksum against canonical manifest before exposing durable artifact actions. +- repair-manifestVersion: cold-archive - Reconcile cold-archive manifest version against canonical manifest before exposing durable artifact actions. +- repair-accessPolicy: institutional-mirror - Reconcile institutional-mirror access policy against canonical manifest before exposing durable artifact actions. +- refresh-replica-verification: institutional-mirror - Refresh checksum and manifest verification for institutional-mirror before export or reproduce actions continue. +- repair-landing-page-datacite: landing-page - Update landing page DataCite metadata before persistent links or exports are enabled. +- repair-landing-page-access-policy: landing-page - Align landing page access policy with canonical storage policy before public previews are trusted. + +--- +# Artifact Replica Consistency Guard: notebook-rerun-pack + +Artifact type: notebook +Decision: repair-review +Generated: 2026-05-22T18:10:00Z +Replicas: 2 + +## Visible Actions +- enable-preview + +## Gated Actions +- enable-export + +## Findings +- replica-verification-stale: public-mirror - public-mirror was last verified 73 hours ago, above the 48-hour freshness threshold. + +## Repair Plan +- refresh-replica-verification: public-mirror - Refresh checksum and manifest verification for public-mirror before export or reproduce actions continue. + +--- +# Artifact Replica Consistency Guard: model-weight-capsule + +Artifact type: model +Decision: publish-artifact +Generated: 2026-05-22T18:20:00Z +Replicas: 3 + +## Visible Actions +- enable-preview +- enable-export +- enable-reproduce + +## Gated Actions +- None + +## Findings +- None + +## Repair Plan +- No repair action required diff --git a/artifact-replica-consistency-guard/reports/summary.svg b/artifact-replica-consistency-guard/reports/summary.svg new file mode 100644 index 00000000..531f1488 --- /dev/null +++ b/artifact-replica-consistency-guard/reports/summary.svg @@ -0,0 +1,23 @@ + + + Artifact Replica Consistency Guard + Checks hosted artifact mirrors before previews, exports, persistent links, and reproduce buttons are enabled + + + Held + 1 + + + + Repair Review + 1 + + + + Publish + 1 + + Findings: 7 | Blocking: 5 | Review: 2 | Gated actions: 4 + Checks: checksums, manifest versions, access policy parity, mirror freshness, DataCite and schema.org landing-page consistency + Synthetic artifact manifests only. No storage provider, DOI registry, external mirror, credential, or network calls. + diff --git a/artifact-replica-consistency-guard/requirements-map.md b/artifact-replica-consistency-guard/requirements-map.md new file mode 100644 index 00000000..aa0d4d64 --- /dev/null +++ b/artifact-replica-consistency-guard/requirements-map.md @@ -0,0 +1,19 @@ +# Requirements Map + +Issue #14 asks for scalable storage, structured metadata, FAIR compliance, executable environments, previews, persistent access, and versioned artifacts. This slice focuses on replica consistency as a storage safety layer. + +| Issue requirement | Implementation coverage | +| --- | --- | +| Scalable storage engine | `replicas[]` models primary storage, cold archive, institutional mirror, and public mirror states. | +| Major artifact types | Synthetic scenarios cover datasets, notebooks, and model capsules. | +| Metadata-aware previews | `actionsRequested` and `visibleActions` decide when previews are safe despite mirror repair needs. | +| Upload versioning and diffing | `manifestVersion` comparison catches replica drift before versions are exposed. | +| JSON-LD/DataCite/schema.org readiness | `landingPage` checks validate DataCite DOI and schema.org URL alignment against canonical metadata. | +| FAIR accessibility and reusability | Access-policy parity checks protect persistent links, previews, exports, and reproduce actions. | +| Executable environments and reproduce buttons | `enable-reproduce` is gated when replica evidence is inconsistent. | +| Reviewer-ready demo | `demo.js` writes JSON, Markdown, SVG, and H.264 MP4 artifacts in `reports/`. | +| Safe local verification | `test.js` covers hold, repair-review, and publish outcomes with no external calls or secrets. | + +## Distinctness + +This module avoids duplicating existing issue #14 slices by staying narrow: it does not implement a broad FAIR manifest, access/compute governance, executable-environment drift, provenance chain, quarantine/rerun guard, storage quota/dedupe ledger, resumable upload checkpoint guard, artifact package integrity gate, preview cache guard, raw-instrument preview gate, notebook preview gate, retention/tombstone ledger, model-card lineage gate, license compatibility gate, sensitive-redaction gate, schema-evolution checker, data dictionary release gate, persistent-ID guard, or SBOM quarantine gate. It validates whether already-hosted replicas agree enough to safely expose artifact actions. diff --git a/artifact-replica-consistency-guard/sample-data.js b/artifact-replica-consistency-guard/sample-data.js new file mode 100644 index 00000000..ac12d2f1 --- /dev/null +++ b/artifact-replica-consistency-guard/sample-data.js @@ -0,0 +1,76 @@ +const scenarios = [ + { + name: 'Controlled dataset has drifted archive and public landing-page policy', + generatedAt: '2026-05-22T18:00:00Z', + artifactId: 'dataset-cellatlas-v4', + artifactType: 'dataset', + canonical: { + checksum: 'sha256:canonical-111', + manifestVersion: '4.2.0', + accessPolicy: 'controlled', + dataciteDoi: '10.1234/cellatlas.v4', + schemaOrgUrl: 'https://scibase.example/artifacts/dataset-cellatlas-v4', + }, + replicas: [ + {id: 'primary-object-store', tier: 'primary', checksum: 'sha256:canonical-111', manifestVersion: '4.2.0', accessPolicy: 'controlled', lastVerifiedAt: '2026-05-21T18:00:00Z'}, + {id: 'cold-archive', tier: 'archive', checksum: 'sha256:archive-222', manifestVersion: '4.1.0', accessPolicy: 'controlled', lastVerifiedAt: '2026-05-18T18:00:00Z'}, + {id: 'institutional-mirror', tier: 'mirror', checksum: 'sha256:canonical-111', manifestVersion: '4.2.0', accessPolicy: 'public', lastVerifiedAt: '2026-05-10T18:00:00Z'}, + ], + landingPage: { + dataciteDoi: '10.1234/cellatlas.v3', + schemaOrgUrl: 'https://scibase.example/artifacts/dataset-cellatlas-v4', + accessPolicy: 'public', + }, + actionsRequested: ['enable-preview', 'enable-export', 'enable-reproduce'], + }, + { + name: 'Notebook mirror lag needs repair review', + generatedAt: '2026-05-22T18:10:00Z', + artifactId: 'notebook-rerun-pack', + artifactType: 'notebook', + maxMirrorLagHours: 48, + canonical: { + checksum: 'sha256:notebook-555', + manifestVersion: '2.0.1', + accessPolicy: 'public', + dataciteDoi: '10.1234/notebook.rerun', + schemaOrgUrl: 'https://scibase.example/artifacts/notebook-rerun-pack', + }, + replicas: [ + {id: 'primary-object-store', tier: 'primary', checksum: 'sha256:notebook-555', manifestVersion: '2.0.1', accessPolicy: 'public', lastVerifiedAt: '2026-05-22T17:00:00Z'}, + {id: 'public-mirror', tier: 'mirror', checksum: 'sha256:notebook-555', manifestVersion: '2.0.1', accessPolicy: 'public', lastVerifiedAt: '2026-05-19T17:00:00Z'}, + ], + landingPage: { + dataciteDoi: '10.1234/notebook.rerun', + schemaOrgUrl: 'https://scibase.example/artifacts/notebook-rerun-pack', + accessPolicy: 'public', + }, + actionsRequested: ['enable-preview', 'enable-export'], + }, + { + name: 'Model capsule replicas are ready for publication', + generatedAt: '2026-05-22T18:20:00Z', + artifactId: 'model-weight-capsule', + artifactType: 'model', + canonical: { + checksum: 'sha256:model-777', + manifestVersion: '1.3.0', + accessPolicy: 'open', + dataciteDoi: '10.1234/model.weight', + schemaOrgUrl: 'https://scibase.example/artifacts/model-weight-capsule', + }, + replicas: [ + {id: 'primary-object-store', tier: 'primary', checksum: 'sha256:model-777', manifestVersion: '1.3.0', accessPolicy: 'open', lastVerifiedAt: '2026-05-22T17:30:00Z'}, + {id: 'cold-archive', tier: 'archive', checksum: 'sha256:model-777', manifestVersion: '1.3.0', accessPolicy: 'open', lastVerifiedAt: '2026-05-22T14:30:00Z'}, + {id: 'institutional-mirror', tier: 'mirror', checksum: 'sha256:model-777', manifestVersion: '1.3.0', accessPolicy: 'open', lastVerifiedAt: '2026-05-22T16:45:00Z'}, + ], + landingPage: { + dataciteDoi: '10.1234/model.weight', + schemaOrgUrl: 'https://scibase.example/artifacts/model-weight-capsule', + accessPolicy: 'open', + }, + actionsRequested: ['enable-preview', 'enable-export', 'enable-reproduce'], + }, +]; + +module.exports = {scenarios}; diff --git a/artifact-replica-consistency-guard/test.js b/artifact-replica-consistency-guard/test.js new file mode 100644 index 00000000..643e1bac --- /dev/null +++ b/artifact-replica-consistency-guard/test.js @@ -0,0 +1,115 @@ +const test = require('node:test'); +const assert = require('node:assert/strict'); + +const { + evaluateArtifactReplicaConsistency, + buildReplicaConsistencyPacket, +} = require('./index'); + +test('holds public access when replicas disagree on checksum, manifest version, and access policy', () => { + const result = evaluateArtifactReplicaConsistency({ + generatedAt: '2026-05-22T18:00:00Z', + artifactId: 'dataset-cellatlas-v4', + artifactType: 'dataset', + canonical: { + checksum: 'sha256:canonical-111', + manifestVersion: '4.2.0', + accessPolicy: 'controlled', + dataciteDoi: '10.1234/cellatlas.v4', + schemaOrgUrl: 'https://scibase.example/artifacts/dataset-cellatlas-v4', + }, + replicas: [ + {id: 'primary-object-store', tier: 'primary', checksum: 'sha256:canonical-111', manifestVersion: '4.2.0', accessPolicy: 'controlled', lastVerifiedAt: '2026-05-21T18:00:00Z'}, + {id: 'cold-archive', tier: 'archive', checksum: 'sha256:archive-222', manifestVersion: '4.1.0', accessPolicy: 'controlled', lastVerifiedAt: '2026-05-18T18:00:00Z'}, + {id: 'institutional-mirror', tier: 'mirror', checksum: 'sha256:canonical-111', manifestVersion: '4.2.0', accessPolicy: 'public', lastVerifiedAt: '2026-05-10T18:00:00Z'}, + ], + landingPage: { + dataciteDoi: '10.1234/cellatlas.v3', + schemaOrgUrl: 'https://scibase.example/artifacts/dataset-cellatlas-v4', + accessPolicy: 'public', + }, + actionsRequested: ['enable-preview', 'enable-export', 'enable-reproduce'], + }); + + assert.equal(result.decision, 'hold-artifact'); + assert.deepEqual( + result.findings.map((finding) => finding.type), + [ + 'replica-checksum-mismatch', + 'manifest-version-mismatch', + 'access-policy-mismatch', + 'replica-verification-stale', + 'landing-page-datacite-mismatch', + 'landing-page-access-policy-mismatch', + ] + ); + assert.equal(result.summary.blockingFindingCount, 5); + assert.equal(result.gatedActions.sort().join(','), 'enable-export,enable-preview,enable-reproduce'); + assert.match(result.repairPlan[0].reason, /cold-archive/); +}); + +test('routes stale mirror lag to repair review while keeping safe previews visible', () => { + const result = evaluateArtifactReplicaConsistency({ + generatedAt: '2026-05-22T18:10:00Z', + artifactId: 'notebook-rerun-pack', + artifactType: 'notebook', + maxMirrorLagHours: 48, + canonical: { + checksum: 'sha256:notebook-555', + manifestVersion: '2.0.1', + accessPolicy: 'public', + dataciteDoi: '10.1234/notebook.rerun', + schemaOrgUrl: 'https://scibase.example/artifacts/notebook-rerun-pack', + }, + replicas: [ + {id: 'primary-object-store', tier: 'primary', checksum: 'sha256:notebook-555', manifestVersion: '2.0.1', accessPolicy: 'public', lastVerifiedAt: '2026-05-22T17:00:00Z'}, + {id: 'public-mirror', tier: 'mirror', checksum: 'sha256:notebook-555', manifestVersion: '2.0.1', accessPolicy: 'public', lastVerifiedAt: '2026-05-19T17:00:00Z'}, + ], + landingPage: { + dataciteDoi: '10.1234/notebook.rerun', + schemaOrgUrl: 'https://scibase.example/artifacts/notebook-rerun-pack', + accessPolicy: 'public', + }, + actionsRequested: ['enable-preview', 'enable-export'], + }); + + assert.equal(result.decision, 'repair-review'); + assert.deepEqual(result.findings.map((finding) => finding.type), ['replica-verification-stale']); + assert.equal(result.summary.reviewFindingCount, 1); + assert.deepEqual(result.gatedActions, ['enable-export']); + assert.deepEqual(result.visibleActions, ['enable-preview']); +}); + +test('approves consistent replicas and builds a publication-ready packet', () => { + const result = evaluateArtifactReplicaConsistency({ + generatedAt: '2026-05-22T18:20:00Z', + artifactId: 'model-weight-capsule', + artifactType: 'model', + canonical: { + checksum: 'sha256:model-777', + manifestVersion: '1.3.0', + accessPolicy: 'open', + dataciteDoi: '10.1234/model.weight', + schemaOrgUrl: 'https://scibase.example/artifacts/model-weight-capsule', + }, + replicas: [ + {id: 'primary-object-store', tier: 'primary', checksum: 'sha256:model-777', manifestVersion: '1.3.0', accessPolicy: 'open', lastVerifiedAt: '2026-05-22T17:30:00Z'}, + {id: 'cold-archive', tier: 'archive', checksum: 'sha256:model-777', manifestVersion: '1.3.0', accessPolicy: 'open', lastVerifiedAt: '2026-05-22T14:30:00Z'}, + {id: 'institutional-mirror', tier: 'mirror', checksum: 'sha256:model-777', manifestVersion: '1.3.0', accessPolicy: 'open', lastVerifiedAt: '2026-05-22T16:45:00Z'}, + ], + landingPage: { + dataciteDoi: '10.1234/model.weight', + schemaOrgUrl: 'https://scibase.example/artifacts/model-weight-capsule', + accessPolicy: 'open', + }, + actionsRequested: ['enable-preview', 'enable-export', 'enable-reproduce'], + }); + const packet = buildReplicaConsistencyPacket(result); + + assert.equal(result.decision, 'publish-artifact'); + assert.equal(result.summary.findingCount, 0); + assert.deepEqual(result.gatedActions, []); + assert.deepEqual(result.visibleActions.sort(), ['enable-export', 'enable-preview', 'enable-reproduce']); + assert.match(packet, /model-weight-capsule/); + assert.match(packet, /No repair action required/); +});