From d21a80c03424b50d61c9ac7b1d72c757b875e192 Mon Sep 17 00:00:00 2001 From: tuanadr Date: Wed, 20 May 2026 18:14:40 +0700 Subject: [PATCH] Add repository access review gate --- repository-access-review-gate/README.md | 23 ++ .../acceptance-notes.md | 7 + .../demo-output/demo.mp4 | Bin 0 -> 48708 bytes .../demo-output/demo.svg | 13 ++ .../demo-output/release-hold-packet.json | 107 +++++++++ .../demo-output/reviewer-packet.md | 15 ++ repository-access-review-gate/demo.js | 81 +++++++ repository-access-review-gate/index.js | 212 ++++++++++++++++++ .../requirements-map.md | 12 + repository-access-review-gate/sample-data.js | 128 +++++++++++ repository-access-review-gate/test.js | 62 +++++ 11 files changed, 660 insertions(+) create mode 100644 repository-access-review-gate/README.md create mode 100644 repository-access-review-gate/acceptance-notes.md create mode 100644 repository-access-review-gate/demo-output/demo.mp4 create mode 100644 repository-access-review-gate/demo-output/demo.svg create mode 100644 repository-access-review-gate/demo-output/release-hold-packet.json create mode 100644 repository-access-review-gate/demo-output/reviewer-packet.md create mode 100644 repository-access-review-gate/demo.js create mode 100644 repository-access-review-gate/index.js create mode 100644 repository-access-review-gate/requirements-map.md create mode 100644 repository-access-review-gate/sample-data.js create mode 100644 repository-access-review-gate/test.js 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 0000000000000000000000000000000000000000..e6fcdf314f1e73e65a31a7acf596aadb8fc63387 GIT binary patch literal 48708 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 literal 0 HcmV?d00001 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 @@ + + + + Repository Access Review Gate + repo-sci-artery-flow-042 / v2.1.0 + 5 release holds before export + 1. external_reviewer_expired -> u-ext-17 + 2. license_access_mismatch -> data-steward + 3. privileged_role_without_release_scope -> u-admin-2 + 4. restricted_dataset_approval_expired -> u-ext-17 + 5. secret_link_exposure -> security-review + audit digest: 8f8ae3250c816f296ba59f67... + 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 = ` + + + Repository Access Review Gate + ${packet.repositoryId} / ${packet.releaseTag} + ${packet.releaseHolds.length} release holds before export + ${packet.releaseHolds + .map( + (hold, index) => + `${index + 1}. ${hold.code} -> ${hold.owner}`, + ) + .join('\n ')} + audit digest: ${packet.auditDigest.slice(0, 24)}... + +`; +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');