Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions artifact-replica-consistency-guard/README.md
Original file line number Diff line number Diff line change
@@ -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.
147 changes: 147 additions & 0 deletions artifact-replica-consistency-guard/demo.js
Original file line number Diff line number Diff line change
@@ -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 = `<svg xmlns="http://www.w3.org/2000/svg" width="960" height="540" viewBox="0 0 960 540">
<rect width="960" height="540" fill="#17202a"/>
<text x="48" y="72" fill="#f8fafc" font-family="Arial, sans-serif" font-size="32" font-weight="700">Artifact Replica Consistency Guard</text>
<text x="48" y="112" fill="#bfdbfe" font-family="Arial, sans-serif" font-size="18">Checks hosted artifact mirrors before previews, exports, persistent links, and reproduce buttons are enabled</text>
<g transform="translate(48 154)">
<rect width="260" height="148" rx="8" fill="#7f1d1d"/>
<text x="24" y="46" fill="#fee2e2" font-family="Arial, sans-serif" font-size="20" font-weight="700">Held</text>
<text x="24" y="108" fill="#fef2f2" font-family="Arial, sans-serif" font-size="56" font-weight="700">${decisionCounts['hold-artifact'] || 0}</text>
</g>
<g transform="translate(350 154)">
<rect width="260" height="148" rx="8" fill="#854d0e"/>
<text x="24" y="46" fill="#fef3c7" font-family="Arial, sans-serif" font-size="20" font-weight="700">Repair Review</text>
<text x="24" y="108" fill="#fffbeb" font-family="Arial, sans-serif" font-size="56" font-weight="700">${decisionCounts['repair-review'] || 0}</text>
</g>
<g transform="translate(652 154)">
<rect width="260" height="148" rx="8" fill="#166534"/>
<text x="24" y="46" fill="#dcfce7" font-family="Arial, sans-serif" font-size="20" font-weight="700">Publish</text>
<text x="24" y="108" fill="#f0fdf4" font-family="Arial, sans-serif" font-size="56" font-weight="700">${decisionCounts['publish-artifact'] || 0}</text>
</g>
<text x="48" y="374" fill="#e5e7eb" font-family="Arial, sans-serif" font-size="22">Findings: ${totals.findings} | Blocking: ${totals.blocking} | Review: ${totals.review} | Gated actions: ${totals.gated}</text>
<text x="48" y="418" fill="#d1d5db" font-family="Arial, sans-serif" font-size="18">Checks: checksums, manifest versions, access policy parity, mirror freshness, DataCite and schema.org landing-page consistency</text>
<text x="48" y="472" fill="#9ca3af" font-family="Arial, sans-serif" font-size="16">Synthetic artifact manifests only. No storage provider, DOI registry, external mirror, credential, or network calls.</text>
</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));
Loading