diff --git a/repository-access-review-gate/README.md b/repository-access-review-gate/README.md
new file mode 100644
index 0000000..838c558
--- /dev/null
+++ b/repository-access-review-gate/README.md
@@ -0,0 +1,23 @@
+# Repository Access Review Gate
+
+This self-contained slice adds a release-time access review for scientific project repositories.
+It checks whether collaborators, reviewer links, restricted datasets, and artifact licenses are safe before a tagged repository export is made public.
+
+The module is intentionally dependency-free and uses synthetic data only.
+
+## What it produces
+
+- `releaseStatus`: `ready` or `hold`.
+- `releaseHolds`: deterministic blockers for stale external reviewers, expired data-use approvals, privileged role drift, secret-link exposure, and license/access mismatch.
+- `accessMatrix`: per-collaborator keep/revoke/downgrade decisions.
+- `reviewerChecklist`: action-ready remediation items for repository owners and data stewards.
+- `auditDigest`: SHA-256 digest over the packet for reviewer evidence.
+
+## Run locally
+
+```bash
+node repository-access-review-gate/test.js
+node repository-access-review-gate/demo.js
+```
+
+The demo writes JSON, Markdown, SVG, and MP4 artifacts to `repository-access-review-gate/demo-output/`.
diff --git a/repository-access-review-gate/acceptance-notes.md b/repository-access-review-gate/acceptance-notes.md
new file mode 100644
index 0000000..9297657
--- /dev/null
+++ b/repository-access-review-gate/acceptance-notes.md
@@ -0,0 +1,7 @@
+# Acceptance Notes
+
+- The module stays self-contained under `repository-access-review-gate/`.
+- No credentials, network calls, or live user data are used.
+- Tests cover both a blocked release and a ready release with a warning-only temporary access case.
+- Demo artifacts include reviewer-friendly JSON, Markdown, SVG, and a short MP4.
+- The implementation is intentionally separate from prior issue #10 submissions such as repository ledgers, release engines, schema migration, merge queue governance, and environment drift gates.
diff --git a/repository-access-review-gate/demo-output/demo.mp4 b/repository-access-review-gate/demo-output/demo.mp4
new file mode 100644
index 0000000..e6fcdf3
Binary files /dev/null and b/repository-access-review-gate/demo-output/demo.mp4 differ
diff --git a/repository-access-review-gate/demo-output/demo.svg b/repository-access-review-gate/demo-output/demo.svg
new file mode 100644
index 0000000..c9a517d
--- /dev/null
+++ b/repository-access-review-gate/demo-output/demo.svg
@@ -0,0 +1,13 @@
+
diff --git a/repository-access-review-gate/demo-output/release-hold-packet.json b/repository-access-review-gate/demo-output/release-hold-packet.json
new file mode 100644
index 0000000..87c815f
--- /dev/null
+++ b/repository-access-review-gate/demo-output/release-hold-packet.json
@@ -0,0 +1,107 @@
+{
+ "packetType": "repository-access-review-gate",
+ "repositoryId": "repo-sci-artery-flow-042",
+ "releaseTag": "v2.1.0",
+ "releaseStatus": "hold",
+ "generatedAt": "2026-05-20T12:00:00Z",
+ "releaseHolds": [
+ {
+ "code": "external_reviewer_expired",
+ "severity": "blocker",
+ "owner": "u-ext-17",
+ "evidence": "External Reviewer 17 review access expired on 2026-05-01T00:00:00Z",
+ "requiredAction": "Revoke reviewer access or issue a fresh time-boxed invite before release."
+ },
+ {
+ "code": "license_access_mismatch",
+ "severity": "blocker",
+ "owner": "data-steward",
+ "evidence": "data/restricted-waveforms.parquet is restricted-dua but v2.1.0 targets public export.",
+ "requiredAction": "Block public export or replace the artifact with a license-compatible derivative."
+ },
+ {
+ "code": "privileged_role_without_release_scope",
+ "severity": "blocker",
+ "owner": "u-admin-2",
+ "evidence": "Samir Dev is admin but is not approved for v2.1.0.",
+ "requiredAction": "Downgrade the role or record release-scope approval from a repository owner."
+ },
+ {
+ "code": "restricted_dataset_approval_expired",
+ "severity": "blocker",
+ "owner": "u-ext-17",
+ "evidence": "External Reviewer 17 has restricted-data download access with an expired DUA approval.",
+ "requiredAction": "Remove restricted-data access until the data-use approval is renewed."
+ },
+ {
+ "code": "secret_link_exposure",
+ "severity": "blocker",
+ "owner": "security-review",
+ "evidence": "1 artifact link(s) expose secret-bearing URLs in the release packet.",
+ "requiredAction": "Replace secret links with scoped repository object grants before export."
+ }
+ ],
+ "reviewerChecklist": [
+ {
+ "owner": "u-ext-17",
+ "requiredAction": "Revoke reviewer access or issue a fresh time-boxed invite before release.",
+ "evidence": "External Reviewer 17 review access expired on 2026-05-01T00:00:00Z"
+ },
+ {
+ "owner": "data-steward",
+ "requiredAction": "Block public export or replace the artifact with a license-compatible derivative.",
+ "evidence": "data/restricted-waveforms.parquet is restricted-dua but v2.1.0 targets public export."
+ },
+ {
+ "owner": "u-admin-2",
+ "requiredAction": "Downgrade the role or record release-scope approval from a repository owner.",
+ "evidence": "Samir Dev is admin but is not approved for v2.1.0."
+ },
+ {
+ "owner": "u-ext-17",
+ "requiredAction": "Remove restricted-data access until the data-use approval is renewed.",
+ "evidence": "External Reviewer 17 has restricted-data download access with an expired DUA approval."
+ },
+ {
+ "owner": "security-review",
+ "requiredAction": "Replace secret links with scoped repository object grants before export.",
+ "evidence": "1 artifact link(s) expose secret-bearing URLs in the release packet."
+ }
+ ],
+ "accessMatrix": [
+ {
+ "userId": "u-owner-1",
+ "name": "Dr. Amina Park",
+ "role": "owner",
+ "scope": "repository-admin",
+ "releaseApproved": true,
+ "decision": "keep"
+ },
+ {
+ "userId": "u-admin-2",
+ "name": "Samir Dev",
+ "role": "admin",
+ "scope": "all-components",
+ "releaseApproved": false,
+ "decision": "downgrade_before_release"
+ },
+ {
+ "userId": "u-ext-17",
+ "name": "External Reviewer 17",
+ "role": "reviewer",
+ "scope": "manuscript,data",
+ "releaseApproved": true,
+ "decision": "revoke_before_release"
+ },
+ {
+ "userId": "u-analyst-4",
+ "name": "Mina Analyst",
+ "role": "maintainer",
+ "scope": "code,results",
+ "releaseApproved": true,
+ "decision": "keep"
+ }
+ ],
+ "warnings": [],
+ "auditDigest": "8f8ae3250c816f296ba59f67236e29f9ccd8ae375092da4c7dc8dd59c5da9ff3"
+}
diff --git a/repository-access-review-gate/demo-output/reviewer-packet.md b/repository-access-review-gate/demo-output/reviewer-packet.md
new file mode 100644
index 0000000..785de8e
--- /dev/null
+++ b/repository-access-review-gate/demo-output/reviewer-packet.md
@@ -0,0 +1,15 @@
+# Repository access review gate demo
+
+Repository: repo-sci-artery-flow-042
+Release: v2.1.0
+Status: hold
+Blocking holds: 5
+
+## Required reviewer actions
+- u-ext-17: Revoke reviewer access or issue a fresh time-boxed invite before release.
+- data-steward: Block public export or replace the artifact with a license-compatible derivative.
+- u-admin-2: Downgrade the role or record release-scope approval from a repository owner.
+- u-ext-17: Remove restricted-data access until the data-use approval is renewed.
+- security-review: Replace secret links with scoped repository object grants before export.
+
+Audit digest: 8f8ae3250c816f296ba59f67236e29f9ccd8ae375092da4c7dc8dd59c5da9ff3
diff --git a/repository-access-review-gate/demo.js b/repository-access-review-gate/demo.js
new file mode 100644
index 0000000..7c10f8d
--- /dev/null
+++ b/repository-access-review-gate/demo.js
@@ -0,0 +1,81 @@
+const fs = require('fs');
+const path = require('path');
+const { execFileSync } = require('child_process');
+const { buildReleaseHoldPacket } = require('./index');
+const { sampleRepository } = require('./sample-data');
+
+const outputDir = path.join(__dirname, 'demo-output');
+fs.mkdirSync(outputDir, { recursive: true });
+
+const packet = buildReleaseHoldPacket(sampleRepository, {
+ asOf: '2026-05-20T12:00:00Z',
+ releaseTag: 'v2.1.0',
+});
+
+fs.writeFileSync(path.join(outputDir, 'release-hold-packet.json'), `${JSON.stringify(packet, null, 2)}\n`);
+
+const svg = `
+`;
+fs.writeFileSync(path.join(outputDir, 'demo.svg'), svg);
+
+const markdown = [
+ '# Repository access review gate demo',
+ '',
+ `Repository: ${packet.repositoryId}`,
+ `Release: ${packet.releaseTag}`,
+ `Status: ${packet.releaseStatus}`,
+ `Blocking holds: ${packet.releaseHolds.length}`,
+ '',
+ '## Required reviewer actions',
+ ...packet.reviewerChecklist.map((item) => `- ${item.owner}: ${item.requiredAction}`),
+ '',
+ `Audit digest: ${packet.auditDigest}`,
+ '',
+].join('\n');
+fs.writeFileSync(path.join(outputDir, 'reviewer-packet.md'), markdown);
+
+function renderMp4() {
+ const videoPath = path.join(outputDir, 'demo.mp4');
+ const font = 'C\\:/Windows/Fonts/arial.ttf';
+ const text = (value) => String(value).replace(/\\/g, '\\\\').replace(/:/g, '\\:').replace(/'/g, "\\'");
+ const filters = [
+ `drawtext=fontfile='${font}':text='${text('Repository Access Review Gate')}':x=70:y=80:fontsize=44:fontcolor=white`,
+ `drawtext=fontfile='${font}':text='${text(`${packet.releaseHolds.length} blocking release holds detected`)}':x=70:y=160:fontsize=34:fontcolor=0xffd166`,
+ ...packet.releaseHolds.slice(0, 5).map((hold, index) =>
+ `drawtext=fontfile='${font}':text='${text(`${index + 1}. ${hold.code} -> ${hold.owner}`)}':x=90:y=${240 + index * 54}:fontsize=27:fontcolor=white`,
+ ),
+ `drawtext=fontfile='${font}':text='${text(`audit ${packet.auditDigest.slice(0, 20)}...`)}':x=70:y=630:fontsize=24:fontcolor=0x93c5fd`,
+ ].join(',');
+
+ execFileSync('ffmpeg', [
+ '-y',
+ '-f',
+ 'lavfi',
+ '-i',
+ 'color=c=0x111827:s=1280x720:d=7',
+ '-vf',
+ filters,
+ '-c:v',
+ 'libx264',
+ '-pix_fmt',
+ 'yuv420p',
+ videoPath,
+ ], { stdio: 'inherit' });
+}
+
+renderMp4();
+
+console.log(`Wrote demo artifacts to ${outputDir}`);
diff --git a/repository-access-review-gate/index.js b/repository-access-review-gate/index.js
new file mode 100644
index 0000000..2c26fb0
--- /dev/null
+++ b/repository-access-review-gate/index.js
@@ -0,0 +1,212 @@
+const crypto = require('crypto');
+
+function stableStringify(value) {
+ if (Array.isArray(value)) {
+ return `[${value.map(stableStringify).join(',')}]`;
+ }
+ if (value && typeof value === 'object') {
+ return `{${Object.keys(value)
+ .sort()
+ .map((key) => `${JSON.stringify(key)}:${stableStringify(value[key])}`)
+ .join(',')}}`;
+ }
+ return JSON.stringify(value);
+}
+
+function digest(value) {
+ return crypto.createHash('sha256').update(stableStringify(value)).digest('hex');
+}
+
+function toTime(value) {
+ return new Date(value).getTime();
+}
+
+function daysUntil(value, asOf) {
+ return Math.ceil((toTime(value) - toTime(asOf)) / 86_400_000);
+}
+
+function addHold(holds, code, owner, evidence, requiredAction) {
+ holds.push({
+ code,
+ severity: 'blocker',
+ owner,
+ evidence,
+ requiredAction,
+ });
+}
+
+function addWarning(warnings, code, owner, evidence, requiredAction) {
+ warnings.push({
+ code,
+ severity: 'warning',
+ owner,
+ evidence,
+ requiredAction,
+ });
+}
+
+function evaluateCollaborator(collaborator, repository, options) {
+ const holds = [];
+ const warnings = [];
+ const decisions = new Set();
+ const asOf = options.asOf;
+
+ const isExternalReviewer = collaborator.affiliation === 'external' && collaborator.role === 'reviewer';
+ if (isExternalReviewer && collaborator.access.expiresAt && toTime(collaborator.access.expiresAt) < toTime(asOf)) {
+ addHold(
+ holds,
+ 'external_reviewer_expired',
+ collaborator.userId,
+ `${collaborator.name} review access expired on ${collaborator.access.expiresAt}`,
+ 'Revoke reviewer access or issue a fresh time-boxed invite before release.',
+ );
+ decisions.add('revoke_before_release');
+ }
+
+ if (
+ collaborator.access.canDownloadRestrictedData &&
+ collaborator.access.dataUseApprovalExpiresAt &&
+ toTime(collaborator.access.dataUseApprovalExpiresAt) < toTime(asOf)
+ ) {
+ addHold(
+ holds,
+ 'restricted_dataset_approval_expired',
+ collaborator.userId,
+ `${collaborator.name} has restricted-data download access with an expired DUA approval.`,
+ 'Remove restricted-data access until the data-use approval is renewed.',
+ );
+ decisions.add('revoke_before_release');
+ }
+
+ const privileged = ['owner', 'admin', 'maintainer'].includes(collaborator.role);
+ if (privileged && !collaborator.access.releaseApproved) {
+ addHold(
+ holds,
+ 'privileged_role_without_release_scope',
+ collaborator.userId,
+ `${collaborator.name} is ${collaborator.role} but is not approved for ${options.releaseTag}.`,
+ 'Downgrade the role or record release-scope approval from a repository owner.',
+ );
+ decisions.add('downgrade_before_release');
+ }
+
+ if (
+ collaborator.access.expiresAt &&
+ toTime(collaborator.access.expiresAt) >= toTime(asOf) &&
+ daysUntil(collaborator.access.expiresAt, asOf) <= repository.policy.nearExpiryWarningDays
+ ) {
+ addWarning(
+ warnings,
+ 'temporary_access_near_expiry',
+ collaborator.userId,
+ `${collaborator.name} access expires in ${daysUntil(collaborator.access.expiresAt, asOf)} days.`,
+ 'Confirm whether temporary access should survive the release review window.',
+ );
+ }
+
+ return {
+ holds,
+ warnings,
+ accessDecision: {
+ userId: collaborator.userId,
+ name: collaborator.name,
+ role: collaborator.role,
+ scope: collaborator.access.scope,
+ releaseApproved: Boolean(collaborator.access.releaseApproved),
+ decision: decisions.has('revoke_before_release')
+ ? 'revoke_before_release'
+ : decisions.has('downgrade_before_release')
+ ? 'downgrade_before_release'
+ : 'keep',
+ },
+ };
+}
+
+function evaluateRepositoryAccessReview(repository, options = {}) {
+ const reviewOptions = {
+ asOf: options.asOf || new Date().toISOString(),
+ releaseTag: options.releaseTag || repository.release.tag,
+ };
+ const holds = [];
+ const warnings = [];
+ const accessMatrix = [];
+
+ for (const collaborator of repository.collaborators) {
+ const collaboratorReview = evaluateCollaborator(collaborator, repository, reviewOptions);
+ holds.push(...collaboratorReview.holds);
+ warnings.push(...collaboratorReview.warnings);
+ accessMatrix.push(collaboratorReview.accessDecision);
+ }
+
+ const exposedLinks = repository.artifacts.filter((artifact) => artifact.secretLink);
+ if (exposedLinks.length > 0) {
+ addHold(
+ holds,
+ 'secret_link_exposure',
+ 'security-review',
+ `${exposedLinks.length} artifact link(s) expose secret-bearing URLs in the release packet.`,
+ 'Replace secret links with scoped repository object grants before export.',
+ );
+ }
+
+ const licenseMismatch = repository.artifacts.find(
+ (artifact) => artifact.license === 'restricted-dua' && repository.release.visibility === 'public',
+ );
+ if (licenseMismatch) {
+ addHold(
+ holds,
+ 'license_access_mismatch',
+ licenseMismatch.owner,
+ `${licenseMismatch.path} is restricted-dua but ${repository.release.tag} targets public export.`,
+ 'Block public export or replace the artifact with a license-compatible derivative.',
+ );
+ }
+
+ const sortedHolds = holds.sort((left, right) => left.code.localeCompare(right.code));
+ const sortedWarnings = warnings.sort((left, right) => left.code.localeCompare(right.code));
+
+ return {
+ repositoryId: repository.repositoryId,
+ releaseTag: reviewOptions.releaseTag,
+ releaseStatus: sortedHolds.length > 0 ? 'hold' : 'ready',
+ summary: {
+ collaboratorsReviewed: repository.collaborators.length,
+ artifactsReviewed: repository.artifacts.length,
+ blockingHolds: sortedHolds.length,
+ warningCount: sortedWarnings.length,
+ },
+ holds: sortedHolds,
+ warnings: sortedWarnings,
+ accessMatrix,
+ };
+}
+
+function buildReleaseHoldPacket(repository, options = {}) {
+ const review = evaluateRepositoryAccessReview(repository, options);
+ const packet = {
+ packetType: 'repository-access-review-gate',
+ repositoryId: review.repositoryId,
+ releaseTag: review.releaseTag,
+ releaseStatus: review.releaseStatus,
+ generatedAt: options.asOf || new Date().toISOString(),
+ releaseHolds: review.holds,
+ reviewerChecklist: review.holds.map((hold) => ({
+ owner: hold.owner,
+ requiredAction: hold.requiredAction,
+ evidence: hold.evidence,
+ })),
+ accessMatrix: review.accessMatrix,
+ warnings: review.warnings,
+ };
+ return {
+ ...packet,
+ auditDigest: digest(packet),
+ };
+}
+
+module.exports = {
+ evaluateRepositoryAccessReview,
+ buildReleaseHoldPacket,
+ stableStringify,
+ digest,
+};
diff --git a/repository-access-review-gate/requirements-map.md b/repository-access-review-gate/requirements-map.md
new file mode 100644
index 0000000..32477b7
--- /dev/null
+++ b/repository-access-review-gate/requirements-map.md
@@ -0,0 +1,12 @@
+# Requirements Map
+
+Issue #10 asks for project repositories that combine collaboration, publication, reproducibility, versioning, provenance, access, and export workflows.
+
+| Requirement area | Coverage in this slice |
+| --- | --- |
+| Collaboration and review | Builds a collaborator access matrix for owners, admins, maintainers, and external reviewers. |
+| Provenance and contribution trust | Records release-scope approval, owner attribution, and deterministic audit digests. |
+| Data and artifact governance | Blocks expired restricted-data approvals and secret-bearing artifact links. |
+| Versioned release workflow | Evaluates a named release tag before public export. |
+| Programmatic access/export safety | Produces a release hold packet that can gate API/export flows. |
+| Reproducibility and archival trust | Preserves license/access evidence so restricted datasets are not accidentally exported as public release assets. |
diff --git a/repository-access-review-gate/sample-data.js b/repository-access-review-gate/sample-data.js
new file mode 100644
index 0000000..1deee1b
--- /dev/null
+++ b/repository-access-review-gate/sample-data.js
@@ -0,0 +1,128 @@
+const sampleRepository = {
+ repositoryId: 'repo-sci-artery-flow-042',
+ title: 'Artery Flow Digital Twin Release',
+ policy: {
+ nearExpiryWarningDays: 14,
+ },
+ release: {
+ tag: 'v2.1.0',
+ visibility: 'public',
+ },
+ collaborators: [
+ {
+ userId: 'u-owner-1',
+ name: 'Dr. Amina Park',
+ role: 'owner',
+ affiliation: 'internal',
+ access: {
+ scope: 'repository-admin',
+ releaseApproved: true,
+ },
+ },
+ {
+ userId: 'u-admin-2',
+ name: 'Samir Dev',
+ role: 'admin',
+ affiliation: 'internal',
+ access: {
+ scope: 'all-components',
+ releaseApproved: false,
+ },
+ },
+ {
+ userId: 'u-ext-17',
+ name: 'External Reviewer 17',
+ role: 'reviewer',
+ affiliation: 'external',
+ access: {
+ scope: 'manuscript,data',
+ expiresAt: '2026-05-01T00:00:00Z',
+ releaseApproved: true,
+ canDownloadRestrictedData: true,
+ dataUseApprovalExpiresAt: '2026-05-05T00:00:00Z',
+ },
+ },
+ {
+ userId: 'u-analyst-4',
+ name: 'Mina Analyst',
+ role: 'maintainer',
+ affiliation: 'internal',
+ access: {
+ scope: 'code,results',
+ releaseApproved: true,
+ },
+ },
+ ],
+ artifacts: [
+ {
+ artifactId: 'data-waveforms',
+ path: 'data/restricted-waveforms.parquet',
+ owner: 'data-steward',
+ license: 'restricted-dua',
+ secretLink: false,
+ },
+ {
+ artifactId: 'supplement-secret',
+ path: 'results/supplemental-viewer.url',
+ owner: 'security-review',
+ license: 'internal-review',
+ secretLink: true,
+ },
+ {
+ artifactId: 'manuscript',
+ path: 'manuscript/preprint.md',
+ owner: 'u-owner-1',
+ license: 'cc-by-4.0',
+ secretLink: false,
+ },
+ ],
+};
+
+const readyRepository = {
+ repositoryId: 'repo-sci-hydrogel-ready',
+ title: 'Hydrogel Stress Test Release',
+ policy: {
+ nearExpiryWarningDays: 14,
+ },
+ release: {
+ tag: 'v1.4.2',
+ visibility: 'institutional',
+ },
+ collaborators: [
+ {
+ userId: 'u-owner-8',
+ name: 'Dr. Tessa Rowan',
+ role: 'owner',
+ affiliation: 'internal',
+ access: {
+ scope: 'repository-admin',
+ releaseApproved: true,
+ },
+ },
+ {
+ userId: 'u-ext-22',
+ name: 'External Reviewer 22',
+ role: 'reviewer',
+ affiliation: 'external',
+ access: {
+ scope: 'manuscript',
+ expiresAt: '2026-05-29T00:00:00Z',
+ releaseApproved: true,
+ },
+ },
+ ],
+ artifacts: [
+ {
+ artifactId: 'preprint',
+ path: 'manuscript/preprint.md',
+ owner: 'u-owner-8',
+ license: 'cc-by-4.0',
+ secretLink: false,
+ },
+ ],
+};
+
+module.exports = {
+ sampleRepository,
+ readyRepository,
+};
diff --git a/repository-access-review-gate/test.js b/repository-access-review-gate/test.js
new file mode 100644
index 0000000..f7a52db
--- /dev/null
+++ b/repository-access-review-gate/test.js
@@ -0,0 +1,62 @@
+const assert = require('assert');
+const {
+ evaluateRepositoryAccessReview,
+ buildReleaseHoldPacket,
+} = require('./index');
+const { sampleRepository, readyRepository } = require('./sample-data');
+
+function testReleaseHoldSignals() {
+ const review = evaluateRepositoryAccessReview(sampleRepository, {
+ asOf: '2026-05-20T12:00:00Z',
+ releaseTag: 'v2.1.0',
+ });
+
+ assert.equal(review.releaseStatus, 'hold');
+ assert.equal(review.summary.blockingHolds, 5);
+ assert.ok(review.holds.some((hold) => hold.code === 'external_reviewer_expired'));
+ assert.ok(review.holds.some((hold) => hold.code === 'restricted_dataset_approval_expired'));
+ assert.ok(review.holds.some((hold) => hold.code === 'privileged_role_without_release_scope'));
+ assert.ok(review.holds.some((hold) => hold.code === 'secret_link_exposure'));
+ assert.ok(review.holds.some((hold) => hold.code === 'license_access_mismatch'));
+ assert.equal(review.accessMatrix.find((entry) => entry.userId === 'u-ext-17').decision, 'revoke_before_release');
+ assert.equal(review.accessMatrix.find((entry) => entry.userId === 'u-admin-2').decision, 'downgrade_before_release');
+}
+
+function testReleasePacketIsDeterministicAndReviewReady() {
+ const packet = buildReleaseHoldPacket(sampleRepository, {
+ asOf: '2026-05-20T12:00:00Z',
+ releaseTag: 'v2.1.0',
+ });
+
+ assert.match(packet.auditDigest, /^[a-f0-9]{64}$/);
+ assert.deepEqual(
+ packet.releaseHolds.map((hold) => hold.code),
+ [
+ 'external_reviewer_expired',
+ 'license_access_mismatch',
+ 'privileged_role_without_release_scope',
+ 'restricted_dataset_approval_expired',
+ 'secret_link_exposure',
+ ],
+ );
+ assert.equal(packet.reviewerChecklist.length, 5);
+ assert.ok(packet.reviewerChecklist.every((item) => item.owner && item.requiredAction));
+}
+
+function testReadyRepositoryPassesWithAuditableWarnings() {
+ const review = evaluateRepositoryAccessReview(readyRepository, {
+ asOf: '2026-05-20T12:00:00Z',
+ releaseTag: 'v1.4.2',
+ });
+
+ assert.equal(review.releaseStatus, 'ready');
+ assert.equal(review.summary.blockingHolds, 0);
+ assert.equal(review.summary.warningCount, 1);
+ assert.equal(review.warnings[0].code, 'temporary_access_near_expiry');
+}
+
+testReleaseHoldSignals();
+testReleasePacketIsDeterministicAndReviewReady();
+testReadyRepositoryPassesWithAuditableWarnings();
+
+console.log('repository-access-review-gate tests passed');