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 = `
+`;
+
+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 @@
+
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/);
+});