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 = `
+`;
+
+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 00000000..d8d69958
Binary files /dev/null and b/artifact-replica-consistency-guard/reports/demo.mp4 differ
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 @@
+
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/);
+});