diff --git a/repository-restore-rehearsal-guard/README.md b/repository-restore-rehearsal-guard/README.md
new file mode 100644
index 00000000..73855496
--- /dev/null
+++ b/repository-restore-rehearsal-guard/README.md
@@ -0,0 +1,30 @@
+# Repository Restore Rehearsal Guard
+
+This module adds a focused Project Repository & Version Control slice for SCIBASE issue #10. It validates whether a tagged repository snapshot can be restored before DOI/export publication.
+
+The guard is intentionally narrower than repository ledgers, rollback, legal-hold, embargo, access-review, and release-engine work. It answers one reviewer question: can this exact scientific snapshot be restored with the expected refs, components, large artifacts, environment locks, notebook replay evidence, and export manifest?
+
+## What It Checks
+
+- release candidates declare an immutable snapshot id
+- restore drills are recent and passed
+- protected refs and semantic tags are included in the snapshot
+- required `manuscript`, `data`, `code`, and `metadata` components match expected hashes
+- large or restricted artifacts have ready mirrors with matching hashes
+- container and dependency locks are available for replay
+- notebook replay evidence is fresh
+- export manifests point at the restored snapshot
+
+## Usage
+
+```bash
+npm run check
+npm test
+npm run demo
+```
+
+`npm run demo` writes deterministic reviewer artifacts into `reports/`, including JSON, Markdown, SVG, and an MP4 demo video when `ffmpeg` is available.
+
+## Safety
+
+All examples are synthetic. The module does not scan private repositories, call external services, open network connections, or include credentials.
diff --git a/repository-restore-rehearsal-guard/acceptance-notes.md b/repository-restore-rehearsal-guard/acceptance-notes.md
new file mode 100644
index 00000000..c04b42c6
--- /dev/null
+++ b/repository-restore-rehearsal-guard/acceptance-notes.md
@@ -0,0 +1,21 @@
+# Acceptance Notes
+
+## Reviewer Path
+
+1. Run `npm run check` to verify syntax.
+2. Run `npm test` to verify restore decisions, blockers, clean release behavior, and deterministic audit digests.
+3. Run `npm run demo` to regenerate reviewer artifacts in `reports/`.
+4. Inspect `reports/restore-rehearsal-report.md` for the release queue and remediation queue.
+
+## Expected Results
+
+- `neuro-reproducibility-atlas` is `restore_ready`.
+- `climate-table-fork` is held because restore drill, ref coverage, tag coverage, data checksum, mirror readiness, and environment lock checks fail.
+- `protein-model-sandbox` is held because the snapshot/drill/notebook replay evidence is stale and no export manifest is linked.
+
+## Demo Artifacts
+
+- `reports/restore-rehearsal-packet.json`
+- `reports/restore-rehearsal-report.md`
+- `reports/summary.svg`
+- `reports/demo.mp4`
diff --git a/repository-restore-rehearsal-guard/demo.js b/repository-restore-rehearsal-guard/demo.js
new file mode 100644
index 00000000..baa6704c
--- /dev/null
+++ b/repository-restore-rehearsal-guard/demo.js
@@ -0,0 +1,88 @@
+const fs = require("node:fs")
+const path = require("node:path")
+const { spawnSync } = require("node:child_process")
+const { evaluateRestoreRehearsal } = require("./index")
+const { repositories, policy } = require("./sample-data")
+
+const reportsDir = path.join(__dirname, "reports")
+fs.mkdirSync(reportsDir, { recursive: true })
+
+const packet = evaluateRestoreRehearsal({
+ asOf: "2026-05-23T02:00:00.000Z",
+ repositories,
+ policy,
+})
+
+fs.writeFileSync(
+ path.join(reportsDir, "restore-rehearsal-packet.json"),
+ `${JSON.stringify(packet, null, 2)}\n`,
+)
+
+const markdown = [
+ "# Repository Restore Rehearsal Report",
+ "",
+ `Repositories reviewed: ${packet.summary.totalRepositories}`,
+ `Restore-ready repositories: ${packet.summary.readyRepositories}`,
+ `Held repositories: ${packet.summary.heldRepositories}`,
+ `Critical findings: ${packet.summary.criticalFindings}`,
+ `Warning findings: ${packet.summary.warningFindings}`,
+ `Audit digest: \`${packet.audit.digest}\``,
+ "",
+ "## Repository Decisions",
+ ...packet.repositories.flatMap((repository) => [
+ "",
+ `### ${repository.title}`,
+ `- Status: ${repository.status}`,
+ `- Snapshot: ${repository.snapshotId}`,
+ `- Candidate tag: ${repository.releaseCandidate.tag || "none"}`,
+ `- Checks passed: ${repository.summary.passedChecks}/${repository.summary.checks}`,
+ `- Finding codes: ${repository.findings.map((finding) => finding.code).join(", ") || "none"}`,
+ ]),
+ "",
+ "## Remediation Queue",
+ ...packet.remediationQueue.map((item) => (
+ `- ${item.repositoryId}/${item.snapshotId}: ${item.action} (${item.severity})`
+ )),
+ "",
+]
+
+fs.writeFileSync(path.join(reportsDir, "restore-rehearsal-report.md"), markdown.join("\n"))
+
+const svg = `
+`
+
+fs.writeFileSync(path.join(reportsDir, "summary.svg"), svg)
+
+const ffmpeg = spawnSync("ffmpeg", [
+ "-y",
+ "-f",
+ "lavfi",
+ "-i",
+ "color=c=0x102018:s=960x540:d=5:r=15",
+ "-vf",
+ "drawbox=x=48:y=172:w=250:h=150:color=0x047857@1:t=fill,drawbox=x=355:y=172:w=250:h=150:color=0xb45309@1:t=fill,drawbox=x=662:y=172:w=250:h=150:color=0xbe123c@1:t=fill,drawbox=x=48:y=368:w=864:h=18:color=0x22c55e@1:t=fill",
+ "-pix_fmt",
+ "yuv420p",
+ path.join(reportsDir, "demo.mp4"),
+], { stdio: "ignore" })
+
+if (ffmpeg.status !== 0) {
+ console.warn("ffmpeg video generation failed; JSON, Markdown, and SVG reports were still generated.")
+}
+
+console.log(`Wrote repository restore rehearsal artifacts to ${reportsDir}`)
diff --git a/repository-restore-rehearsal-guard/index.js b/repository-restore-rehearsal-guard/index.js
new file mode 100644
index 00000000..0e27ce57
--- /dev/null
+++ b/repository-restore-rehearsal-guard/index.js
@@ -0,0 +1,451 @@
+const crypto = require("node:crypto")
+
+const DAY_MS = 24 * 60 * 60 * 1000
+
+const DEFAULT_POLICY = Object.freeze({
+ maxSnapshotAgeDays: 30,
+ maxRestoreDrillAgeDays: 21,
+ maxNotebookReplayAgeDays: 14,
+ requiredRefs: ["refs/heads/main"],
+ requiredComponents: ["manuscript", "data", "code", "metadata"],
+ requiredEnvironmentLocks: ["containerDigest", "dependencyLock"],
+ policyVersion: "repository-restore-rehearsal-v1",
+})
+
+function evaluateRestoreRehearsal(input = {}) {
+ const policy = { ...DEFAULT_POLICY, ...(input.policy || {}) }
+ const asOf = input.asOf || new Date().toISOString()
+ const now = Date.parse(asOf)
+ const repositories = Array.isArray(input.repositories) ? input.repositories : []
+ const repositoryDecisions = repositories.map((repository) => evaluateRepository(repository, now, policy))
+ const remediationQueue = repositoryDecisions.flatMap((repository) => repository.remediationQueue)
+ const summary = summarize(repositoryDecisions, remediationQueue)
+ const auditPayload = {
+ asOf,
+ policy,
+ summary,
+ repositories: repositoryDecisions.map((repository) => ({
+ repositoryId: repository.repositoryId,
+ status: repository.status,
+ snapshotId: repository.snapshotId,
+ findingCodes: repository.findings.map((finding) => finding.code),
+ })),
+ }
+
+ return {
+ asOf,
+ policy,
+ summary,
+ repositories: repositoryDecisions,
+ remediationQueue,
+ releaseQueue: repositoryDecisions
+ .filter((repository) => repository.status === "restore_ready")
+ .map((repository) => ({
+ repositoryId: repository.repositoryId,
+ snapshotId: repository.snapshotId,
+ tag: repository.releaseCandidate.tag,
+ action: "allow_release_export",
+ })),
+ audit: {
+ algorithm: "sha256",
+ digest: digest(auditPayload),
+ policyVersion: policy.policyVersion,
+ },
+ }
+}
+
+function evaluateRepository(repository, now, policy) {
+ const releaseCandidate = repository.releaseCandidate || {}
+ const snapshot = findSnapshot(repository.snapshots, releaseCandidate.snapshotId)
+ const findings = []
+ const checks = []
+
+ addCheck(checks, findings, {
+ code: "candidate_snapshot_declared",
+ severity: "critical",
+ pass: Boolean(releaseCandidate.snapshotId),
+ message: "Release candidate declares the snapshot that should be restored before export.",
+ remediation: "Bind the release candidate to an immutable snapshot id before tagging or exporting.",
+ })
+
+ addCheck(checks, findings, {
+ code: "snapshot_found",
+ severity: "critical",
+ pass: Boolean(snapshot),
+ message: "Declared snapshot exists in the repository restore catalog.",
+ remediation: "Create or re-index the immutable snapshot before continuing release review.",
+ })
+
+ if (!snapshot) {
+ return repositoryDecision(repository, releaseCandidate, null, checks, findings, policy)
+ }
+
+ const snapshotAgeDays = daysBetween(now, snapshot.createdAt)
+ addCheck(checks, findings, {
+ code: "snapshot_recency",
+ severity: "warning",
+ pass: snapshotAgeDays <= policy.maxSnapshotAgeDays,
+ message: `Snapshot age is ${snapshotAgeDays} days.`,
+ remediation: "Cut a fresh snapshot or document why an older snapshot is still the release source.",
+ evidence: { snapshotAgeDays, maxSnapshotAgeDays: policy.maxSnapshotAgeDays },
+ })
+
+ const drill = latestRestoreDrill(repository.restoreDrills, snapshot.id)
+ const drillAgeDays = drill ? daysBetween(now, drill.completedAt) : null
+ addCheck(checks, findings, {
+ code: "restore_drill_recent_passed",
+ severity: "critical",
+ pass: Boolean(drill && drill.status === "passed" && drillAgeDays <= policy.maxRestoreDrillAgeDays),
+ message: drill
+ ? `Latest restore drill status is ${drill.status} and age is ${drillAgeDays} days.`
+ : "No restore drill exists for the release snapshot.",
+ remediation: "Run a restore rehearsal from the immutable snapshot and attach the passed drill report.",
+ evidence: drill ? { drillId: drill.id, status: drill.status, drillAgeDays } : { drillId: null },
+ })
+
+ const requiredRefs = unique([...(policy.requiredRefs || []), ...(releaseCandidate.requiredRefs || [])])
+ const snapshotRefs = new Set(normalizeArray(snapshot.refs))
+ const missingRefs = requiredRefs.filter((ref) => !snapshotRefs.has(ref))
+ addCheck(checks, findings, {
+ code: "ref_coverage",
+ severity: "critical",
+ pass: missingRefs.length === 0,
+ message: missingRefs.length === 0
+ ? "Snapshot covers every required protected branch ref."
+ : `Snapshot is missing required refs: ${missingRefs.join(", ")}.`,
+ remediation: "Rebuild the snapshot with all protected branch refs required for release reconstruction.",
+ evidence: { requiredRefs, missingRefs },
+ })
+
+ const snapshotTags = new Set(normalizeArray(snapshot.tags))
+ addCheck(checks, findings, {
+ code: "tag_coverage",
+ severity: "warning",
+ pass: !releaseCandidate.tag || snapshotTags.has(releaseCandidate.tag),
+ message: releaseCandidate.tag
+ ? `Release tag ${releaseCandidate.tag} is ${snapshotTags.has(releaseCandidate.tag) ? "present" : "missing"} in the snapshot.`
+ : "Release candidate does not declare a semantic tag.",
+ remediation: "Attach the release tag to the immutable snapshot before DOI or export publication.",
+ evidence: { tag: releaseCandidate.tag || null },
+ })
+
+ const componentResult = evaluateComponentIntegrity(repository.expectedComponents, snapshot, policy)
+ addCheck(checks, findings, {
+ code: "component_manifest_integrity",
+ severity: "critical",
+ pass: componentResult.failures.length === 0,
+ message: componentResult.failures.length === 0
+ ? "Required repository components match the snapshot manifest."
+ : `Component manifest has ${componentResult.failures.length} restore blockers.`,
+ remediation: "Regenerate the manifest from restored files and reconcile missing or mismatched component hashes.",
+ evidence: componentResult,
+ })
+
+ const mirrorResult = evaluateArtifactMirrors(snapshot)
+ addCheck(checks, findings, {
+ code: "artifact_mirror_ready",
+ severity: "critical",
+ pass: mirrorResult.failures.length === 0,
+ message: mirrorResult.failures.length === 0
+ ? "Large and restricted artifacts have matching restore mirrors."
+ : `Artifact mirror readiness has ${mirrorResult.failures.length} blockers.`,
+ remediation: "Mirror required LFS or restricted artifacts and verify mirror digests before export.",
+ evidence: mirrorResult,
+ })
+
+ const lockResult = evaluateEnvironmentLocks(snapshot, policy)
+ addCheck(checks, findings, {
+ code: "environment_lock_coverage",
+ severity: "warning",
+ pass: lockResult.missing.length === 0,
+ message: lockResult.missing.length === 0
+ ? "Environment locks are present for restore replay."
+ : `Missing environment locks: ${lockResult.missing.join(", ")}.`,
+ remediation: "Attach container image digests and dependency lockfiles used by the restore drill.",
+ evidence: lockResult,
+ })
+
+ const replayResult = evaluateNotebookReplay(snapshot, now, policy)
+ addCheck(checks, findings, {
+ code: "notebook_replay_recent",
+ severity: replayResult.failed.length > 0 ? "critical" : "warning",
+ pass: replayResult.failed.length === 0 && replayResult.stale.length === 0 && replayResult.missing.length === 0,
+ message: replayResult.total === 0
+ ? "Snapshot contains no notebooks requiring replay evidence."
+ : `Notebook replay evidence reviewed for ${replayResult.total} notebook artifacts.`,
+ remediation: "Replay notebooks from the restored environment and attach fresh pass evidence.",
+ evidence: replayResult,
+ })
+
+ addCheck(checks, findings, {
+ code: "export_manifest_linked",
+ severity: "critical",
+ pass: Boolean(snapshot.exportManifest && snapshot.exportManifest.snapshotId === snapshot.id),
+ message: snapshot.exportManifest
+ ? "Export manifest is linked to the restored snapshot."
+ : "Export manifest is missing for the release snapshot.",
+ remediation: "Generate an export manifest that names the immutable snapshot and all restored components.",
+ evidence: {
+ snapshotId: snapshot.id,
+ exportSnapshotId: snapshot.exportManifest ? snapshot.exportManifest.snapshotId : null,
+ doi: snapshot.exportManifest ? snapshot.exportManifest.doi || null : null,
+ },
+ })
+
+ return repositoryDecision(repository, releaseCandidate, snapshot, checks, findings, policy)
+}
+
+function repositoryDecision(repository, releaseCandidate, snapshot, checks, findings, policy) {
+ const criticalFindings = findings.filter((finding) => finding.severity === "critical")
+ const warningFindings = findings.filter((finding) => finding.severity === "warning")
+ const status = criticalFindings.length > 0
+ ? "hold_restore_rehearsal"
+ : warningFindings.length > 0
+ ? "steward_review"
+ : "restore_ready"
+
+ return {
+ repositoryId: repository.repositoryId,
+ title: repository.title,
+ status,
+ snapshotId: snapshot ? snapshot.id : releaseCandidate.snapshotId || null,
+ releaseCandidate,
+ summary: {
+ checks: checks.length,
+ passedChecks: checks.filter((check) => check.pass).length,
+ criticalFindings: criticalFindings.length,
+ warningFindings: warningFindings.length,
+ },
+ checks,
+ findings,
+ remediationQueue: findings.map((finding) => ({
+ repositoryId: repository.repositoryId,
+ snapshotId: snapshot ? snapshot.id : releaseCandidate.snapshotId || null,
+ code: finding.code,
+ severity: finding.severity,
+ action: remediationAction(finding.code),
+ remediation: finding.remediation,
+ })),
+ restorePacket: {
+ policyVersion: policy.policyVersion,
+ candidateTag: releaseCandidate.tag || null,
+ restoredSnapshot: snapshot ? snapshot.id : null,
+ decision: status,
+ },
+ }
+}
+
+function evaluateComponentIntegrity(expectedComponents = [], snapshot, policy) {
+ const expected = expectedComponents.length > 0
+ ? expectedComponents
+ : policy.requiredComponents.map((component) => ({ component, hash: null }))
+ const artifacts = new Map((snapshot.artifacts || []).map((artifact) => [artifact.component, artifact]))
+ const required = new Set(policy.requiredComponents || [])
+ const failures = []
+ const covered = []
+
+ for (const component of expected) {
+ const artifact = artifacts.get(component.component)
+ if (!artifact && required.has(component.component)) {
+ failures.push({ component: component.component, reason: "missing_required_component" })
+ continue
+ }
+
+ if (!artifact) {
+ continue
+ }
+
+ if (component.hash && artifact.hash !== component.hash) {
+ failures.push({
+ component: component.component,
+ reason: "hash_mismatch",
+ expectedHash: component.hash,
+ snapshotHash: artifact.hash,
+ })
+ continue
+ }
+
+ covered.push(component.component)
+ }
+
+ for (const requiredComponent of required) {
+ if (!artifacts.has(requiredComponent)) {
+ failures.push({ component: requiredComponent, reason: "required_component_absent_from_snapshot" })
+ }
+ }
+
+ return {
+ covered: unique(covered),
+ failures: uniqueFailures(failures),
+ }
+}
+
+function evaluateArtifactMirrors(snapshot) {
+ const failures = []
+ const mirrored = []
+
+ for (const artifact of snapshot.artifacts || []) {
+ const needsMirror = artifact.large === true || artifact.restricted === true || artifact.storage === "lfs"
+ if (!needsMirror) {
+ continue
+ }
+
+ if (!artifact.mirror || artifact.mirror.status !== "ready") {
+ failures.push({ path: artifact.path, component: artifact.component, reason: "mirror_not_ready" })
+ continue
+ }
+
+ if (artifact.mirror.hash && artifact.mirror.hash !== artifact.hash) {
+ failures.push({ path: artifact.path, component: artifact.component, reason: "mirror_hash_mismatch" })
+ continue
+ }
+
+ mirrored.push({ path: artifact.path, component: artifact.component })
+ }
+
+ return { mirrored, failures }
+}
+
+function evaluateEnvironmentLocks(snapshot, policy) {
+ const locks = snapshot.environmentLocks || {}
+ return {
+ present: Object.keys(locks).filter((key) => Boolean(locks[key])),
+ missing: (policy.requiredEnvironmentLocks || []).filter((key) => !locks[key]),
+ }
+}
+
+function evaluateNotebookReplay(snapshot, now, policy) {
+ const notebooks = (snapshot.artifacts || []).filter((artifact) => artifact.component === "notebooks")
+ const failed = []
+ const stale = []
+ const missing = []
+
+ for (const notebook of notebooks) {
+ if (!notebook.replay) {
+ missing.push(notebook.path)
+ continue
+ }
+
+ if (notebook.replay.status !== "passed") {
+ failed.push(notebook.path)
+ continue
+ }
+
+ const ageDays = daysBetween(now, notebook.replay.completedAt)
+ if (ageDays > policy.maxNotebookReplayAgeDays) {
+ stale.push({ path: notebook.path, ageDays })
+ }
+ }
+
+ return {
+ total: notebooks.length,
+ failed,
+ stale,
+ missing,
+ }
+}
+
+function addCheck(checks, findings, check) {
+ checks.push({
+ code: check.code,
+ severity: check.severity,
+ pass: check.pass,
+ message: check.message,
+ evidence: check.evidence || {},
+ })
+
+ if (!check.pass) {
+ findings.push({
+ code: check.code,
+ severity: check.severity,
+ message: check.message,
+ remediation: check.remediation,
+ evidence: check.evidence || {},
+ })
+ }
+}
+
+function summarize(repositoryDecisions, remediationQueue) {
+ return {
+ totalRepositories: repositoryDecisions.length,
+ readyRepositories: repositoryDecisions.filter((repository) => repository.status === "restore_ready").length,
+ stewardReviewRepositories: repositoryDecisions.filter((repository) => repository.status === "steward_review").length,
+ heldRepositories: repositoryDecisions.filter((repository) => repository.status === "hold_restore_rehearsal").length,
+ totalFindings: remediationQueue.length,
+ criticalFindings: remediationQueue.filter((item) => item.severity === "critical").length,
+ warningFindings: remediationQueue.filter((item) => item.severity === "warning").length,
+ }
+}
+
+function findSnapshot(snapshots = [], snapshotId) {
+ if (!snapshotId) {
+ return null
+ }
+
+ return snapshots.find((snapshot) => snapshot.id === snapshotId) || null
+}
+
+function latestRestoreDrill(drills = [], snapshotId) {
+ const matching = drills.filter((drill) => drill.snapshotId === snapshotId)
+ matching.sort((a, b) => Date.parse(b.completedAt) - Date.parse(a.completedAt))
+ return matching[0] || null
+}
+
+function daysBetween(now, dateValue) {
+ if (!dateValue) {
+ return Number.POSITIVE_INFINITY
+ }
+
+ return Math.floor((now - Date.parse(dateValue)) / DAY_MS)
+}
+
+function normalizeArray(value) {
+ return Array.isArray(value) ? value.filter(Boolean) : []
+}
+
+function unique(values) {
+ return [...new Set(values)]
+}
+
+function uniqueFailures(failures) {
+ const seen = new Set()
+ return failures.filter((failure) => {
+ const key = `${failure.component}:${failure.reason}`
+ if (seen.has(key)) {
+ return false
+ }
+
+ seen.add(key)
+ return true
+ })
+}
+
+function remediationAction(code) {
+ const actions = {
+ candidate_snapshot_declared: "bind_release_candidate_snapshot",
+ snapshot_found: "restore_snapshot_catalog",
+ snapshot_recency: "refresh_release_snapshot",
+ restore_drill_recent_passed: "run_restore_rehearsal",
+ ref_coverage: "rebuild_snapshot_refs",
+ tag_coverage: "attach_release_tag",
+ component_manifest_integrity: "reconcile_component_manifest",
+ artifact_mirror_ready: "repair_artifact_mirror",
+ environment_lock_coverage: "attach_environment_locks",
+ notebook_replay_recent: "rerun_notebook_replay",
+ export_manifest_linked: "generate_export_manifest",
+ }
+
+ return actions[code] || "steward_review"
+}
+
+function digest(payload) {
+ return crypto
+ .createHash("sha256")
+ .update(JSON.stringify(payload))
+ .digest("hex")
+}
+
+module.exports = {
+ DEFAULT_POLICY,
+ evaluateRestoreRehearsal,
+}
diff --git a/repository-restore-rehearsal-guard/package.json b/repository-restore-rehearsal-guard/package.json
new file mode 100644
index 00000000..61856ecd
--- /dev/null
+++ b/repository-restore-rehearsal-guard/package.json
@@ -0,0 +1,11 @@
+{
+ "name": "repository-restore-rehearsal-guard",
+ "version": "1.0.0",
+ "private": true,
+ "type": "commonjs",
+ "scripts": {
+ "check": "node --check index.js && node --check sample-data.js && node --check test.js && node --check demo.js",
+ "test": "node test.js",
+ "demo": "node demo.js"
+ }
+}
diff --git a/repository-restore-rehearsal-guard/reports/demo.mp4 b/repository-restore-rehearsal-guard/reports/demo.mp4
new file mode 100644
index 00000000..87f6c63b
Binary files /dev/null and b/repository-restore-rehearsal-guard/reports/demo.mp4 differ
diff --git a/repository-restore-rehearsal-guard/reports/restore-rehearsal-packet.json b/repository-restore-rehearsal-guard/reports/restore-rehearsal-packet.json
new file mode 100644
index 00000000..70f358db
--- /dev/null
+++ b/repository-restore-rehearsal-guard/reports/restore-rehearsal-packet.json
@@ -0,0 +1,818 @@
+{
+ "asOf": "2026-05-23T02:00:00.000Z",
+ "policy": {
+ "maxSnapshotAgeDays": 30,
+ "maxRestoreDrillAgeDays": 21,
+ "maxNotebookReplayAgeDays": 14,
+ "requiredRefs": [
+ "refs/heads/main"
+ ],
+ "requiredComponents": [
+ "manuscript",
+ "data",
+ "code",
+ "metadata"
+ ],
+ "requiredEnvironmentLocks": [
+ "containerDigest",
+ "dependencyLock"
+ ],
+ "policyVersion": "repository-restore-rehearsal-v1"
+ },
+ "summary": {
+ "totalRepositories": 3,
+ "readyRepositories": 1,
+ "stewardReviewRepositories": 0,
+ "heldRepositories": 2,
+ "totalFindings": 10,
+ "criticalFindings": 6,
+ "warningFindings": 4
+ },
+ "repositories": [
+ {
+ "repositoryId": "neuro-reproducibility-atlas",
+ "title": "Neuro Reproducibility Atlas",
+ "status": "restore_ready",
+ "snapshotId": "snap-2026-05-21",
+ "releaseCandidate": {
+ "snapshotId": "snap-2026-05-21",
+ "tag": "v1.2.0",
+ "requiredRefs": [
+ "refs/heads/release/v1.2"
+ ]
+ },
+ "summary": {
+ "checks": 11,
+ "passedChecks": 11,
+ "criticalFindings": 0,
+ "warningFindings": 0
+ },
+ "checks": [
+ {
+ "code": "candidate_snapshot_declared",
+ "severity": "critical",
+ "pass": true,
+ "message": "Release candidate declares the snapshot that should be restored before export.",
+ "evidence": {}
+ },
+ {
+ "code": "snapshot_found",
+ "severity": "critical",
+ "pass": true,
+ "message": "Declared snapshot exists in the repository restore catalog.",
+ "evidence": {}
+ },
+ {
+ "code": "snapshot_recency",
+ "severity": "warning",
+ "pass": true,
+ "message": "Snapshot age is 1 days.",
+ "evidence": {
+ "snapshotAgeDays": 1,
+ "maxSnapshotAgeDays": 30
+ }
+ },
+ {
+ "code": "restore_drill_recent_passed",
+ "severity": "critical",
+ "pass": true,
+ "message": "Latest restore drill status is passed and age is 0 days.",
+ "evidence": {
+ "drillId": "drill-neuro-20260522",
+ "status": "passed",
+ "drillAgeDays": 0
+ }
+ },
+ {
+ "code": "ref_coverage",
+ "severity": "critical",
+ "pass": true,
+ "message": "Snapshot covers every required protected branch ref.",
+ "evidence": {
+ "requiredRefs": [
+ "refs/heads/main",
+ "refs/heads/release/v1.2"
+ ],
+ "missingRefs": []
+ }
+ },
+ {
+ "code": "tag_coverage",
+ "severity": "warning",
+ "pass": true,
+ "message": "Release tag v1.2.0 is present in the snapshot.",
+ "evidence": {
+ "tag": "v1.2.0"
+ }
+ },
+ {
+ "code": "component_manifest_integrity",
+ "severity": "critical",
+ "pass": true,
+ "message": "Required repository components match the snapshot manifest.",
+ "evidence": {
+ "covered": [
+ "manuscript",
+ "data",
+ "code",
+ "metadata"
+ ],
+ "failures": []
+ }
+ },
+ {
+ "code": "artifact_mirror_ready",
+ "severity": "critical",
+ "pass": true,
+ "message": "Large and restricted artifacts have matching restore mirrors.",
+ "evidence": {
+ "mirrored": [
+ {
+ "path": "data/participants.parquet",
+ "component": "data"
+ }
+ ],
+ "failures": []
+ }
+ },
+ {
+ "code": "environment_lock_coverage",
+ "severity": "warning",
+ "pass": true,
+ "message": "Environment locks are present for restore replay.",
+ "evidence": {
+ "present": [
+ "containerDigest",
+ "dependencyLock"
+ ],
+ "missing": []
+ }
+ },
+ {
+ "code": "notebook_replay_recent",
+ "severity": "warning",
+ "pass": true,
+ "message": "Notebook replay evidence reviewed for 1 notebook artifacts.",
+ "evidence": {
+ "total": 1,
+ "failed": [],
+ "stale": [],
+ "missing": []
+ }
+ },
+ {
+ "code": "export_manifest_linked",
+ "severity": "critical",
+ "pass": true,
+ "message": "Export manifest is linked to the restored snapshot.",
+ "evidence": {
+ "snapshotId": "snap-2026-05-21",
+ "exportSnapshotId": "snap-2026-05-21",
+ "doi": "10.5555/scibase.neuro-atlas.v1.2.0"
+ }
+ }
+ ],
+ "findings": [],
+ "remediationQueue": [],
+ "restorePacket": {
+ "policyVersion": "repository-restore-rehearsal-v1",
+ "candidateTag": "v1.2.0",
+ "restoredSnapshot": "snap-2026-05-21",
+ "decision": "restore_ready"
+ }
+ },
+ {
+ "repositoryId": "climate-table-fork",
+ "title": "Climate Table Fork",
+ "status": "hold_restore_rehearsal",
+ "snapshotId": "snap-2026-05-08",
+ "releaseCandidate": {
+ "snapshotId": "snap-2026-05-08",
+ "tag": "preprint-v0.9",
+ "requiredRefs": [
+ "refs/heads/release/preprint"
+ ]
+ },
+ "summary": {
+ "checks": 11,
+ "passedChecks": 5,
+ "criticalFindings": 4,
+ "warningFindings": 2
+ },
+ "checks": [
+ {
+ "code": "candidate_snapshot_declared",
+ "severity": "critical",
+ "pass": true,
+ "message": "Release candidate declares the snapshot that should be restored before export.",
+ "evidence": {}
+ },
+ {
+ "code": "snapshot_found",
+ "severity": "critical",
+ "pass": true,
+ "message": "Declared snapshot exists in the repository restore catalog.",
+ "evidence": {}
+ },
+ {
+ "code": "snapshot_recency",
+ "severity": "warning",
+ "pass": true,
+ "message": "Snapshot age is 14 days.",
+ "evidence": {
+ "snapshotAgeDays": 14,
+ "maxSnapshotAgeDays": 30
+ }
+ },
+ {
+ "code": "restore_drill_recent_passed",
+ "severity": "critical",
+ "pass": false,
+ "message": "Latest restore drill status is failed and age is 13 days.",
+ "evidence": {
+ "drillId": "drill-climate-20260509",
+ "status": "failed",
+ "drillAgeDays": 13
+ }
+ },
+ {
+ "code": "ref_coverage",
+ "severity": "critical",
+ "pass": false,
+ "message": "Snapshot is missing required refs: refs/heads/release/preprint.",
+ "evidence": {
+ "requiredRefs": [
+ "refs/heads/main",
+ "refs/heads/release/preprint"
+ ],
+ "missingRefs": [
+ "refs/heads/release/preprint"
+ ]
+ }
+ },
+ {
+ "code": "tag_coverage",
+ "severity": "warning",
+ "pass": false,
+ "message": "Release tag preprint-v0.9 is missing in the snapshot.",
+ "evidence": {
+ "tag": "preprint-v0.9"
+ }
+ },
+ {
+ "code": "component_manifest_integrity",
+ "severity": "critical",
+ "pass": false,
+ "message": "Component manifest has 1 restore blockers.",
+ "evidence": {
+ "covered": [
+ "manuscript",
+ "code",
+ "metadata"
+ ],
+ "failures": [
+ {
+ "component": "data",
+ "reason": "hash_mismatch",
+ "expectedHash": "sha256:expected-climate-data",
+ "snapshotHash": "sha256:actual-climate-data"
+ }
+ ]
+ }
+ },
+ {
+ "code": "artifact_mirror_ready",
+ "severity": "critical",
+ "pass": false,
+ "message": "Artifact mirror readiness has 1 blockers.",
+ "evidence": {
+ "mirrored": [],
+ "failures": [
+ {
+ "path": "data/raw-grid.parquet",
+ "component": "data",
+ "reason": "mirror_not_ready"
+ }
+ ]
+ }
+ },
+ {
+ "code": "environment_lock_coverage",
+ "severity": "warning",
+ "pass": false,
+ "message": "Missing environment locks: dependencyLock.",
+ "evidence": {
+ "present": [
+ "containerDigest"
+ ],
+ "missing": [
+ "dependencyLock"
+ ]
+ }
+ },
+ {
+ "code": "notebook_replay_recent",
+ "severity": "warning",
+ "pass": true,
+ "message": "Snapshot contains no notebooks requiring replay evidence.",
+ "evidence": {
+ "total": 0,
+ "failed": [],
+ "stale": [],
+ "missing": []
+ }
+ },
+ {
+ "code": "export_manifest_linked",
+ "severity": "critical",
+ "pass": true,
+ "message": "Export manifest is linked to the restored snapshot.",
+ "evidence": {
+ "snapshotId": "snap-2026-05-08",
+ "exportSnapshotId": "snap-2026-05-08",
+ "doi": "10.5555/scibase.climate.preprint"
+ }
+ }
+ ],
+ "findings": [
+ {
+ "code": "restore_drill_recent_passed",
+ "severity": "critical",
+ "message": "Latest restore drill status is failed and age is 13 days.",
+ "remediation": "Run a restore rehearsal from the immutable snapshot and attach the passed drill report.",
+ "evidence": {
+ "drillId": "drill-climate-20260509",
+ "status": "failed",
+ "drillAgeDays": 13
+ }
+ },
+ {
+ "code": "ref_coverage",
+ "severity": "critical",
+ "message": "Snapshot is missing required refs: refs/heads/release/preprint.",
+ "remediation": "Rebuild the snapshot with all protected branch refs required for release reconstruction.",
+ "evidence": {
+ "requiredRefs": [
+ "refs/heads/main",
+ "refs/heads/release/preprint"
+ ],
+ "missingRefs": [
+ "refs/heads/release/preprint"
+ ]
+ }
+ },
+ {
+ "code": "tag_coverage",
+ "severity": "warning",
+ "message": "Release tag preprint-v0.9 is missing in the snapshot.",
+ "remediation": "Attach the release tag to the immutable snapshot before DOI or export publication.",
+ "evidence": {
+ "tag": "preprint-v0.9"
+ }
+ },
+ {
+ "code": "component_manifest_integrity",
+ "severity": "critical",
+ "message": "Component manifest has 1 restore blockers.",
+ "remediation": "Regenerate the manifest from restored files and reconcile missing or mismatched component hashes.",
+ "evidence": {
+ "covered": [
+ "manuscript",
+ "code",
+ "metadata"
+ ],
+ "failures": [
+ {
+ "component": "data",
+ "reason": "hash_mismatch",
+ "expectedHash": "sha256:expected-climate-data",
+ "snapshotHash": "sha256:actual-climate-data"
+ }
+ ]
+ }
+ },
+ {
+ "code": "artifact_mirror_ready",
+ "severity": "critical",
+ "message": "Artifact mirror readiness has 1 blockers.",
+ "remediation": "Mirror required LFS or restricted artifacts and verify mirror digests before export.",
+ "evidence": {
+ "mirrored": [],
+ "failures": [
+ {
+ "path": "data/raw-grid.parquet",
+ "component": "data",
+ "reason": "mirror_not_ready"
+ }
+ ]
+ }
+ },
+ {
+ "code": "environment_lock_coverage",
+ "severity": "warning",
+ "message": "Missing environment locks: dependencyLock.",
+ "remediation": "Attach container image digests and dependency lockfiles used by the restore drill.",
+ "evidence": {
+ "present": [
+ "containerDigest"
+ ],
+ "missing": [
+ "dependencyLock"
+ ]
+ }
+ }
+ ],
+ "remediationQueue": [
+ {
+ "repositoryId": "climate-table-fork",
+ "snapshotId": "snap-2026-05-08",
+ "code": "restore_drill_recent_passed",
+ "severity": "critical",
+ "action": "run_restore_rehearsal",
+ "remediation": "Run a restore rehearsal from the immutable snapshot and attach the passed drill report."
+ },
+ {
+ "repositoryId": "climate-table-fork",
+ "snapshotId": "snap-2026-05-08",
+ "code": "ref_coverage",
+ "severity": "critical",
+ "action": "rebuild_snapshot_refs",
+ "remediation": "Rebuild the snapshot with all protected branch refs required for release reconstruction."
+ },
+ {
+ "repositoryId": "climate-table-fork",
+ "snapshotId": "snap-2026-05-08",
+ "code": "tag_coverage",
+ "severity": "warning",
+ "action": "attach_release_tag",
+ "remediation": "Attach the release tag to the immutable snapshot before DOI or export publication."
+ },
+ {
+ "repositoryId": "climate-table-fork",
+ "snapshotId": "snap-2026-05-08",
+ "code": "component_manifest_integrity",
+ "severity": "critical",
+ "action": "reconcile_component_manifest",
+ "remediation": "Regenerate the manifest from restored files and reconcile missing or mismatched component hashes."
+ },
+ {
+ "repositoryId": "climate-table-fork",
+ "snapshotId": "snap-2026-05-08",
+ "code": "artifact_mirror_ready",
+ "severity": "critical",
+ "action": "repair_artifact_mirror",
+ "remediation": "Mirror required LFS or restricted artifacts and verify mirror digests before export."
+ },
+ {
+ "repositoryId": "climate-table-fork",
+ "snapshotId": "snap-2026-05-08",
+ "code": "environment_lock_coverage",
+ "severity": "warning",
+ "action": "attach_environment_locks",
+ "remediation": "Attach container image digests and dependency lockfiles used by the restore drill."
+ }
+ ],
+ "restorePacket": {
+ "policyVersion": "repository-restore-rehearsal-v1",
+ "candidateTag": "preprint-v0.9",
+ "restoredSnapshot": "snap-2026-05-08",
+ "decision": "hold_restore_rehearsal"
+ }
+ },
+ {
+ "repositoryId": "protein-model-sandbox",
+ "title": "Protein Model Sandbox",
+ "status": "hold_restore_rehearsal",
+ "snapshotId": "snap-2026-04-14",
+ "releaseCandidate": {
+ "snapshotId": "snap-2026-04-14",
+ "tag": "v0.4.0",
+ "requiredRefs": [
+ "refs/heads/release/v0.4"
+ ]
+ },
+ "summary": {
+ "checks": 11,
+ "passedChecks": 7,
+ "criticalFindings": 2,
+ "warningFindings": 2
+ },
+ "checks": [
+ {
+ "code": "candidate_snapshot_declared",
+ "severity": "critical",
+ "pass": true,
+ "message": "Release candidate declares the snapshot that should be restored before export.",
+ "evidence": {}
+ },
+ {
+ "code": "snapshot_found",
+ "severity": "critical",
+ "pass": true,
+ "message": "Declared snapshot exists in the repository restore catalog.",
+ "evidence": {}
+ },
+ {
+ "code": "snapshot_recency",
+ "severity": "warning",
+ "pass": false,
+ "message": "Snapshot age is 38 days.",
+ "evidence": {
+ "snapshotAgeDays": 38,
+ "maxSnapshotAgeDays": 30
+ }
+ },
+ {
+ "code": "restore_drill_recent_passed",
+ "severity": "critical",
+ "pass": false,
+ "message": "Latest restore drill status is passed and age is 35 days.",
+ "evidence": {
+ "drillId": "drill-protein-20260417",
+ "status": "passed",
+ "drillAgeDays": 35
+ }
+ },
+ {
+ "code": "ref_coverage",
+ "severity": "critical",
+ "pass": true,
+ "message": "Snapshot covers every required protected branch ref.",
+ "evidence": {
+ "requiredRefs": [
+ "refs/heads/main",
+ "refs/heads/release/v0.4"
+ ],
+ "missingRefs": []
+ }
+ },
+ {
+ "code": "tag_coverage",
+ "severity": "warning",
+ "pass": true,
+ "message": "Release tag v0.4.0 is present in the snapshot.",
+ "evidence": {
+ "tag": "v0.4.0"
+ }
+ },
+ {
+ "code": "component_manifest_integrity",
+ "severity": "critical",
+ "pass": true,
+ "message": "Required repository components match the snapshot manifest.",
+ "evidence": {
+ "covered": [
+ "manuscript",
+ "data",
+ "code",
+ "metadata"
+ ],
+ "failures": []
+ }
+ },
+ {
+ "code": "artifact_mirror_ready",
+ "severity": "critical",
+ "pass": true,
+ "message": "Large and restricted artifacts have matching restore mirrors.",
+ "evidence": {
+ "mirrored": [
+ {
+ "path": "data/features.arrow",
+ "component": "data"
+ }
+ ],
+ "failures": []
+ }
+ },
+ {
+ "code": "environment_lock_coverage",
+ "severity": "warning",
+ "pass": true,
+ "message": "Environment locks are present for restore replay.",
+ "evidence": {
+ "present": [
+ "containerDigest",
+ "dependencyLock"
+ ],
+ "missing": []
+ }
+ },
+ {
+ "code": "notebook_replay_recent",
+ "severity": "warning",
+ "pass": false,
+ "message": "Notebook replay evidence reviewed for 1 notebook artifacts.",
+ "evidence": {
+ "total": 1,
+ "failed": [],
+ "stale": [
+ {
+ "path": "notebooks/evaluation.ipynb",
+ "ageDays": 36
+ }
+ ],
+ "missing": []
+ }
+ },
+ {
+ "code": "export_manifest_linked",
+ "severity": "critical",
+ "pass": false,
+ "message": "Export manifest is missing for the release snapshot.",
+ "evidence": {
+ "snapshotId": "snap-2026-04-14",
+ "exportSnapshotId": null,
+ "doi": null
+ }
+ }
+ ],
+ "findings": [
+ {
+ "code": "snapshot_recency",
+ "severity": "warning",
+ "message": "Snapshot age is 38 days.",
+ "remediation": "Cut a fresh snapshot or document why an older snapshot is still the release source.",
+ "evidence": {
+ "snapshotAgeDays": 38,
+ "maxSnapshotAgeDays": 30
+ }
+ },
+ {
+ "code": "restore_drill_recent_passed",
+ "severity": "critical",
+ "message": "Latest restore drill status is passed and age is 35 days.",
+ "remediation": "Run a restore rehearsal from the immutable snapshot and attach the passed drill report.",
+ "evidence": {
+ "drillId": "drill-protein-20260417",
+ "status": "passed",
+ "drillAgeDays": 35
+ }
+ },
+ {
+ "code": "notebook_replay_recent",
+ "severity": "warning",
+ "message": "Notebook replay evidence reviewed for 1 notebook artifacts.",
+ "remediation": "Replay notebooks from the restored environment and attach fresh pass evidence.",
+ "evidence": {
+ "total": 1,
+ "failed": [],
+ "stale": [
+ {
+ "path": "notebooks/evaluation.ipynb",
+ "ageDays": 36
+ }
+ ],
+ "missing": []
+ }
+ },
+ {
+ "code": "export_manifest_linked",
+ "severity": "critical",
+ "message": "Export manifest is missing for the release snapshot.",
+ "remediation": "Generate an export manifest that names the immutable snapshot and all restored components.",
+ "evidence": {
+ "snapshotId": "snap-2026-04-14",
+ "exportSnapshotId": null,
+ "doi": null
+ }
+ }
+ ],
+ "remediationQueue": [
+ {
+ "repositoryId": "protein-model-sandbox",
+ "snapshotId": "snap-2026-04-14",
+ "code": "snapshot_recency",
+ "severity": "warning",
+ "action": "refresh_release_snapshot",
+ "remediation": "Cut a fresh snapshot or document why an older snapshot is still the release source."
+ },
+ {
+ "repositoryId": "protein-model-sandbox",
+ "snapshotId": "snap-2026-04-14",
+ "code": "restore_drill_recent_passed",
+ "severity": "critical",
+ "action": "run_restore_rehearsal",
+ "remediation": "Run a restore rehearsal from the immutable snapshot and attach the passed drill report."
+ },
+ {
+ "repositoryId": "protein-model-sandbox",
+ "snapshotId": "snap-2026-04-14",
+ "code": "notebook_replay_recent",
+ "severity": "warning",
+ "action": "rerun_notebook_replay",
+ "remediation": "Replay notebooks from the restored environment and attach fresh pass evidence."
+ },
+ {
+ "repositoryId": "protein-model-sandbox",
+ "snapshotId": "snap-2026-04-14",
+ "code": "export_manifest_linked",
+ "severity": "critical",
+ "action": "generate_export_manifest",
+ "remediation": "Generate an export manifest that names the immutable snapshot and all restored components."
+ }
+ ],
+ "restorePacket": {
+ "policyVersion": "repository-restore-rehearsal-v1",
+ "candidateTag": "v0.4.0",
+ "restoredSnapshot": "snap-2026-04-14",
+ "decision": "hold_restore_rehearsal"
+ }
+ }
+ ],
+ "remediationQueue": [
+ {
+ "repositoryId": "climate-table-fork",
+ "snapshotId": "snap-2026-05-08",
+ "code": "restore_drill_recent_passed",
+ "severity": "critical",
+ "action": "run_restore_rehearsal",
+ "remediation": "Run a restore rehearsal from the immutable snapshot and attach the passed drill report."
+ },
+ {
+ "repositoryId": "climate-table-fork",
+ "snapshotId": "snap-2026-05-08",
+ "code": "ref_coverage",
+ "severity": "critical",
+ "action": "rebuild_snapshot_refs",
+ "remediation": "Rebuild the snapshot with all protected branch refs required for release reconstruction."
+ },
+ {
+ "repositoryId": "climate-table-fork",
+ "snapshotId": "snap-2026-05-08",
+ "code": "tag_coverage",
+ "severity": "warning",
+ "action": "attach_release_tag",
+ "remediation": "Attach the release tag to the immutable snapshot before DOI or export publication."
+ },
+ {
+ "repositoryId": "climate-table-fork",
+ "snapshotId": "snap-2026-05-08",
+ "code": "component_manifest_integrity",
+ "severity": "critical",
+ "action": "reconcile_component_manifest",
+ "remediation": "Regenerate the manifest from restored files and reconcile missing or mismatched component hashes."
+ },
+ {
+ "repositoryId": "climate-table-fork",
+ "snapshotId": "snap-2026-05-08",
+ "code": "artifact_mirror_ready",
+ "severity": "critical",
+ "action": "repair_artifact_mirror",
+ "remediation": "Mirror required LFS or restricted artifacts and verify mirror digests before export."
+ },
+ {
+ "repositoryId": "climate-table-fork",
+ "snapshotId": "snap-2026-05-08",
+ "code": "environment_lock_coverage",
+ "severity": "warning",
+ "action": "attach_environment_locks",
+ "remediation": "Attach container image digests and dependency lockfiles used by the restore drill."
+ },
+ {
+ "repositoryId": "protein-model-sandbox",
+ "snapshotId": "snap-2026-04-14",
+ "code": "snapshot_recency",
+ "severity": "warning",
+ "action": "refresh_release_snapshot",
+ "remediation": "Cut a fresh snapshot or document why an older snapshot is still the release source."
+ },
+ {
+ "repositoryId": "protein-model-sandbox",
+ "snapshotId": "snap-2026-04-14",
+ "code": "restore_drill_recent_passed",
+ "severity": "critical",
+ "action": "run_restore_rehearsal",
+ "remediation": "Run a restore rehearsal from the immutable snapshot and attach the passed drill report."
+ },
+ {
+ "repositoryId": "protein-model-sandbox",
+ "snapshotId": "snap-2026-04-14",
+ "code": "notebook_replay_recent",
+ "severity": "warning",
+ "action": "rerun_notebook_replay",
+ "remediation": "Replay notebooks from the restored environment and attach fresh pass evidence."
+ },
+ {
+ "repositoryId": "protein-model-sandbox",
+ "snapshotId": "snap-2026-04-14",
+ "code": "export_manifest_linked",
+ "severity": "critical",
+ "action": "generate_export_manifest",
+ "remediation": "Generate an export manifest that names the immutable snapshot and all restored components."
+ }
+ ],
+ "releaseQueue": [
+ {
+ "repositoryId": "neuro-reproducibility-atlas",
+ "snapshotId": "snap-2026-05-21",
+ "tag": "v1.2.0",
+ "action": "allow_release_export"
+ }
+ ],
+ "audit": {
+ "algorithm": "sha256",
+ "digest": "33359f292086b7e89d2ea1bf3c76937a1557755ddf9627660e44c40cb7f1d319",
+ "policyVersion": "repository-restore-rehearsal-v1"
+ }
+}
diff --git a/repository-restore-rehearsal-guard/reports/restore-rehearsal-report.md b/repository-restore-rehearsal-guard/reports/restore-rehearsal-report.md
new file mode 100644
index 00000000..d04eb385
--- /dev/null
+++ b/repository-restore-rehearsal-guard/reports/restore-rehearsal-report.md
@@ -0,0 +1,43 @@
+# Repository Restore Rehearsal Report
+
+Repositories reviewed: 3
+Restore-ready repositories: 1
+Held repositories: 2
+Critical findings: 6
+Warning findings: 4
+Audit digest: `33359f292086b7e89d2ea1bf3c76937a1557755ddf9627660e44c40cb7f1d319`
+
+## Repository Decisions
+
+### Neuro Reproducibility Atlas
+- Status: restore_ready
+- Snapshot: snap-2026-05-21
+- Candidate tag: v1.2.0
+- Checks passed: 11/11
+- Finding codes: none
+
+### Climate Table Fork
+- Status: hold_restore_rehearsal
+- Snapshot: snap-2026-05-08
+- Candidate tag: preprint-v0.9
+- Checks passed: 5/11
+- Finding codes: restore_drill_recent_passed, ref_coverage, tag_coverage, component_manifest_integrity, artifact_mirror_ready, environment_lock_coverage
+
+### Protein Model Sandbox
+- Status: hold_restore_rehearsal
+- Snapshot: snap-2026-04-14
+- Candidate tag: v0.4.0
+- Checks passed: 7/11
+- Finding codes: snapshot_recency, restore_drill_recent_passed, notebook_replay_recent, export_manifest_linked
+
+## Remediation Queue
+- climate-table-fork/snap-2026-05-08: run_restore_rehearsal (critical)
+- climate-table-fork/snap-2026-05-08: rebuild_snapshot_refs (critical)
+- climate-table-fork/snap-2026-05-08: attach_release_tag (warning)
+- climate-table-fork/snap-2026-05-08: reconcile_component_manifest (critical)
+- climate-table-fork/snap-2026-05-08: repair_artifact_mirror (critical)
+- climate-table-fork/snap-2026-05-08: attach_environment_locks (warning)
+- protein-model-sandbox/snap-2026-04-14: refresh_release_snapshot (warning)
+- protein-model-sandbox/snap-2026-04-14: run_restore_rehearsal (critical)
+- protein-model-sandbox/snap-2026-04-14: rerun_notebook_replay (warning)
+- protein-model-sandbox/snap-2026-04-14: generate_export_manifest (critical)
diff --git a/repository-restore-rehearsal-guard/reports/summary.svg b/repository-restore-rehearsal-guard/reports/summary.svg
new file mode 100644
index 00000000..a229779a
--- /dev/null
+++ b/repository-restore-rehearsal-guard/reports/summary.svg
@@ -0,0 +1,16 @@
+
diff --git a/repository-restore-rehearsal-guard/requirements-map.md b/repository-restore-rehearsal-guard/requirements-map.md
new file mode 100644
index 00000000..1e943c24
--- /dev/null
+++ b/repository-restore-rehearsal-guard/requirements-map.md
@@ -0,0 +1,15 @@
+# Requirements Map
+
+| Issue #10 requirement | Implementation coverage |
+| --- | --- |
+| File and metadata versioning | Checks immutable snapshot ids, semantic tags, refs, component hashes, and export manifest linkage. |
+| Git-compatible repository lifecycle | Validates protected branch refs and release tags before export. |
+| Hash-based integrity | Compares expected component hashes with restored snapshot artifact hashes. |
+| Git LFS / large files | Requires large or restricted artifacts to have ready mirrors with matching digests. |
+| Computation-aware reproducibility | Requires recent passed restore drills, environment locks, and notebook replay evidence. |
+| Repository identifiers and citation | Blocks export when the DOI/export manifest is missing or points at the wrong snapshot. |
+| Programmatic export | Emits a deterministic release queue, remediation queue, and audit packet suitable for API or reviewer dashboard integration. |
+
+## Non-Overlap Notes
+
+This PR is scoped to restore rehearsal readiness. It does not implement generic repository ledgers, rollback UI, legal-hold workflows, release embargo, access review, dependency licensing, component-owner approval quorum, or sensitive-artifact scanning.
diff --git a/repository-restore-rehearsal-guard/sample-data.js b/repository-restore-rehearsal-guard/sample-data.js
new file mode 100644
index 00000000..7dbcc4ac
--- /dev/null
+++ b/repository-restore-rehearsal-guard/sample-data.js
@@ -0,0 +1,177 @@
+const policy = {
+ maxSnapshotAgeDays: 30,
+ maxRestoreDrillAgeDays: 21,
+ maxNotebookReplayAgeDays: 14,
+ requiredRefs: ["refs/heads/main"],
+ requiredEnvironmentLocks: ["containerDigest", "dependencyLock"],
+}
+
+const repositories = [
+ {
+ repositoryId: "neuro-reproducibility-atlas",
+ title: "Neuro Reproducibility Atlas",
+ releaseCandidate: {
+ snapshotId: "snap-2026-05-21",
+ tag: "v1.2.0",
+ requiredRefs: ["refs/heads/release/v1.2"],
+ },
+ expectedComponents: [
+ { component: "manuscript", hash: "sha256:manuscript-9f4a" },
+ { component: "data", hash: "sha256:data-18aa" },
+ { component: "code", hash: "sha256:code-73be" },
+ { component: "metadata", hash: "sha256:metadata-4420" },
+ ],
+ snapshots: [
+ {
+ id: "snap-2026-05-21",
+ createdAt: "2026-05-21T10:00:00.000Z",
+ refs: ["refs/heads/main", "refs/heads/release/v1.2"],
+ tags: ["v1.2.0"],
+ environmentLocks: {
+ containerDigest: "sha256:container-20260521",
+ dependencyLock: "renv.lock@sha256:836d",
+ },
+ exportManifest: {
+ snapshotId: "snap-2026-05-21",
+ doi: "10.5555/scibase.neuro-atlas.v1.2.0",
+ },
+ artifacts: [
+ { component: "manuscript", path: "manuscript/main.md", hash: "sha256:manuscript-9f4a" },
+ {
+ component: "data",
+ path: "data/participants.parquet",
+ hash: "sha256:data-18aa",
+ large: true,
+ storage: "lfs",
+ mirror: { status: "ready", hash: "sha256:data-18aa" },
+ },
+ { component: "code", path: "code/pipeline.py", hash: "sha256:code-73be" },
+ {
+ component: "notebooks",
+ path: "notebooks/reproduce.ipynb",
+ hash: "sha256:notebook-530a",
+ replay: { status: "passed", completedAt: "2026-05-22T08:00:00.000Z" },
+ },
+ { component: "metadata", path: "metadata.json", hash: "sha256:metadata-4420" },
+ ],
+ },
+ ],
+ restoreDrills: [
+ {
+ id: "drill-neuro-20260522",
+ snapshotId: "snap-2026-05-21",
+ status: "passed",
+ completedAt: "2026-05-22T09:00:00.000Z",
+ },
+ ],
+ },
+ {
+ repositoryId: "climate-table-fork",
+ title: "Climate Table Fork",
+ releaseCandidate: {
+ snapshotId: "snap-2026-05-08",
+ tag: "preprint-v0.9",
+ requiredRefs: ["refs/heads/release/preprint"],
+ },
+ expectedComponents: [
+ { component: "manuscript", hash: "sha256:climate-manuscript" },
+ { component: "data", hash: "sha256:expected-climate-data" },
+ { component: "code", hash: "sha256:climate-code" },
+ { component: "metadata", hash: "sha256:climate-metadata" },
+ ],
+ snapshots: [
+ {
+ id: "snap-2026-05-08",
+ createdAt: "2026-05-08T11:00:00.000Z",
+ refs: ["refs/heads/main"],
+ tags: [],
+ environmentLocks: {
+ containerDigest: "sha256:container-climate",
+ },
+ exportManifest: {
+ snapshotId: "snap-2026-05-08",
+ doi: "10.5555/scibase.climate.preprint",
+ },
+ artifacts: [
+ { component: "manuscript", path: "manuscript/paper.md", hash: "sha256:climate-manuscript" },
+ {
+ component: "data",
+ path: "data/raw-grid.parquet",
+ hash: "sha256:actual-climate-data",
+ large: true,
+ storage: "lfs",
+ mirror: { status: "missing" },
+ },
+ { component: "code", path: "code/model.py", hash: "sha256:climate-code" },
+ { component: "metadata", path: "metadata.json", hash: "sha256:climate-metadata" },
+ ],
+ },
+ ],
+ restoreDrills: [
+ {
+ id: "drill-climate-20260509",
+ snapshotId: "snap-2026-05-08",
+ status: "failed",
+ completedAt: "2026-05-09T09:00:00.000Z",
+ },
+ ],
+ },
+ {
+ repositoryId: "protein-model-sandbox",
+ title: "Protein Model Sandbox",
+ releaseCandidate: {
+ snapshotId: "snap-2026-04-14",
+ tag: "v0.4.0",
+ requiredRefs: ["refs/heads/release/v0.4"],
+ },
+ expectedComponents: [
+ { component: "manuscript", hash: "sha256:protein-manuscript" },
+ { component: "data", hash: "sha256:protein-data" },
+ { component: "code", hash: "sha256:protein-code" },
+ { component: "metadata", hash: "sha256:protein-metadata" },
+ ],
+ snapshots: [
+ {
+ id: "snap-2026-04-14",
+ createdAt: "2026-04-14T09:00:00.000Z",
+ refs: ["refs/heads/main", "refs/heads/release/v0.4"],
+ tags: ["v0.4.0"],
+ environmentLocks: {
+ containerDigest: "sha256:container-protein",
+ dependencyLock: "requirements.txt@sha256:7e8a",
+ },
+ artifacts: [
+ { component: "manuscript", path: "manuscript/model.md", hash: "sha256:protein-manuscript" },
+ {
+ component: "data",
+ path: "data/features.arrow",
+ hash: "sha256:protein-data",
+ large: true,
+ mirror: { status: "ready", hash: "sha256:protein-data" },
+ },
+ { component: "code", path: "code/train.py", hash: "sha256:protein-code" },
+ {
+ component: "notebooks",
+ path: "notebooks/evaluation.ipynb",
+ hash: "sha256:protein-notebook",
+ replay: { status: "passed", completedAt: "2026-04-16T10:00:00.000Z" },
+ },
+ { component: "metadata", path: "metadata.json", hash: "sha256:protein-metadata" },
+ ],
+ },
+ ],
+ restoreDrills: [
+ {
+ id: "drill-protein-20260417",
+ snapshotId: "snap-2026-04-14",
+ status: "passed",
+ completedAt: "2026-04-17T10:00:00.000Z",
+ },
+ ],
+ },
+]
+
+module.exports = {
+ policy,
+ repositories,
+}
diff --git a/repository-restore-rehearsal-guard/test.js b/repository-restore-rehearsal-guard/test.js
new file mode 100644
index 00000000..fba19bb2
--- /dev/null
+++ b/repository-restore-rehearsal-guard/test.js
@@ -0,0 +1,96 @@
+const assert = require("node:assert/strict")
+const { evaluateRestoreRehearsal } = require("./index")
+const { repositories, policy } = require("./sample-data")
+
+const packet = evaluateRestoreRehearsal({
+ asOf: "2026-05-23T02:00:00.000Z",
+ repositories,
+ policy,
+})
+
+assert.equal(packet.summary.totalRepositories, 3)
+assert.equal(packet.summary.readyRepositories, 1)
+assert.equal(packet.summary.stewardReviewRepositories, 0)
+assert.equal(packet.summary.heldRepositories, 2)
+assert.equal(packet.summary.criticalFindings, 6)
+assert.equal(packet.summary.warningFindings, 4)
+assert.equal(packet.releaseQueue.length, 1)
+assert.equal(packet.releaseQueue[0].repositoryId, "neuro-reproducibility-atlas")
+
+const ready = packet.repositories.find((repository) => repository.repositoryId === "neuro-reproducibility-atlas")
+assert.equal(ready.status, "restore_ready")
+assert(ready.checks.every((check) => check.pass))
+assert.equal(ready.restorePacket.candidateTag, "v1.2.0")
+
+const climate = packet.repositories.find((repository) => repository.repositoryId === "climate-table-fork")
+assert.equal(climate.status, "hold_restore_rehearsal")
+assert(climate.findings.some((finding) => finding.code === "restore_drill_recent_passed"))
+assert(climate.findings.some((finding) => finding.code === "ref_coverage"))
+assert(climate.findings.some((finding) => finding.code === "component_manifest_integrity"))
+assert(climate.findings.some((finding) => finding.code === "artifact_mirror_ready"))
+assert(climate.findings.some((finding) => finding.code === "environment_lock_coverage"))
+
+const climateManifest = climate.findings.find((finding) => finding.code === "component_manifest_integrity")
+assert(climateManifest.evidence.failures.some((failure) => failure.reason === "hash_mismatch"))
+
+const protein = packet.repositories.find((repository) => repository.repositoryId === "protein-model-sandbox")
+assert.equal(protein.status, "hold_restore_rehearsal")
+assert(protein.findings.some((finding) => finding.code === "snapshot_recency"))
+assert(protein.findings.some((finding) => finding.code === "restore_drill_recent_passed"))
+assert(protein.findings.some((finding) => finding.code === "notebook_replay_recent"))
+assert(protein.findings.some((finding) => finding.code === "export_manifest_linked"))
+
+const minimalReady = evaluateRestoreRehearsal({
+ asOf: "2026-05-23T02:00:00.000Z",
+ repositories: [{
+ repositoryId: "minimal-ready",
+ title: "Minimal Ready",
+ releaseCandidate: {
+ snapshotId: "snap-minimal",
+ tag: "v1.0.0",
+ },
+ expectedComponents: [
+ { component: "manuscript", hash: "sha256:a" },
+ { component: "data", hash: "sha256:b" },
+ { component: "code", hash: "sha256:c" },
+ { component: "metadata", hash: "sha256:d" },
+ ],
+ snapshots: [{
+ id: "snap-minimal",
+ createdAt: "2026-05-22T02:00:00.000Z",
+ refs: ["refs/heads/main"],
+ tags: ["v1.0.0"],
+ environmentLocks: {
+ containerDigest: "sha256:container",
+ dependencyLock: "package-lock.json@sha256:1",
+ },
+ exportManifest: {
+ snapshotId: "snap-minimal",
+ },
+ artifacts: [
+ { component: "manuscript", path: "manuscript/main.md", hash: "sha256:a" },
+ { component: "data", path: "data/table.csv", hash: "sha256:b" },
+ { component: "code", path: "code/analysis.py", hash: "sha256:c" },
+ { component: "metadata", path: "metadata.json", hash: "sha256:d" },
+ ],
+ }],
+ restoreDrills: [{
+ id: "drill-minimal",
+ snapshotId: "snap-minimal",
+ status: "passed",
+ completedAt: "2026-05-22T03:00:00.000Z",
+ }],
+ }],
+ policy,
+})
+assert.equal(minimalReady.summary.readyRepositories, 1)
+assert.equal(minimalReady.summary.totalFindings, 0)
+
+const repeated = evaluateRestoreRehearsal({
+ asOf: "2026-05-23T02:00:00.000Z",
+ repositories,
+ policy,
+})
+assert.equal(repeated.audit.digest, packet.audit.digest)
+
+console.log("repository restore rehearsal guard tests passed")