diff --git a/repository-external-reference-pin-guard/.gitignore b/repository-external-reference-pin-guard/.gitignore
new file mode 100644
index 00000000..eddc6484
--- /dev/null
+++ b/repository-external-reference-pin-guard/.gitignore
@@ -0,0 +1,4 @@
+node_modules/
+reports/demo-frame.png
+__pycache__/
+*.tmp
diff --git a/repository-external-reference-pin-guard/README.md b/repository-external-reference-pin-guard/README.md
new file mode 100644
index 00000000..6798860a
--- /dev/null
+++ b/repository-external-reference-pin-guard/README.md
@@ -0,0 +1,32 @@
+# Repository External Reference Pin Guard
+
+Self-contained Project Repository & Version Control slice for issue #10.
+
+This module checks whether a scientific repository can safely publish a DOI, citation badge, API view, or export bundle when it depends on external references such as Git submodules, linked datasets, API snapshots, model weights, or external code/data pointers.
+
+## What It Checks
+
+- Git submodules and external code are pinned to immutable commit SHAs, rejecting null all-zero placeholders.
+- Linked datasets and model weights have full-length SHA checksum, parseable DOI, or immutable version evidence; placeholders such as `pending` and floating aliases such as `latest` do not count.
+- Supplied checksum and DOI metadata must be valid even when another durable identifier is present, so malformed evidence cannot slip into export or citation packets.
+- The external-reference manifest itself must be an array, so object-shaped or missing reviewer data cannot be treated as a clean empty audit.
+- Malformed external-reference entries create release-blocking repair actions instead of crashing assessment or disappearing from reviewer packets.
+- API sources use parseable, non-future dated snapshots with full-length SHA checksum evidence instead of floating "latest" endpoints.
+- Export bundles do not require authenticated external references.
+- License and attribution metadata are present before DOI publication.
+- Reference verification evidence is present, fresh enough for release, and not future-dated.
+
+## Commands
+
+```powershell
+npm test
+npm run demo
+npm run video
+npm run check
+```
+
+The demo writes deterministic JSON, Markdown, SVG, and MP4 reviewer artifacts under `reports/`.
+
+## Safety
+
+All records are synthetic. The module does not call external repositories, APIs, DOI registries, identity providers, storage systems, payment systems, or private research databases.
diff --git a/repository-external-reference-pin-guard/acceptance-notes.md b/repository-external-reference-pin-guard/acceptance-notes.md
new file mode 100644
index 00000000..7177cb8c
--- /dev/null
+++ b/repository-external-reference-pin-guard/acceptance-notes.md
@@ -0,0 +1,34 @@
+# Acceptance Notes
+
+## Local Validation
+
+Run from `repository-external-reference-pin-guard/`:
+
+```powershell
+npm test
+npm run demo
+npm run video
+npm run check
+```
+
+Expected evidence:
+
+- `reports/blocked-packet.json` blocks repository release when external references are floating, authenticated only, stale, missing durable identifiers, or carrying malformed checksum/DOI evidence.
+- All-zero Git commit placeholders are treated as unpinned references rather than immutable release evidence.
+- Floating version aliases such as `latest` are blocked unless the reference also has checksum or DOI evidence.
+- Invalid checksum placeholders such as `pending` do not count as durable identifier or API snapshot evidence and now produce explicit evidence-repair actions.
+- Invalid DOI placeholders such as `pending` do not count as durable identifier evidence and now produce explicit evidence-repair actions.
+- Truncated checksum values such as `sha256:abcdef` do not count as API snapshot evidence or export metadata even when another identifier is valid.
+- Malformed object-shaped reference manifests produce `MALFORMED_REFERENCE_MANIFEST` blockers and `repair_reference_manifest:*` actions instead of being treated as empty clean audits.
+- Malformed external-reference entries produce `MALFORMED_REFERENCE_ENTRY` blockers and `repair_reference_entry:*` actions instead of crashing or disappearing from reviewer packets.
+- Future-dated API snapshots do not count as pinned snapshot evidence for DOI/export release.
+- Otherwise pinned references without verification timestamps are blocked until verification evidence is refreshed.
+- `reports/warning-packet.json` stages pinned references that still need license and attribution metadata.
+- `reports/clean-packet.json` releases a repository with immutable external pins, fresh verification evidence, and exportable metadata.
+- `reports/external-reference-report.md` summarizes lanes and finding codes.
+- `reports/summary.svg` provides a visual reviewer packet.
+- `reports/demo.mp4` is a short H.264 walkthrough generated from synthetic frames.
+
+## Safety Boundaries
+
+All records are synthetic. No external repositories, live APIs, DOI registries, object stores, credentials, private research data, identity providers, or payment systems are contacted.
diff --git a/repository-external-reference-pin-guard/demo.js b/repository-external-reference-pin-guard/demo.js
new file mode 100644
index 00000000..fd6c14e0
--- /dev/null
+++ b/repository-external-reference-pin-guard/demo.js
@@ -0,0 +1,60 @@
+const fs = require('fs');
+const path = require('path');
+
+const { assessExternalReferences } = require('./index');
+const {
+ riskyRepository,
+ cleanRepository,
+ warningRepository,
+ malformedRepository,
+ malformedManifestRepository
+} = require('./sample-data');
+
+const reportsDir = path.join(__dirname, 'reports');
+fs.mkdirSync(reportsDir, { recursive: true });
+
+const packets = [
+ ['blocked-packet.json', assessExternalReferences(riskyRepository)],
+ ['malformed-manifest-packet.json', assessExternalReferences(malformedManifestRepository)],
+ ['malformed-packet.json', assessExternalReferences(malformedRepository)],
+ ['clean-packet.json', assessExternalReferences(cleanRepository)],
+ ['warning-packet.json', assessExternalReferences(warningRepository)]
+];
+
+for (const [fileName, packet] of packets) {
+ fs.writeFileSync(path.join(reportsDir, fileName), `${JSON.stringify(packet, null, 2)}\n`);
+ console.log(`${fileName}: ${packet.status}; findings=${packet.findings.length}; digest=${packet.auditDigest.slice(0, 12)}`);
+}
+
+const markdown = [
+ '# Repository External Reference Pin Guard Report',
+ '',
+ '| Packet | Status | DOI publication | Export bundle | API access | Findings |',
+ '| --- | --- | --- | --- | --- | --- |',
+ ...packets.map(([fileName, packet]) => `| ${fileName} | ${packet.status} | ${packet.releaseLanes.doiPublication} | ${packet.releaseLanes.exportBundle} | ${packet.releaseLanes.apiAccess} | ${packet.findings.map((finding) => finding.code).join(', ') || 'none'} |`),
+ '',
+ 'Synthetic data only. No external repositories, APIs, DOI registries, or private data sources are contacted.'
+].join('\n');
+fs.writeFileSync(path.join(reportsDir, 'external-reference-report.md'), `${markdown}\n`);
+
+const rows = packets.map(([fileName, packet], index) => {
+ const y = 94 + index * 68;
+ const color = packet.status === 'hold_repository_release' ? '#b91c1c' : packet.status === 'stage_reference_metadata_revision' ? '#a16207' : '#047857';
+ return `
+
+
+
+ ${packet.repositoryId}
+ ${fileName} | findings ${packet.findings.length} | digest ${packet.auditDigest.slice(0, 16)}
+ `;
+}).join('');
+
+const svgHeight = 408;
+const svg = `
+`;
+fs.writeFileSync(path.join(reportsDir, 'summary.svg'), svg);
diff --git a/repository-external-reference-pin-guard/index.js b/repository-external-reference-pin-guard/index.js
new file mode 100644
index 00000000..ea4d621b
--- /dev/null
+++ b/repository-external-reference-pin-guard/index.js
@@ -0,0 +1,312 @@
+const crypto = require('crypto');
+
+const DEFAULT_POLICY = {
+ maxReferenceAgeDays: 180
+};
+
+const CHECKSUM_HEX_LENGTHS = {
+ sha256: 64,
+ sha384: 96,
+ sha512: 128
+};
+
+function assessExternalReferences(repository) {
+ const policy = { ...DEFAULT_POLICY, ...(repository.policy || {}) };
+ const references = normalizeReferenceEntries(repository.references);
+ const findings = references
+ .flatMap((reference) => assessReference(reference, repository.assessedAt, policy))
+ .sort(compareFindings);
+
+ const blockerCount = findings.filter((finding) => finding.severity === 'blocker').length;
+ const warningCount = findings.filter((finding) => finding.severity === 'warning').length;
+
+ const packet = {
+ repositoryId: repository.repositoryId,
+ status: chooseStatus(blockerCount, warningCount),
+ releaseLanes: chooseReleaseLanes(blockerCount, warningCount),
+ findings,
+ actions: buildActions(repository, findings),
+ referenceSignals: buildSignals(findings),
+ referenceSummary: summarizeReferences(references),
+ assessedAt: repository.assessedAt
+ };
+
+ packet.auditDigest = digestPacket(packet);
+ return packet;
+}
+
+function assessReference(reference, assessedAt, policy) {
+ if (reference.__malformedReferenceManifest) {
+ return [
+ finding(reference, 'MALFORMED_REFERENCE_MANIFEST', 'blocker', 'External reference manifest must be an array before release.')
+ ];
+ }
+
+ if (reference.__malformedReferenceEntry) {
+ return [
+ finding(reference, 'MALFORMED_REFERENCE_ENTRY', 'blocker', 'External reference entry must be a structured object before release.')
+ ];
+ }
+
+ const findings = [];
+
+ if (hasInvalidChecksumEvidence(reference)) {
+ findings.push(finding(reference, 'INVALID_CHECKSUM_EVIDENCE', 'blocker', 'Checksum evidence must use a supported algorithm and full-length hexadecimal digest.'));
+ }
+
+ if (hasInvalidDoiEvidence(reference)) {
+ findings.push(finding(reference, 'INVALID_DOI_EVIDENCE', 'blocker', 'DOI evidence must be a parseable DOI or DOI URL before release.'));
+ }
+
+ if (isGitReference(reference) && !hasPinnedCommit(reference)) {
+ findings.push(finding(reference, 'FLOATING_GIT_REFERENCE', 'blocker', 'Git reference must be pinned to an immutable commit SHA before release.'));
+ }
+
+ if (reference.kind === 'api_source' && !hasSnapshotEvidence(reference, assessedAt)) {
+ findings.push(finding(reference, 'FLOATING_API_REFERENCE', 'blocker', 'API source must reference a dated snapshot with checksum evidence.'));
+ }
+
+ if (needsDurableIdentifier(reference) && !hasDurableIdentifier(reference)) {
+ findings.push(finding(reference, 'MISSING_DURABLE_IDENTIFIER', 'blocker', 'External data or model reference needs a checksum, DOI, or immutable version.'));
+ }
+
+ if (reference.authRequired) {
+ findings.push(finding(reference, 'AUTH_REQUIRED_REFERENCE', 'blocker', 'Export bundles cannot depend on authenticated external references.'));
+ }
+
+ if (!hasText(reference.license)) {
+ findings.push(finding(reference, 'MISSING_LICENSE', 'warning', 'External reference needs license metadata before DOI publication.'));
+ }
+
+ if (!hasText(reference.attribution)) {
+ findings.push(finding(reference, 'MISSING_ATTRIBUTION', 'warning', 'External reference needs attribution metadata before DOI publication.'));
+ }
+
+ if (hasMissingVerificationEvidence(reference, findings)) {
+ findings.push(finding(reference, 'STALE_REFERENCE_EVIDENCE', 'blocker', 'External reference needs verification evidence before release.'));
+ } else if (isStale(reference.lastVerifiedAt, assessedAt, policy.maxReferenceAgeDays)) {
+ findings.push(finding(reference, 'STALE_REFERENCE_EVIDENCE', 'blocker', 'External reference verification is older than policy allows.'));
+ }
+
+ return findings;
+}
+
+function isGitReference(reference) {
+ return reference.kind === 'git_submodule' || reference.kind === 'external_code';
+}
+
+function hasPinnedCommit(reference) {
+ const commitSha = reference.commitSha || '';
+ return /^[a-f0-9]{40}$/i.test(commitSha) && !/^0{40}$/.test(commitSha);
+}
+
+function hasSnapshotEvidence(reference, assessedAt) {
+ if (!hasText(reference.snapshotDate) || !hasValidChecksum(reference.checksum)) return false;
+ const snapshot = Date.parse(reference.snapshotDate);
+ const assessed = Date.parse(assessedAt);
+ if (Number.isNaN(snapshot) || Number.isNaN(assessed)) return false;
+ return snapshot <= assessed;
+}
+
+function needsDurableIdentifier(reference) {
+ return ['linked_dataset', 'model_weights', 'external_data'].includes(reference.kind);
+}
+
+function normalizeReferenceEntries(value) {
+ if (!Array.isArray(value)) {
+ return [
+ {
+ id: 'reference-manifest',
+ kind: 'unknown',
+ target: null,
+ __malformedReferenceManifest: true
+ }
+ ];
+ }
+
+ return value.map((reference, index) => {
+ if (isRecord(reference)) return reference;
+
+ return {
+ id: `malformed-reference-entry-${index + 1}`,
+ kind: 'unknown',
+ target: null,
+ __malformedReferenceEntry: true
+ };
+ });
+}
+
+function isRecord(value) {
+ return value && typeof value === 'object' && !Array.isArray(value);
+}
+
+function hasDurableIdentifier(reference) {
+ return hasValidChecksum(reference.checksum) || hasValidDoi(reference.doi) || hasImmutableVersion(reference.version);
+}
+
+function hasValidChecksum(checksum) {
+ if (typeof checksum !== 'string') return false;
+ const match = checksum.trim().match(/^(sha256|sha384|sha512):([a-f0-9]+)$/i);
+ if (!match) return false;
+
+ const algorithm = match[1].toLowerCase();
+ return match[2].length === CHECKSUM_HEX_LENGTHS[algorithm];
+}
+
+function hasValidDoi(doi) {
+ if (!hasText(doi)) return false;
+ const normalized = doi.trim().replace(/^https?:\/\/(dx\.)?doi\.org\//i, '');
+ return /^10\.\d{4,9}\/\S+$/i.test(normalized);
+}
+
+function hasInvalidChecksumEvidence(reference) {
+ return hasText(reference.checksum) && !hasValidChecksum(reference.checksum);
+}
+
+function hasInvalidDoiEvidence(reference) {
+ return hasText(reference.doi) && !hasValidDoi(reference.doi);
+}
+
+function hasImmutableVersion(version) {
+ if (!hasText(version)) return false;
+ return !/^(latest|main|master|head|current|stable|dev|nightly)$/i.test(version.trim());
+}
+
+function hasMissingVerificationEvidence(reference, findings) {
+ return !hasText(reference.lastVerifiedAt)
+ && !findings.some((item) => item.severity === 'blocker');
+}
+
+function isStale(lastVerifiedAt, assessedAt, maxAgeDays) {
+ if (!lastVerifiedAt || !assessedAt) return false;
+ const verified = Date.parse(lastVerifiedAt);
+ const assessed = Date.parse(assessedAt);
+ if (Number.isNaN(verified) || Number.isNaN(assessed)) return true;
+ if (verified > assessed) return true;
+ const ageDays = Math.max(0, (assessed - verified) / (24 * 60 * 60 * 1000));
+ return ageDays > maxAgeDays;
+}
+
+function chooseStatus(blockerCount, warningCount) {
+ if (blockerCount > 0) return 'hold_repository_release';
+ if (warningCount > 0) return 'stage_reference_metadata_revision';
+ return 'release_repository_references';
+}
+
+function chooseReleaseLanes(blockerCount, warningCount) {
+ if (blockerCount > 0) {
+ return {
+ doiPublication: 'blocked',
+ exportBundle: 'blocked',
+ apiAccess: 'metadata_only'
+ };
+ }
+
+ if (warningCount > 0) {
+ return {
+ doiPublication: 'metadata_revision',
+ exportBundle: 'draft_only',
+ apiAccess: 'allowed'
+ };
+ }
+
+ return {
+ doiPublication: 'allowed',
+ exportBundle: 'allowed',
+ apiAccess: 'allowed'
+ };
+}
+
+function buildActions(repository, findings) {
+ if (!findings.length) return [`release_with_reference_pin_monitoring:${repository.repositoryId}`];
+
+ const byReference = new Map();
+ for (const item of findings) {
+ if (!byReference.has(item.referenceId)) byReference.set(item.referenceId, new Set());
+ byReference.get(item.referenceId).add(item.code);
+ }
+
+ const actions = new Set();
+ for (const [referenceId, codes] of byReference.entries()) {
+ if (codes.has('MALFORMED_REFERENCE_MANIFEST')) actions.add(`repair_reference_manifest:${referenceId}`);
+ if (codes.has('MALFORMED_REFERENCE_ENTRY')) actions.add(`repair_reference_entry:${referenceId}`);
+ if (codes.has('AUTH_REQUIRED_REFERENCE')) actions.add(`replace_or_snapshot_auth_reference:${referenceId}`);
+ if (codes.has('FLOATING_GIT_REFERENCE') || codes.has('FLOATING_API_REFERENCE')) actions.add(`pin_external_reference:${referenceId}`);
+ if (codes.has('INVALID_CHECKSUM_EVIDENCE') || codes.has('INVALID_DOI_EVIDENCE')) actions.add(`repair_reference_evidence:${referenceId}`);
+ if (codes.has('MISSING_DURABLE_IDENTIFIER')) actions.add(`add_checksum_or_doi:${referenceId}`);
+ if (codes.has('STALE_REFERENCE_EVIDENCE')) actions.add(`refresh_reference_verification:${referenceId}`);
+ if (codes.has('MISSING_LICENSE') || codes.has('MISSING_ATTRIBUTION')) actions.add(`complete_license_attribution:${referenceId}`);
+ }
+
+ return [...actions].sort();
+}
+
+function buildSignals(findings) {
+ const codes = new Set(findings.map((finding) => finding.code));
+ return {
+ immutablePins: !codes.has('MALFORMED_REFERENCE_MANIFEST')
+ && !codes.has('MALFORMED_REFERENCE_ENTRY')
+ && !codes.has('FLOATING_GIT_REFERENCE')
+ && !codes.has('FLOATING_API_REFERENCE'),
+ exportable: !codes.has('MALFORMED_REFERENCE_MANIFEST')
+ && !codes.has('MALFORMED_REFERENCE_ENTRY')
+ && !codes.has('AUTH_REQUIRED_REFERENCE')
+ && !codes.has('MISSING_DURABLE_IDENTIFIER')
+ && !codes.has('INVALID_CHECKSUM_EVIDENCE')
+ && !codes.has('INVALID_DOI_EVIDENCE'),
+ attributionComplete: !codes.has('MALFORMED_REFERENCE_MANIFEST')
+ && !codes.has('MALFORMED_REFERENCE_ENTRY')
+ && !codes.has('MISSING_LICENSE')
+ && !codes.has('MISSING_ATTRIBUTION'),
+ verificationFresh: !codes.has('MALFORMED_REFERENCE_MANIFEST')
+ && !codes.has('MALFORMED_REFERENCE_ENTRY')
+ && !codes.has('STALE_REFERENCE_EVIDENCE')
+ };
+}
+
+function summarizeReferences(references = []) {
+ return references.reduce((summary, reference) => {
+ summary.total += 1;
+ summary.byKind[reference.kind] = (summary.byKind[reference.kind] || 0) + 1;
+ return summary;
+ }, { total: 0, byKind: {} });
+}
+
+function finding(reference, code, severity, message) {
+ return {
+ referenceId: reference.id,
+ kind: reference.kind,
+ target: reference.target,
+ code,
+ severity,
+ message
+ };
+}
+
+function compareFindings(left, right) {
+ return `${left.referenceId}:${left.code}`.localeCompare(`${right.referenceId}:${right.code}`);
+}
+
+function hasText(value) {
+ return typeof value === 'string' && value.trim().length > 0;
+}
+
+function digestPacket(packet) {
+ return crypto.createHash('sha256').update(stableStringify(packet)).digest('hex');
+}
+
+function stableStringify(value) {
+ if (Array.isArray(value)) return `[${value.map(stableStringify).join(',')}]`;
+ if (value && typeof value === 'object') {
+ return `{${Object.keys(value)
+ .filter((key) => value[key] !== undefined)
+ .sort()
+ .map((key) => `${JSON.stringify(key)}:${stableStringify(value[key])}`)
+ .join(',')}}`;
+ }
+ return JSON.stringify(value);
+}
+
+module.exports = {
+ assessExternalReferences
+};
diff --git a/repository-external-reference-pin-guard/make-demo-video.py b/repository-external-reference-pin-guard/make-demo-video.py
new file mode 100644
index 00000000..4e22c1a0
--- /dev/null
+++ b/repository-external-reference-pin-guard/make-demo-video.py
@@ -0,0 +1,78 @@
+from pathlib import Path
+import subprocess
+import textwrap
+
+from PIL import Image, ImageDraw, ImageFont
+
+
+ROOT = Path(__file__).resolve().parent
+REPORTS = ROOT / "reports"
+REPORTS.mkdir(exist_ok=True)
+FRAME = REPORTS / "demo-frame.png"
+VIDEO = REPORTS / "demo.mp4"
+
+
+def font(size, bold=False):
+ candidates = [
+ "C:/Windows/Fonts/arialbd.ttf" if bold else "C:/Windows/Fonts/arial.ttf",
+ "C:/Windows/Fonts/segoeuib.ttf" if bold else "C:/Windows/Fonts/segoeui.ttf",
+ ]
+ for candidate in candidates:
+ if Path(candidate).exists():
+ return ImageFont.truetype(candidate, size)
+ return ImageFont.load_default()
+
+
+img = Image.new("RGB", (1280, 720), "#ffffff")
+draw = ImageDraw.Draw(img)
+title_font = font(44, True)
+body_font = font(23)
+small_font = font(18)
+
+draw.rectangle((0, 0, 1280, 92), fill="#0f172a")
+draw.text((48, 26), "Repository External Reference Pin Guard", fill="#ffffff", font=title_font)
+
+cards = [
+ ("MALFORMED_REFERENCE_MANIFEST", "#991b1b", "Blocks object-shaped or missing reference manifests before they pass as clean audits"),
+ ("MALFORMED_REFERENCE_ENTRY", "#991b1b", "Turns malformed external-reference entries into release-blocking repair evidence"),
+ ("hold_repository_release", "#991b1b", "Blocks floating git refs, auth-only APIs, stale dataset evidence"),
+ ("stage_reference_metadata_revision", "#a16207", "Stages pinned references that still need license or attribution"),
+ ("release_repository_references", "#047857", "Allows DOI/export release only with immutable pins, parseable DOIs, and checksums"),
+]
+
+for index, (status, color, description) in enumerate(cards):
+ y = 112 + index * 90
+ draw.rounded_rectangle((58, y, 1222, y + 72), radius=8, outline="#cbd5e1", width=2, fill="#f8fafc")
+ draw.ellipse((88, y + 18, 126, y + 56), fill=color)
+ draw.text((150, y + 12), status, fill="#111827", font=body_font)
+ for line_index, line in enumerate(textwrap.wrap(description, width=78)):
+ draw.text((150, y + 42 + line_index * 22), line, fill="#475569", font=small_font)
+
+draw.text((58, 596), "Synthetic evidence only. No external repositories, APIs, DOI registries, or private data sources are contacted.", fill="#334155", font=small_font)
+img.save(FRAME)
+
+subprocess.run(
+ [
+ "ffmpeg",
+ "-y",
+ "-loop",
+ "1",
+ "-i",
+ str(FRAME),
+ "-t",
+ "7.5",
+ "-r",
+ "24",
+ "-vf",
+ "format=yuv420p",
+ "-movflags",
+ "+faststart",
+ str(VIDEO),
+ ],
+ check=True,
+ stdout=subprocess.DEVNULL,
+ stderr=subprocess.DEVNULL,
+)
+
+FRAME.unlink(missing_ok=True)
+print(f"wrote {VIDEO}")
diff --git a/repository-external-reference-pin-guard/package.json b/repository-external-reference-pin-guard/package.json
new file mode 100644
index 00000000..fd485f57
--- /dev/null
+++ b/repository-external-reference-pin-guard/package.json
@@ -0,0 +1,12 @@
+{
+ "name": "repository-external-reference-pin-guard",
+ "version": "1.0.0",
+ "private": true,
+ "type": "commonjs",
+ "scripts": {
+ "test": "node test.js",
+ "demo": "node demo.js",
+ "video": "python make-demo-video.py",
+ "check": "node --check index.js && node --check sample-data.js && node --check test.js && node --check demo.js && python -m py_compile make-demo-video.py"
+ }
+}
diff --git a/repository-external-reference-pin-guard/reports/blocked-packet.json b/repository-external-reference-pin-guard/reports/blocked-packet.json
new file mode 100644
index 00000000..1eae1c9e
--- /dev/null
+++ b/repository-external-reference-pin-guard/reports/blocked-packet.json
@@ -0,0 +1,91 @@
+{
+ "repositoryId": "repo-reference-risk",
+ "status": "hold_repository_release",
+ "releaseLanes": {
+ "doiPublication": "blocked",
+ "exportBundle": "blocked",
+ "apiAccess": "metadata_only"
+ },
+ "findings": [
+ {
+ "referenceId": "api-weather-source",
+ "kind": "api_source",
+ "target": "https://api.example.invalid/weather/latest",
+ "code": "AUTH_REQUIRED_REFERENCE",
+ "severity": "blocker",
+ "message": "Export bundles cannot depend on authenticated external references."
+ },
+ {
+ "referenceId": "api-weather-source",
+ "kind": "api_source",
+ "target": "https://api.example.invalid/weather/latest",
+ "code": "FLOATING_API_REFERENCE",
+ "severity": "blocker",
+ "message": "API source must reference a dated snapshot with checksum evidence."
+ },
+ {
+ "referenceId": "dataset-lab-export",
+ "kind": "linked_dataset",
+ "target": "https://data.example.invalid/lab-export.csv",
+ "code": "INVALID_CHECKSUM_EVIDENCE",
+ "severity": "blocker",
+ "message": "Checksum evidence must use a supported algorithm and full-length hexadecimal digest."
+ },
+ {
+ "referenceId": "dataset-lab-export",
+ "kind": "linked_dataset",
+ "target": "https://data.example.invalid/lab-export.csv",
+ "code": "INVALID_DOI_EVIDENCE",
+ "severity": "blocker",
+ "message": "DOI evidence must be a parseable DOI or DOI URL before release."
+ },
+ {
+ "referenceId": "dataset-lab-export",
+ "kind": "linked_dataset",
+ "target": "https://data.example.invalid/lab-export.csv",
+ "code": "MISSING_DURABLE_IDENTIFIER",
+ "severity": "blocker",
+ "message": "External data or model reference needs a checksum, DOI, or immutable version."
+ },
+ {
+ "referenceId": "dataset-lab-export",
+ "kind": "linked_dataset",
+ "target": "https://data.example.invalid/lab-export.csv",
+ "code": "STALE_REFERENCE_EVIDENCE",
+ "severity": "blocker",
+ "message": "External reference verification is older than policy allows."
+ },
+ {
+ "referenceId": "submodule-analysis-tools",
+ "kind": "git_submodule",
+ "target": "https://github.com/example/analysis-tools",
+ "code": "FLOATING_GIT_REFERENCE",
+ "severity": "blocker",
+ "message": "Git reference must be pinned to an immutable commit SHA before release."
+ }
+ ],
+ "actions": [
+ "add_checksum_or_doi:dataset-lab-export",
+ "pin_external_reference:api-weather-source",
+ "pin_external_reference:submodule-analysis-tools",
+ "refresh_reference_verification:dataset-lab-export",
+ "repair_reference_evidence:dataset-lab-export",
+ "replace_or_snapshot_auth_reference:api-weather-source"
+ ],
+ "referenceSignals": {
+ "immutablePins": false,
+ "exportable": false,
+ "attributionComplete": true,
+ "verificationFresh": false
+ },
+ "referenceSummary": {
+ "total": 3,
+ "byKind": {
+ "git_submodule": 1,
+ "linked_dataset": 1,
+ "api_source": 1
+ }
+ },
+ "assessedAt": "2026-05-28T12:00:00Z",
+ "auditDigest": "0cb0a30094dfe5515aec52b275734cc495e22ef6e44921295fac7d26c6f21a52"
+}
diff --git a/repository-external-reference-pin-guard/reports/clean-packet.json b/repository-external-reference-pin-guard/reports/clean-packet.json
new file mode 100644
index 00000000..452c118b
--- /dev/null
+++ b/repository-external-reference-pin-guard/reports/clean-packet.json
@@ -0,0 +1,29 @@
+{
+ "repositoryId": "repo-reference-clean",
+ "status": "release_repository_references",
+ "releaseLanes": {
+ "doiPublication": "allowed",
+ "exportBundle": "allowed",
+ "apiAccess": "allowed"
+ },
+ "findings": [],
+ "actions": [
+ "release_with_reference_pin_monitoring:repo-reference-clean"
+ ],
+ "referenceSignals": {
+ "immutablePins": true,
+ "exportable": true,
+ "attributionComplete": true,
+ "verificationFresh": true
+ },
+ "referenceSummary": {
+ "total": 3,
+ "byKind": {
+ "git_submodule": 1,
+ "linked_dataset": 1,
+ "api_source": 1
+ }
+ },
+ "assessedAt": "2026-05-28T12:00:00Z",
+ "auditDigest": "5b1ede709bdfe56bb066cce64315055c0e3e87678a2bc86d7d8846ab17ae48a1"
+}
diff --git a/repository-external-reference-pin-guard/reports/demo.mp4 b/repository-external-reference-pin-guard/reports/demo.mp4
new file mode 100644
index 00000000..30f2129d
Binary files /dev/null and b/repository-external-reference-pin-guard/reports/demo.mp4 differ
diff --git a/repository-external-reference-pin-guard/reports/external-reference-report.md b/repository-external-reference-pin-guard/reports/external-reference-report.md
new file mode 100644
index 00000000..0120cde8
--- /dev/null
+++ b/repository-external-reference-pin-guard/reports/external-reference-report.md
@@ -0,0 +1,11 @@
+# Repository External Reference Pin Guard Report
+
+| Packet | Status | DOI publication | Export bundle | API access | Findings |
+| --- | --- | --- | --- | --- | --- |
+| blocked-packet.json | hold_repository_release | blocked | blocked | metadata_only | AUTH_REQUIRED_REFERENCE, FLOATING_API_REFERENCE, INVALID_CHECKSUM_EVIDENCE, INVALID_DOI_EVIDENCE, MISSING_DURABLE_IDENTIFIER, STALE_REFERENCE_EVIDENCE, FLOATING_GIT_REFERENCE |
+| malformed-manifest-packet.json | hold_repository_release | blocked | blocked | metadata_only | MALFORMED_REFERENCE_MANIFEST |
+| malformed-packet.json | hold_repository_release | blocked | blocked | metadata_only | MALFORMED_REFERENCE_ENTRY |
+| clean-packet.json | release_repository_references | allowed | allowed | allowed | none |
+| warning-packet.json | stage_reference_metadata_revision | metadata_revision | draft_only | allowed | MISSING_ATTRIBUTION, MISSING_LICENSE |
+
+Synthetic data only. No external repositories, APIs, DOI registries, or private data sources are contacted.
diff --git a/repository-external-reference-pin-guard/reports/malformed-manifest-packet.json b/repository-external-reference-pin-guard/reports/malformed-manifest-packet.json
new file mode 100644
index 00000000..384be8e7
--- /dev/null
+++ b/repository-external-reference-pin-guard/reports/malformed-manifest-packet.json
@@ -0,0 +1,36 @@
+{
+ "repositoryId": "repo-reference-malformed-manifest",
+ "status": "hold_repository_release",
+ "releaseLanes": {
+ "doiPublication": "blocked",
+ "exportBundle": "blocked",
+ "apiAccess": "metadata_only"
+ },
+ "findings": [
+ {
+ "referenceId": "reference-manifest",
+ "kind": "unknown",
+ "target": null,
+ "code": "MALFORMED_REFERENCE_MANIFEST",
+ "severity": "blocker",
+ "message": "External reference manifest must be an array before release."
+ }
+ ],
+ "actions": [
+ "repair_reference_manifest:reference-manifest"
+ ],
+ "referenceSignals": {
+ "immutablePins": false,
+ "exportable": false,
+ "attributionComplete": false,
+ "verificationFresh": false
+ },
+ "referenceSummary": {
+ "total": 1,
+ "byKind": {
+ "unknown": 1
+ }
+ },
+ "assessedAt": "2026-05-28T12:00:00Z",
+ "auditDigest": "cd76d8b327ffca47048a3b054d92a71799bde2685529573670e07a3362ac5e69"
+}
diff --git a/repository-external-reference-pin-guard/reports/malformed-packet.json b/repository-external-reference-pin-guard/reports/malformed-packet.json
new file mode 100644
index 00000000..3c052d7c
--- /dev/null
+++ b/repository-external-reference-pin-guard/reports/malformed-packet.json
@@ -0,0 +1,36 @@
+{
+ "repositoryId": "repo-reference-malformed-entry",
+ "status": "hold_repository_release",
+ "releaseLanes": {
+ "doiPublication": "blocked",
+ "exportBundle": "blocked",
+ "apiAccess": "metadata_only"
+ },
+ "findings": [
+ {
+ "referenceId": "malformed-reference-entry-1",
+ "kind": "unknown",
+ "target": null,
+ "code": "MALFORMED_REFERENCE_ENTRY",
+ "severity": "blocker",
+ "message": "External reference entry must be a structured object before release."
+ }
+ ],
+ "actions": [
+ "repair_reference_entry:malformed-reference-entry-1"
+ ],
+ "referenceSignals": {
+ "immutablePins": false,
+ "exportable": false,
+ "attributionComplete": false,
+ "verificationFresh": false
+ },
+ "referenceSummary": {
+ "total": 1,
+ "byKind": {
+ "unknown": 1
+ }
+ },
+ "assessedAt": "2026-05-28T12:00:00Z",
+ "auditDigest": "5712c8d01b35d778254650adc9a25bd0b08e9268a8a0a9bfc4d36f8ddafae41a"
+}
diff --git a/repository-external-reference-pin-guard/reports/summary.svg b/repository-external-reference-pin-guard/reports/summary.svg
new file mode 100644
index 00000000..d7715810
--- /dev/null
+++ b/repository-external-reference-pin-guard/reports/summary.svg
@@ -0,0 +1,36 @@
+
diff --git a/repository-external-reference-pin-guard/reports/warning-packet.json b/repository-external-reference-pin-guard/reports/warning-packet.json
new file mode 100644
index 00000000..a707e1c7
--- /dev/null
+++ b/repository-external-reference-pin-guard/reports/warning-packet.json
@@ -0,0 +1,44 @@
+{
+ "repositoryId": "repo-reference-warning",
+ "status": "stage_reference_metadata_revision",
+ "releaseLanes": {
+ "doiPublication": "metadata_revision",
+ "exportBundle": "draft_only",
+ "apiAccess": "allowed"
+ },
+ "findings": [
+ {
+ "referenceId": "model-weights",
+ "kind": "model_weights",
+ "target": "https://models.example.invalid/model-v3.bin",
+ "code": "MISSING_ATTRIBUTION",
+ "severity": "warning",
+ "message": "External reference needs attribution metadata before DOI publication."
+ },
+ {
+ "referenceId": "model-weights",
+ "kind": "model_weights",
+ "target": "https://models.example.invalid/model-v3.bin",
+ "code": "MISSING_LICENSE",
+ "severity": "warning",
+ "message": "External reference needs license metadata before DOI publication."
+ }
+ ],
+ "actions": [
+ "complete_license_attribution:model-weights"
+ ],
+ "referenceSignals": {
+ "immutablePins": true,
+ "exportable": true,
+ "attributionComplete": false,
+ "verificationFresh": true
+ },
+ "referenceSummary": {
+ "total": 1,
+ "byKind": {
+ "model_weights": 1
+ }
+ },
+ "assessedAt": "2026-05-28T12:00:00Z",
+ "auditDigest": "2bcec8202016cc03a70f2a1bd751d8f07518e31cd76900ca9f428e638a168afc"
+}
diff --git a/repository-external-reference-pin-guard/requirements-map.md b/repository-external-reference-pin-guard/requirements-map.md
new file mode 100644
index 00000000..11b12135
--- /dev/null
+++ b/repository-external-reference-pin-guard/requirements-map.md
@@ -0,0 +1,15 @@
+# Requirements Map
+
+Issue #10 asks for robust project repositories with versioned files, collaboration, reproducibility, identifiers, citations, programmatic access, and export bundles.
+
+| Issue #10 area | Coverage in this slice |
+| --- | --- |
+| File and metadata versioning | Holds releases when Git submodules or external code are not pinned to immutable commits, including null all-zero commit placeholders. |
+| Hash-based integrity | Requires full-length SHA checksums, parseable DOI evidence, or non-floating immutable versions for external datasets and model weights, and blocks malformed checksum evidence even when another identifier is present. |
+| Computation-aware reproducibility | Blocks reproducibility/export lanes when API data sources are floating, authenticated only, or backed by malformed/future snapshot dates, invalid checksum evidence, or truncated checksum evidence. |
+| Repository identifiers and citation | Prevents DOI publication when external references lack verification timestamps, are stale, future-dated, contain malformed DOI evidence, or lack license/attribution metadata. |
+| Programmatic access and export | Separates API metadata-only access from export-bundle and DOI publication release lanes, and blocks malformed reference manifests or entries with explicit repair actions. |
+
+## Non-Overlap
+
+This contribution is distinct from broad repository ledgers, release engines, structured diffs, provenance attestations, release embargo controls, notebook replay, schema migration, citation impact, API/export contract verification, merge queue governance, environment drift, access review, DOI tombstone handling, metadata readiness, branch hypothesis lineage, sensitive-artifact scanning, dependency-license checks, legal hold, component-owner approval, restore rehearsal, compute sandbox policy, and semantic version-tag governance. It focuses specifically on immutable external reference pins and exportable citation evidence for submodules, linked datasets, API sources, model weights, and external code/data pointers.
diff --git a/repository-external-reference-pin-guard/sample-data.js b/repository-external-reference-pin-guard/sample-data.js
new file mode 100644
index 00000000..041a8ef7
--- /dev/null
+++ b/repository-external-reference-pin-guard/sample-data.js
@@ -0,0 +1,120 @@
+const riskyRepository = {
+ repositoryId: 'repo-reference-risk',
+ assessedAt: '2026-05-28T12:00:00Z',
+ policy: {
+ maxReferenceAgeDays: 180
+ },
+ references: [
+ {
+ id: 'submodule-analysis-tools',
+ kind: 'git_submodule',
+ target: 'https://github.com/example/analysis-tools',
+ branch: 'main',
+ commitSha: '',
+ license: 'MIT',
+ attribution: 'Example Analysis Tools',
+ lastVerifiedAt: '2026-05-20T08:00:00Z'
+ },
+ {
+ id: 'dataset-lab-export',
+ kind: 'linked_dataset',
+ target: 'https://data.example.invalid/lab-export.csv',
+ checksum: 'pending',
+ doi: 'pending',
+ license: 'CC-BY-4.0',
+ attribution: 'Example Lab',
+ lastVerifiedAt: '2025-01-15T08:00:00Z'
+ },
+ {
+ id: 'api-weather-source',
+ kind: 'api_source',
+ target: 'https://api.example.invalid/weather/latest',
+ snapshotDate: '',
+ checksum: 'sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
+ authRequired: true,
+ license: 'CC0-1.0',
+ attribution: 'Example Weather API',
+ lastVerifiedAt: '2026-05-20T08:00:00Z'
+ }
+ ]
+};
+
+const cleanRepository = {
+ repositoryId: 'repo-reference-clean',
+ assessedAt: '2026-05-28T12:00:00Z',
+ references: [
+ {
+ id: 'submodule-analysis-tools',
+ kind: 'git_submodule',
+ target: 'https://github.com/example/analysis-tools',
+ commitSha: '7f9c2d6c8e0f4b1a2d3c5e6f708192a3b4c5d6e7',
+ license: 'MIT',
+ attribution: 'Example Analysis Tools',
+ lastVerifiedAt: '2026-05-20T08:00:00Z'
+ },
+ {
+ id: 'dataset-lab-export',
+ kind: 'linked_dataset',
+ target: 'https://doi.org/10.5281/zenodo.1234567',
+ checksum: 'sha256:1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef',
+ doi: '10.5281/zenodo.1234567',
+ license: 'CC-BY-4.0',
+ attribution: 'Example Lab',
+ lastVerifiedAt: '2026-05-20T08:00:00Z'
+ },
+ {
+ id: 'api-weather-source',
+ kind: 'api_source',
+ target: 'https://api.example.invalid/weather/snapshots/2026-05-01.json',
+ snapshotDate: '2026-05-01',
+ checksum: 'sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb',
+ authRequired: false,
+ license: 'CC0-1.0',
+ attribution: 'Example Weather API',
+ lastVerifiedAt: '2026-05-20T08:00:00Z'
+ }
+ ]
+};
+
+const warningRepository = {
+ repositoryId: 'repo-reference-warning',
+ assessedAt: '2026-05-28T12:00:00Z',
+ references: [
+ {
+ id: 'model-weights',
+ kind: 'model_weights',
+ target: 'https://models.example.invalid/model-v3.bin',
+ checksum: 'sha256:feedfacefeedfacefeedfacefeedfacefeedfacefeedfacefeedfacefeedface',
+ version: 'v3.0.1',
+ license: '',
+ attribution: '',
+ lastVerifiedAt: '2026-05-20T08:00:00Z'
+ }
+ ]
+};
+
+const malformedRepository = {
+ repositoryId: 'repo-reference-malformed-entry',
+ assessedAt: '2026-05-28T12:00:00Z',
+ references: [null]
+};
+
+const malformedManifestRepository = {
+ repositoryId: 'repo-reference-malformed-manifest',
+ assessedAt: '2026-05-28T12:00:00Z',
+ references: {
+ dataset: {
+ id: 'dataset-not-in-list',
+ kind: 'linked_dataset',
+ target: 'https://doi.org/10.5281/zenodo.4567890'
+ }
+ }
+};
+
+module.exports = {
+ riskyRepository,
+ cleanRepository,
+ warningRepository,
+ malformedRepository,
+ malformedManifestRepository
+};
diff --git a/repository-external-reference-pin-guard/test.js b/repository-external-reference-pin-guard/test.js
new file mode 100644
index 00000000..1405818f
--- /dev/null
+++ b/repository-external-reference-pin-guard/test.js
@@ -0,0 +1,489 @@
+const assert = require('assert');
+
+const { assessExternalReferences } = require('./index');
+
+function findingCodes(packet) {
+ return packet.findings.map((finding) => finding.code).sort();
+}
+
+function testBlocksFloatingAndNonExportableExternalReferences() {
+ const packet = assessExternalReferences({
+ repositoryId: 'repo-reference-risk',
+ assessedAt: '2026-05-28T12:00:00Z',
+ policy: {
+ maxReferenceAgeDays: 180
+ },
+ references: [
+ {
+ id: 'submodule-analysis-tools',
+ kind: 'git_submodule',
+ target: 'https://github.com/example/analysis-tools',
+ branch: 'main',
+ commitSha: '',
+ license: 'MIT',
+ attribution: 'Example Analysis Tools'
+ },
+ {
+ id: 'dataset-lab-export',
+ kind: 'linked_dataset',
+ target: 'https://data.example.invalid/lab-export.csv',
+ checksum: '',
+ doi: '',
+ license: 'CC-BY-4.0',
+ attribution: 'Example Lab',
+ lastVerifiedAt: '2025-01-15T08:00:00Z'
+ },
+ {
+ id: 'api-weather-source',
+ kind: 'api_source',
+ target: 'https://api.example.invalid/weather/latest',
+ snapshotDate: '',
+ checksum: 'sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
+ authRequired: true,
+ license: 'CC0-1.0',
+ attribution: 'Example Weather API'
+ }
+ ]
+ });
+
+ assert.equal(packet.status, 'hold_repository_release');
+ assert.equal(packet.releaseLanes.doiPublication, 'blocked');
+ assert.equal(packet.releaseLanes.exportBundle, 'blocked');
+ assert.deepEqual(findingCodes(packet), [
+ 'AUTH_REQUIRED_REFERENCE',
+ 'FLOATING_API_REFERENCE',
+ 'FLOATING_GIT_REFERENCE',
+ 'MISSING_DURABLE_IDENTIFIER',
+ 'STALE_REFERENCE_EVIDENCE'
+ ]);
+ assert.ok(packet.actions.includes('pin_external_reference:submodule-analysis-tools'));
+ assert.ok(packet.actions.includes('add_checksum_or_doi:dataset-lab-export'));
+ assert.ok(packet.actions.includes('replace_or_snapshot_auth_reference:api-weather-source'));
+ assert.match(packet.auditDigest, /^[a-f0-9]{64}$/);
+}
+
+function testAllowsPinnedExportableReferences() {
+ const packet = assessExternalReferences({
+ repositoryId: 'repo-reference-clean',
+ assessedAt: '2026-05-28T12:00:00Z',
+ references: [
+ {
+ id: 'submodule-analysis-tools',
+ kind: 'git_submodule',
+ target: 'https://github.com/example/analysis-tools',
+ commitSha: '7f9c2d6c8e0f4b1a2d3c5e6f708192a3b4c5d6e7',
+ license: 'MIT',
+ attribution: 'Example Analysis Tools',
+ lastVerifiedAt: '2026-05-20T08:00:00Z'
+ },
+ {
+ id: 'dataset-lab-export',
+ kind: 'linked_dataset',
+ target: 'https://doi.org/10.5281/zenodo.1234567',
+ checksum: 'sha256:1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef',
+ doi: '10.5281/zenodo.1234567',
+ license: 'CC-BY-4.0',
+ attribution: 'Example Lab',
+ lastVerifiedAt: '2026-05-20T08:00:00Z'
+ },
+ {
+ id: 'api-weather-source',
+ kind: 'api_source',
+ target: 'https://api.example.invalid/weather/snapshots/2026-05-01.json',
+ snapshotDate: '2026-05-01',
+ checksum: 'sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb',
+ authRequired: false,
+ license: 'CC0-1.0',
+ attribution: 'Example Weather API',
+ lastVerifiedAt: '2026-05-20T08:00:00Z'
+ }
+ ]
+ });
+
+ assert.equal(packet.status, 'release_repository_references');
+ assert.equal(packet.releaseLanes.doiPublication, 'allowed');
+ assert.equal(packet.releaseLanes.exportBundle, 'allowed');
+ assert.deepEqual(packet.findings, []);
+ assert.deepEqual(packet.actions, ['release_with_reference_pin_monitoring:repo-reference-clean']);
+ assert.equal(packet.referenceSignals.immutablePins, true);
+ assert.equal(packet.referenceSignals.exportable, true);
+}
+
+function testStagesReferencesMissingLicenseAttributionOnly() {
+ const packet = assessExternalReferences({
+ repositoryId: 'repo-reference-warning',
+ assessedAt: '2026-05-28T12:00:00Z',
+ references: [
+ {
+ id: 'model-weights',
+ kind: 'model_weights',
+ target: 'https://models.example.invalid/model-v3.bin',
+ checksum: 'sha256:feedfacefeedfacefeedfacefeedfacefeedfacefeedfacefeedfacefeedface',
+ version: 'v3.0.1',
+ license: '',
+ attribution: '',
+ lastVerifiedAt: '2026-05-20T08:00:00Z'
+ }
+ ]
+ });
+
+ assert.equal(packet.status, 'stage_reference_metadata_revision');
+ assert.equal(packet.releaseLanes.doiPublication, 'metadata_revision');
+ assert.equal(packet.releaseLanes.exportBundle, 'draft_only');
+ assert.deepEqual(findingCodes(packet), [
+ 'MISSING_ATTRIBUTION',
+ 'MISSING_LICENSE'
+ ]);
+ assert.deepEqual(packet.actions, ['complete_license_attribution:model-weights']);
+}
+
+function testFloatingVersionAliasDoesNotCountAsDurableIdentifier() {
+ const packet = assessExternalReferences({
+ repositoryId: 'repo-reference-floating-version',
+ assessedAt: '2026-05-28T12:00:00Z',
+ references: [
+ {
+ id: 'model-weights-latest',
+ kind: 'model_weights',
+ target: 'https://models.example.invalid/model.bin',
+ version: 'latest',
+ checksum: '',
+ doi: '',
+ license: 'Apache-2.0',
+ attribution: 'Example Model Lab',
+ lastVerifiedAt: '2026-05-20T08:00:00Z'
+ }
+ ]
+ });
+
+ assert.equal(packet.status, 'hold_repository_release');
+ assert.deepEqual(findingCodes(packet), ['MISSING_DURABLE_IDENTIFIER']);
+ assert.ok(packet.actions.includes('add_checksum_or_doi:model-weights-latest'));
+ assert.equal(packet.referenceSignals.exportable, false);
+}
+
+function testInvalidChecksumDoesNotCountAsDurableIdentifier() {
+ const packet = assessExternalReferences({
+ repositoryId: 'repo-reference-invalid-checksum',
+ assessedAt: '2026-05-28T12:00:00Z',
+ references: [
+ {
+ id: 'dataset-invalid-checksum',
+ kind: 'linked_dataset',
+ target: 'https://data.example.invalid/lab-export.csv',
+ checksum: 'pending',
+ doi: '',
+ version: '',
+ license: 'CC-BY-4.0',
+ attribution: 'Example Lab',
+ lastVerifiedAt: '2026-05-20T08:00:00Z'
+ }
+ ]
+ });
+
+ assert.equal(packet.status, 'hold_repository_release');
+ assert.deepEqual(findingCodes(packet), [
+ 'INVALID_CHECKSUM_EVIDENCE',
+ 'MISSING_DURABLE_IDENTIFIER'
+ ]);
+ assert.ok(packet.actions.includes('add_checksum_or_doi:dataset-invalid-checksum'));
+ assert.ok(packet.actions.includes('repair_reference_evidence:dataset-invalid-checksum'));
+ assert.equal(packet.referenceSignals.exportable, false);
+}
+
+function testDoiPlaceholderDoesNotCountAsDurableIdentifier() {
+ const packet = assessExternalReferences({
+ repositoryId: 'repo-reference-invalid-doi',
+ assessedAt: '2026-05-28T12:00:00Z',
+ references: [
+ {
+ id: 'dataset-invalid-doi',
+ kind: 'linked_dataset',
+ target: 'https://data.example.invalid/lab-export.csv',
+ checksum: '',
+ doi: 'pending',
+ version: '',
+ license: 'CC-BY-4.0',
+ attribution: 'Example Lab',
+ lastVerifiedAt: '2026-05-20T08:00:00Z'
+ }
+ ]
+ });
+
+ assert.equal(packet.status, 'hold_repository_release');
+ assert.deepEqual(findingCodes(packet), [
+ 'INVALID_DOI_EVIDENCE',
+ 'MISSING_DURABLE_IDENTIFIER'
+ ]);
+ assert.ok(packet.actions.includes('add_checksum_or_doi:dataset-invalid-doi'));
+ assert.ok(packet.actions.includes('repair_reference_evidence:dataset-invalid-doi'));
+ assert.equal(packet.referenceSignals.exportable, false);
+}
+
+function testFutureDatedVerificationEvidenceIsNotFresh() {
+ const packet = assessExternalReferences({
+ repositoryId: 'repo-reference-future-verification',
+ assessedAt: '2026-05-28T12:00:00Z',
+ references: [
+ {
+ id: 'dataset-future-verified',
+ kind: 'linked_dataset',
+ target: 'https://doi.org/10.5281/zenodo.7654321',
+ checksum: 'sha256:feedfacefeedfacefeedfacefeedfacefeedfacefeedfacefeedfacefeedface',
+ doi: '10.5281/zenodo.7654321',
+ license: 'CC-BY-4.0',
+ attribution: 'Example Lab',
+ lastVerifiedAt: '2026-05-29T12:00:00Z'
+ }
+ ]
+ });
+
+ assert.equal(packet.status, 'hold_repository_release');
+ assert.deepEqual(findingCodes(packet), ['STALE_REFERENCE_EVIDENCE']);
+ assert.ok(packet.actions.includes('refresh_reference_verification:dataset-future-verified'));
+ assert.equal(packet.referenceSignals.verificationFresh, false);
+}
+
+function testFutureDatedApiSnapshotDoesNotCountAsPinnedEvidence() {
+ const packet = assessExternalReferences({
+ repositoryId: 'repo-reference-future-api-snapshot',
+ assessedAt: '2026-05-28T12:00:00Z',
+ references: [
+ {
+ id: 'api-future-snapshot',
+ kind: 'api_source',
+ target: 'https://api.example.invalid/weather/snapshots/2026-06-01.json',
+ snapshotDate: '2026-06-01',
+ checksum: 'sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc',
+ authRequired: false,
+ license: 'CC0-1.0',
+ attribution: 'Example Weather API',
+ lastVerifiedAt: '2026-05-20T08:00:00Z'
+ }
+ ]
+ });
+
+ assert.equal(packet.status, 'hold_repository_release');
+ assert.deepEqual(findingCodes(packet), ['FLOATING_API_REFERENCE']);
+ assert.ok(packet.actions.includes('pin_external_reference:api-future-snapshot'));
+ assert.equal(packet.referenceSignals.immutablePins, false);
+}
+
+function testInvalidApiSnapshotChecksumDoesNotCountAsPinnedEvidence() {
+ const packet = assessExternalReferences({
+ repositoryId: 'repo-reference-invalid-api-checksum',
+ assessedAt: '2026-05-28T12:00:00Z',
+ references: [
+ {
+ id: 'api-invalid-checksum',
+ kind: 'api_source',
+ target: 'https://api.example.invalid/weather/snapshots/2026-05-01.json',
+ snapshotDate: '2026-05-01',
+ checksum: 'pending',
+ authRequired: false,
+ license: 'CC0-1.0',
+ attribution: 'Example Weather API',
+ lastVerifiedAt: '2026-05-20T08:00:00Z'
+ }
+ ]
+ });
+
+ assert.equal(packet.status, 'hold_repository_release');
+ assert.deepEqual(findingCodes(packet), [
+ 'FLOATING_API_REFERENCE',
+ 'INVALID_CHECKSUM_EVIDENCE'
+ ]);
+ assert.ok(packet.actions.includes('pin_external_reference:api-invalid-checksum'));
+ assert.ok(packet.actions.includes('repair_reference_evidence:api-invalid-checksum'));
+ assert.equal(packet.referenceSignals.immutablePins, false);
+}
+
+function testTruncatedApiSnapshotChecksumDoesNotCountAsPinnedEvidence() {
+ const packet = assessExternalReferences({
+ repositoryId: 'repo-reference-truncated-api-checksum',
+ assessedAt: '2026-05-28T12:00:00Z',
+ references: [
+ {
+ id: 'api-truncated-checksum',
+ kind: 'api_source',
+ target: 'https://api.example.invalid/weather/snapshots/2026-05-01.json',
+ snapshotDate: '2026-05-01',
+ checksum: 'sha256:abcdef',
+ authRequired: false,
+ license: 'CC0-1.0',
+ attribution: 'Example Weather API',
+ lastVerifiedAt: '2026-05-20T08:00:00Z'
+ }
+ ]
+ });
+
+ assert.equal(packet.status, 'hold_repository_release');
+ assert.deepEqual(findingCodes(packet), [
+ 'FLOATING_API_REFERENCE',
+ 'INVALID_CHECKSUM_EVIDENCE'
+ ]);
+ assert.ok(packet.actions.includes('pin_external_reference:api-truncated-checksum'));
+ assert.ok(packet.actions.includes('repair_reference_evidence:api-truncated-checksum'));
+ assert.equal(packet.referenceSignals.immutablePins, false);
+}
+
+function testNullGitCommitShaDoesNotCountAsImmutablePin() {
+ const packet = assessExternalReferences({
+ repositoryId: 'repo-reference-null-git-pin',
+ assessedAt: '2026-05-28T12:00:00Z',
+ references: [
+ {
+ id: 'submodule-null-sha',
+ kind: 'git_submodule',
+ target: 'https://github.com/example/analysis-tools',
+ commitSha: '0000000000000000000000000000000000000000',
+ license: 'MIT',
+ attribution: 'Example Analysis Tools',
+ lastVerifiedAt: '2026-05-20T08:00:00Z'
+ }
+ ]
+ });
+
+ assert.equal(packet.status, 'hold_repository_release');
+ assert.deepEqual(findingCodes(packet), ['FLOATING_GIT_REFERENCE']);
+ assert.ok(packet.actions.includes('pin_external_reference:submodule-null-sha'));
+ assert.equal(packet.referenceSignals.immutablePins, false);
+}
+
+function testMissingVerificationEvidenceBlocksOtherwisePinnedReference() {
+ const packet = assessExternalReferences({
+ repositoryId: 'repo-reference-missing-verification',
+ assessedAt: '2026-05-28T12:00:00Z',
+ references: [
+ {
+ id: 'dataset-needs-verification',
+ kind: 'linked_dataset',
+ target: 'https://doi.org/10.5281/zenodo.2345678',
+ checksum: 'sha256:dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd',
+ doi: '10.5281/zenodo.2345678',
+ license: 'CC-BY-4.0',
+ attribution: 'Example Lab'
+ }
+ ]
+ });
+
+ assert.equal(packet.status, 'hold_repository_release');
+ assert.deepEqual(findingCodes(packet), ['STALE_REFERENCE_EVIDENCE']);
+ assert.ok(packet.actions.includes('refresh_reference_verification:dataset-needs-verification'));
+ assert.equal(packet.referenceSignals.verificationFresh, false);
+}
+
+function testMalformedOptionalEvidenceBlocksEvenWhenAnotherIdentifierIsValid() {
+ const packet = assessExternalReferences({
+ repositoryId: 'repo-reference-poisoned-evidence',
+ assessedAt: '2026-05-28T12:00:00Z',
+ references: [
+ {
+ id: 'dataset-poisoned-evidence',
+ kind: 'linked_dataset',
+ target: 'https://doi.org/10.5281/zenodo.3456789',
+ checksum: 'sha256:abcdef',
+ doi: '10.5281/zenodo.3456789',
+ license: 'CC-BY-4.0',
+ attribution: 'Example Lab',
+ lastVerifiedAt: '2026-05-20T08:00:00Z'
+ },
+ {
+ id: 'model-poisoned-citation',
+ kind: 'model_weights',
+ target: 'https://models.example.invalid/model-v4.bin',
+ checksum: 'sha256:eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee',
+ doi: 'pending',
+ version: 'v4.0.0',
+ license: 'Apache-2.0',
+ attribution: 'Example Model Lab',
+ lastVerifiedAt: '2026-05-20T08:00:00Z'
+ }
+ ]
+ });
+
+ assert.equal(packet.status, 'hold_repository_release');
+ assert.deepEqual(findingCodes(packet), [
+ 'INVALID_CHECKSUM_EVIDENCE',
+ 'INVALID_DOI_EVIDENCE'
+ ]);
+ assert.ok(packet.actions.includes('repair_reference_evidence:dataset-poisoned-evidence'));
+ assert.ok(packet.actions.includes('repair_reference_evidence:model-poisoned-citation'));
+ assert.equal(packet.referenceSignals.exportable, false);
+}
+
+function testMalformedReferenceEntriesBlockReleaseInsteadOfCrashing() {
+ const packet = assessExternalReferences({
+ repositoryId: 'repo-reference-malformed-entry',
+ assessedAt: '2026-05-28T12:00:00Z',
+ references: [null]
+ });
+
+ assert.equal(packet.status, 'hold_repository_release');
+ assert.deepEqual(findingCodes(packet), ['MALFORMED_REFERENCE_ENTRY']);
+ assert.equal(packet.findings[0].referenceId, 'malformed-reference-entry-1');
+ assert.ok(packet.actions.includes('repair_reference_entry:malformed-reference-entry-1'));
+ assert.equal(packet.referenceSignals.immutablePins, false);
+ assert.equal(packet.referenceSignals.exportable, false);
+ assert.equal(packet.referenceSignals.attributionComplete, false);
+ assert.equal(packet.referenceSignals.verificationFresh, false);
+ assert.deepEqual(packet.referenceSummary, {
+ total: 1,
+ byKind: {
+ unknown: 1
+ }
+ });
+}
+
+function testMalformedReferenceManifestBlocksRelease() {
+ const packet = assessExternalReferences({
+ repositoryId: 'repo-reference-malformed-manifest',
+ assessedAt: '2026-05-28T12:00:00Z',
+ references: {
+ dataset: {
+ id: 'dataset-not-in-list',
+ kind: 'linked_dataset',
+ target: 'https://doi.org/10.5281/zenodo.4567890'
+ }
+ }
+ });
+
+ assert.equal(packet.status, 'hold_repository_release');
+ assert.deepEqual(findingCodes(packet), ['MALFORMED_REFERENCE_MANIFEST']);
+ assert.equal(packet.findings[0].referenceId, 'reference-manifest');
+ assert.ok(packet.actions.includes('repair_reference_manifest:reference-manifest'));
+ assert.equal(packet.referenceSignals.immutablePins, false);
+ assert.equal(packet.referenceSignals.exportable, false);
+ assert.equal(packet.referenceSignals.attributionComplete, false);
+ assert.equal(packet.referenceSignals.verificationFresh, false);
+ assert.deepEqual(packet.referenceSummary, {
+ total: 1,
+ byKind: {
+ unknown: 1
+ }
+ });
+}
+
+const tests = [
+ testBlocksFloatingAndNonExportableExternalReferences,
+ testAllowsPinnedExportableReferences,
+ testStagesReferencesMissingLicenseAttributionOnly,
+ testFloatingVersionAliasDoesNotCountAsDurableIdentifier,
+ testInvalidChecksumDoesNotCountAsDurableIdentifier,
+ testDoiPlaceholderDoesNotCountAsDurableIdentifier,
+ testFutureDatedVerificationEvidenceIsNotFresh,
+ testFutureDatedApiSnapshotDoesNotCountAsPinnedEvidence,
+ testInvalidApiSnapshotChecksumDoesNotCountAsPinnedEvidence,
+ testTruncatedApiSnapshotChecksumDoesNotCountAsPinnedEvidence,
+ testNullGitCommitShaDoesNotCountAsImmutablePin,
+ testMissingVerificationEvidenceBlocksOtherwisePinnedReference,
+ testMalformedOptionalEvidenceBlocksEvenWhenAnotherIdentifierIsValid,
+ testMalformedReferenceEntriesBlockReleaseInsteadOfCrashing,
+ testMalformedReferenceManifestBlocksRelease
+];
+
+for (const test of tests) {
+ test();
+}
+
+console.log(`repository-external-reference-pin-guard tests passed (${tests.length})`);