diff --git a/project-role-delegation-guard/README.md b/project-role-delegation-guard/README.md
new file mode 100644
index 00000000..973de0d4
--- /dev/null
+++ b/project-role-delegation-guard/README.md
@@ -0,0 +1,14 @@
+# Project Role Delegation Guard
+
+This self-contained slice reviews temporary delegated authority before sensitive project actions such as publication, restricted-data export, archive freeze, and role transfer.
+
+The guard checks sponsor approval, separation of duties, identity posture, object-level scope, and expiry. It produces deterministic reviewer packets so project owners can approve or block delegated actions without relying on ad hoc comments.
+
+## Run locally
+
+```bash
+node project-role-delegation-guard/test.js
+node project-role-delegation-guard/demo.js
+```
+
+Demo outputs are written to `project-role-delegation-guard/demo-output/`.
diff --git a/project-role-delegation-guard/acceptance-notes.md b/project-role-delegation-guard/acceptance-notes.md
new file mode 100644
index 00000000..d604c8c6
--- /dev/null
+++ b/project-role-delegation-guard/acceptance-notes.md
@@ -0,0 +1,6 @@
+# Acceptance Notes
+
+- The module is dependency-free and uses synthetic data only.
+- Tests cover blocked sensitive delegations, deterministic packet ordering, and a ready case with a near-expiry warning.
+- Demo artifacts include JSON, Markdown, SVG, and MP4 output.
+- The slice is intentionally separate from previous #11 submissions around broad RBAC ledgers, offboarding, access recertification, anonymous review, data-room consent, project archive handoff, profile sync, and audit anomaly monitoring.
diff --git a/project-role-delegation-guard/demo-output/approval-checklist.md b/project-role-delegation-guard/demo-output/approval-checklist.md
new file mode 100644
index 00000000..48cd7190
--- /dev/null
+++ b/project-role-delegation-guard/demo-output/approval-checklist.md
@@ -0,0 +1,11 @@
+# Delegation approval checklist
+
+Workspace: project-neuro-catalyst
+Status: hold
+
+- del-archive-5: Narrow the object-level delegation scope before approving the action.
+- del-export-7: Block the delegation until MFA, SAML, and ORCID posture requirements are satisfied.
+- del-publish-2: Record sponsor approval or route the action back to the project owner.
+- del-transfer-3: Route approval to an independent owner or institutional sponsor.
+
+Audit digest: eb75b6a2db4222fa1c946655d7ea70920e1685e0ed6e5303d829ea16dc51b0ed
diff --git a/project-role-delegation-guard/demo-output/delegation-review-packet.json b/project-role-delegation-guard/demo-output/delegation-review-packet.json
new file mode 100644
index 00000000..02f4912c
--- /dev/null
+++ b/project-role-delegation-guard/demo-output/delegation-review-packet.json
@@ -0,0 +1,98 @@
+{
+ "packetType": "project-role-delegation-guard",
+ "workspaceId": "project-neuro-catalyst",
+ "generatedAt": "2026-05-20T12:00:00Z",
+ "overallStatus": "hold",
+ "decisions": [
+ {
+ "requestId": "del-publish-2",
+ "action": "publish_release",
+ "delegateId": "delegate-2",
+ "decision": "needs_sponsor",
+ "expiresAt": "2026-06-20T00:00:00Z"
+ },
+ {
+ "requestId": "del-export-7",
+ "action": "restricted_data_export",
+ "delegateId": "delegate-7",
+ "decision": "block",
+ "expiresAt": "2026-06-10T00:00:00Z"
+ },
+ {
+ "requestId": "del-transfer-3",
+ "action": "role_transfer",
+ "delegateId": "delegate-2",
+ "decision": "block",
+ "expiresAt": "2026-06-15T00:00:00Z"
+ },
+ {
+ "requestId": "del-archive-5",
+ "action": "archive_freeze",
+ "delegateId": "delegate-9",
+ "decision": "block",
+ "expiresAt": "2026-06-15T00:00:00Z"
+ }
+ ],
+ "holds": [
+ {
+ "requestId": "del-archive-5",
+ "code": "delegated_scope_exceeds_action",
+ "owner": "delegate-9",
+ "severity": "blocker",
+ "evidence": "del-archive-5 includes excess scope(s): repository:admin.",
+ "requiredAction": "Narrow the object-level delegation scope before approving the action."
+ },
+ {
+ "requestId": "del-export-7",
+ "code": "identity_posture_gap",
+ "owner": "delegate-7",
+ "severity": "blocker",
+ "evidence": "Export Contractor is missing mfa, orcid for restricted_data_export.",
+ "requiredAction": "Block the delegation until MFA, SAML, and ORCID posture requirements are satisfied."
+ },
+ {
+ "requestId": "del-publish-2",
+ "code": "missing_sponsor_approval",
+ "owner": "owner-1",
+ "severity": "blocker",
+ "evidence": "publish_release requires sponsor approval before delegate-2 can act.",
+ "requiredAction": "Record sponsor approval or route the action back to the project owner."
+ },
+ {
+ "requestId": "del-transfer-3",
+ "code": "separation_of_duties_violation",
+ "owner": "delegate-2",
+ "severity": "blocker",
+ "evidence": "delegate-2 cannot both request/delegate and approve role_transfer.",
+ "requiredAction": "Route approval to an independent owner or institutional sponsor."
+ }
+ ],
+ "warnings": [],
+ "approvalChecklist": [
+ {
+ "requestId": "del-archive-5",
+ "owner": "delegate-9",
+ "requiredAction": "Narrow the object-level delegation scope before approving the action.",
+ "evidence": "del-archive-5 includes excess scope(s): repository:admin."
+ },
+ {
+ "requestId": "del-export-7",
+ "owner": "delegate-7",
+ "requiredAction": "Block the delegation until MFA, SAML, and ORCID posture requirements are satisfied.",
+ "evidence": "Export Contractor is missing mfa, orcid for restricted_data_export."
+ },
+ {
+ "requestId": "del-publish-2",
+ "owner": "owner-1",
+ "requiredAction": "Record sponsor approval or route the action back to the project owner.",
+ "evidence": "publish_release requires sponsor approval before delegate-2 can act."
+ },
+ {
+ "requestId": "del-transfer-3",
+ "owner": "delegate-2",
+ "requiredAction": "Route approval to an independent owner or institutional sponsor.",
+ "evidence": "delegate-2 cannot both request/delegate and approve role_transfer."
+ }
+ ],
+ "auditDigest": "eb75b6a2db4222fa1c946655d7ea70920e1685e0ed6e5303d829ea16dc51b0ed"
+}
diff --git a/project-role-delegation-guard/demo-output/demo.mp4 b/project-role-delegation-guard/demo-output/demo.mp4
new file mode 100644
index 00000000..41bb123e
Binary files /dev/null and b/project-role-delegation-guard/demo-output/demo.mp4 differ
diff --git a/project-role-delegation-guard/demo-output/demo.svg b/project-role-delegation-guard/demo-output/demo.svg
new file mode 100644
index 00000000..de0e1e4c
--- /dev/null
+++ b/project-role-delegation-guard/demo-output/demo.svg
@@ -0,0 +1,12 @@
+
diff --git a/project-role-delegation-guard/demo.js b/project-role-delegation-guard/demo.js
new file mode 100644
index 00000000..ea8eba81
--- /dev/null
+++ b/project-role-delegation-guard/demo.js
@@ -0,0 +1,74 @@
+const fs = require('fs');
+const path = require('path');
+const { execFileSync } = require('child_process');
+const { buildDelegationReviewPacket } = require('./index');
+const { sampleWorkspace } = require('./sample-data');
+
+const outputDir = path.join(__dirname, 'demo-output');
+fs.mkdirSync(outputDir, { recursive: true });
+
+const packet = buildDelegationReviewPacket(sampleWorkspace, {
+ asOf: '2026-05-20T12:00:00Z',
+});
+
+fs.writeFileSync(path.join(outputDir, 'delegation-review-packet.json'), `${JSON.stringify(packet, null, 2)}\n`);
+
+const rows = packet.holds
+ .map((hold, index) => `${index + 1}. ${hold.requestId}: ${hold.code}`)
+ .join('\n ');
+
+fs.writeFileSync(path.join(outputDir, 'demo.svg'), `
+`);
+
+fs.writeFileSync(path.join(outputDir, 'approval-checklist.md'), [
+ '# Delegation approval checklist',
+ '',
+ `Workspace: ${packet.workspaceId}`,
+ `Status: ${packet.overallStatus}`,
+ '',
+ ...packet.approvalChecklist.map((item) => `- ${item.requestId}: ${item.requiredAction}`),
+ '',
+ `Audit digest: ${packet.auditDigest}`,
+ '',
+].join('\n'));
+
+function renderMp4() {
+ const videoPath = path.join(outputDir, 'demo.mp4');
+ const font = 'C\\:/Windows/Fonts/arial.ttf';
+ const escapeText = (value) => String(value).replace(/\\/g, '\\\\').replace(/:/g, '\\:').replace(/'/g, "\\'");
+ const filters = [
+ `drawtext=fontfile='${font}':text='${escapeText('Project Role Delegation Guard')}':x=70:y=80:fontsize=44:fontcolor=white`,
+ `drawtext=fontfile='${font}':text='${escapeText(`${packet.holds.length} approval blockers found`)}':x=70:y=155:fontsize=34:fontcolor=0xffd166`,
+ ...packet.holds.map((hold, index) =>
+ `drawtext=fontfile='${font}':text='${escapeText(`${hold.requestId}: ${hold.code}`)}':x=90:y=${235 + index * 58}:fontsize=28:fontcolor=white`,
+ ),
+ `drawtext=fontfile='${font}':text='${escapeText(`audit ${packet.auditDigest.slice(0, 20)}...`)}':x=70:y=630:fontsize=24:fontcolor=0x93c5fd`,
+ ].join(',');
+
+ execFileSync('ffmpeg', [
+ '-y',
+ '-f',
+ 'lavfi',
+ '-i',
+ 'color=c=0x0b1320: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/project-role-delegation-guard/index.js b/project-role-delegation-guard/index.js
new file mode 100644
index 00000000..a5f34fba
--- /dev/null
+++ b/project-role-delegation-guard/index.js
@@ -0,0 +1,223 @@
+const crypto = require('crypto');
+
+const SENSITIVE_ACTIONS = new Set([
+ 'publish_release',
+ 'restricted_data_export',
+ 'archive_freeze',
+ 'role_transfer',
+]);
+
+const ALLOWED_SCOPES_BY_ACTION = {
+ publish_release: new Set(['manuscript:approve', 'metadata:update', 'release:submit']),
+ restricted_data_export: new Set(['dataset:read', 'export:restricted']),
+ archive_freeze: new Set(['archive:freeze', 'citation:snapshot']),
+ role_transfer: new Set(['role:transfer']),
+};
+
+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, request, code, owner, evidence, requiredAction) {
+ holds.push({
+ requestId: request.requestId,
+ code,
+ owner,
+ severity: 'blocker',
+ evidence,
+ requiredAction,
+ });
+}
+
+function addWarning(warnings, request, code, owner, evidence, requiredAction) {
+ warnings.push({
+ requestId: request.requestId,
+ code,
+ owner,
+ severity: 'warning',
+ evidence,
+ requiredAction,
+ });
+}
+
+function evaluateRequest(request, workspace, options) {
+ const usersById = new Map(workspace.users.map((user) => [user.userId, user]));
+ const delegate = usersById.get(request.delegateId);
+ const holds = [];
+ const warnings = [];
+ const sensitive = SENSITIVE_ACTIONS.has(request.action);
+
+ if (toTime(request.expiresAt) < toTime(options.asOf)) {
+ addHold(
+ holds,
+ request,
+ 'delegation_expired',
+ request.delegateId,
+ `${request.requestId} expired on ${request.expiresAt}.`,
+ 'Create a new delegation request before approving the sensitive action.',
+ );
+ } else if (daysUntil(request.expiresAt, options.asOf) <= workspace.policy.nearExpiryWarningDays) {
+ addWarning(
+ warnings,
+ request,
+ 'delegation_near_expiry',
+ request.delegateId,
+ `${request.requestId} expires in ${daysUntil(request.expiresAt, options.asOf)} days.`,
+ 'Confirm the delegated authority will still be valid when the action executes.',
+ );
+ }
+
+ if (sensitive && !request.sponsorApproval) {
+ addHold(
+ holds,
+ request,
+ 'missing_sponsor_approval',
+ request.sponsorId,
+ `${request.action} requires sponsor approval before ${request.delegateId} can act.`,
+ 'Record sponsor approval or route the action back to the project owner.',
+ );
+ }
+
+ if (request.requiresIdentityPosture) {
+ const missing = request.requiresIdentityPosture.filter((flag) => !delegate.identity[flag]);
+ if (missing.length > 0) {
+ addHold(
+ holds,
+ request,
+ 'identity_posture_gap',
+ request.delegateId,
+ `${delegate.name} is missing ${missing.join(', ')} for ${request.action}.`,
+ 'Block the delegation until MFA, SAML, and ORCID posture requirements are satisfied.',
+ );
+ }
+ }
+
+ if (sensitive && (request.approverId === request.requestorId || request.approverId === request.delegateId)) {
+ addHold(
+ holds,
+ request,
+ 'separation_of_duties_violation',
+ request.approverId,
+ `${request.approverId} cannot both request/delegate and approve ${request.action}.`,
+ 'Route approval to an independent owner or institutional sponsor.',
+ );
+ }
+
+ const allowedScopes = ALLOWED_SCOPES_BY_ACTION[request.action] || new Set();
+ const extraScopes = request.scopes.filter((scope) => !allowedScopes.has(scope));
+ if (extraScopes.length > 0) {
+ addHold(
+ holds,
+ request,
+ 'delegated_scope_exceeds_action',
+ request.delegateId,
+ `${request.requestId} includes excess scope(s): ${extraScopes.join(', ')}.`,
+ 'Narrow the object-level delegation scope before approving the action.',
+ );
+ }
+
+ const holdCodes = new Set(holds.map((hold) => hold.code));
+ const decision = holdCodes.has('missing_sponsor_approval') && holds.length === 1
+ ? 'needs_sponsor'
+ : holds.length > 0
+ ? 'block'
+ : 'approve';
+
+ return {
+ decision: {
+ requestId: request.requestId,
+ action: request.action,
+ delegateId: request.delegateId,
+ decision,
+ expiresAt: request.expiresAt,
+ },
+ holds,
+ warnings,
+ };
+}
+
+function evaluateDelegationRequests(workspace, options = {}) {
+ const evaluationOptions = {
+ asOf: options.asOf || new Date().toISOString(),
+ };
+ const decisions = [];
+ const holds = [];
+ const warnings = [];
+
+ for (const request of workspace.delegationRequests) {
+ const result = evaluateRequest(request, workspace, evaluationOptions);
+ decisions.push(result.decision);
+ holds.push(...result.holds);
+ warnings.push(...result.warnings);
+ }
+
+ const sortedHolds = holds.sort((left, right) => left.code.localeCompare(right.code));
+ const sortedWarnings = warnings.sort((left, right) => left.code.localeCompare(right.code));
+
+ return {
+ workspaceId: workspace.workspaceId,
+ overallStatus: sortedHolds.length > 0 ? 'hold' : 'ready',
+ summary: {
+ requestsReviewed: workspace.delegationRequests.length,
+ blockingHolds: sortedHolds.length,
+ warnings: sortedWarnings.length,
+ approved: decisions.filter((decision) => decision.decision === 'approve').length,
+ blocked: decisions.filter((decision) => decision.decision === 'block').length,
+ },
+ decisions,
+ holds: sortedHolds,
+ warnings: sortedWarnings,
+ };
+}
+
+function buildDelegationReviewPacket(workspace, options = {}) {
+ const review = evaluateDelegationRequests(workspace, options);
+ const packet = {
+ packetType: 'project-role-delegation-guard',
+ workspaceId: review.workspaceId,
+ generatedAt: options.asOf || new Date().toISOString(),
+ overallStatus: review.overallStatus,
+ decisions: review.decisions,
+ holds: review.holds,
+ warnings: review.warnings,
+ approvalChecklist: review.holds.map((hold) => ({
+ requestId: hold.requestId,
+ owner: hold.owner,
+ requiredAction: hold.requiredAction,
+ evidence: hold.evidence,
+ })),
+ };
+
+ return {
+ ...packet,
+ auditDigest: digest(packet),
+ };
+}
+
+module.exports = {
+ evaluateDelegationRequests,
+ buildDelegationReviewPacket,
+ stableStringify,
+ digest,
+};
diff --git a/project-role-delegation-guard/requirements-map.md b/project-role-delegation-guard/requirements-map.md
new file mode 100644
index 00000000..f0611fc7
--- /dev/null
+++ b/project-role-delegation-guard/requirements-map.md
@@ -0,0 +1,11 @@
+# Requirements Map
+
+Issue #11 asks for user and project management: profiles, roles, access controls, project visibility, invitation workflows, audit trails, and administrative safety.
+
+| Requirement area | Coverage in this slice |
+| --- | --- |
+| User identity | Checks MFA, SAML, and ORCID posture before delegated actions. |
+| Project roles | Evaluates delegated authority for publication, export, archive, and role transfer actions. |
+| Access control | Enforces action-specific object scopes and blocks over-broad delegated scopes. |
+| Auditability | Produces deterministic review packets, approval checklists, and SHA-256 audit digests. |
+| Project governance | Requires sponsor approval and separation of duties for sensitive project workflows. |
diff --git a/project-role-delegation-guard/sample-data.js b/project-role-delegation-guard/sample-data.js
new file mode 100644
index 00000000..29599439
--- /dev/null
+++ b/project-role-delegation-guard/sample-data.js
@@ -0,0 +1,126 @@
+const sampleWorkspace = {
+ workspaceId: 'project-neuro-catalyst',
+ policy: {
+ nearExpiryWarningDays: 7,
+ },
+ users: [
+ {
+ userId: 'owner-1',
+ name: 'Dr. Lina Owner',
+ identity: { mfa: true, saml: true, orcid: true },
+ },
+ {
+ userId: 'delegate-2',
+ name: 'Postdoc Delegate',
+ identity: { mfa: true, saml: true, orcid: true },
+ },
+ {
+ userId: 'delegate-7',
+ name: 'Export Contractor',
+ identity: { mfa: false, saml: true, orcid: false },
+ },
+ {
+ userId: 'delegate-9',
+ name: 'Archive Assistant',
+ identity: { mfa: true, saml: true, orcid: true },
+ },
+ {
+ userId: 'approver-3',
+ name: 'Independent Project Approver',
+ identity: { mfa: true, saml: true, orcid: true },
+ },
+ ],
+ delegationRequests: [
+ {
+ requestId: 'del-publish-2',
+ action: 'publish_release',
+ requestorId: 'owner-1',
+ delegateId: 'delegate-2',
+ approverId: 'approver-3',
+ sponsorId: 'owner-1',
+ sponsorApproval: false,
+ scopes: ['manuscript:approve', 'metadata:update', 'release:submit'],
+ expiresAt: '2026-06-20T00:00:00Z',
+ requiresIdentityPosture: ['mfa', 'saml', 'orcid'],
+ },
+ {
+ requestId: 'del-export-7',
+ action: 'restricted_data_export',
+ requestorId: 'owner-1',
+ delegateId: 'delegate-7',
+ approverId: 'approver-3',
+ sponsorId: 'owner-1',
+ sponsorApproval: true,
+ scopes: ['dataset:read', 'export:restricted'],
+ expiresAt: '2026-06-10T00:00:00Z',
+ requiresIdentityPosture: ['mfa', 'saml', 'orcid'],
+ },
+ {
+ requestId: 'del-transfer-3',
+ action: 'role_transfer',
+ requestorId: 'delegate-2',
+ delegateId: 'delegate-2',
+ approverId: 'delegate-2',
+ sponsorId: 'owner-1',
+ sponsorApproval: true,
+ scopes: ['role:transfer'],
+ expiresAt: '2026-06-15T00:00:00Z',
+ requiresIdentityPosture: ['mfa', 'saml'],
+ },
+ {
+ requestId: 'del-archive-5',
+ action: 'archive_freeze',
+ requestorId: 'owner-1',
+ delegateId: 'delegate-9',
+ approverId: 'approver-3',
+ sponsorId: 'owner-1',
+ sponsorApproval: true,
+ scopes: ['archive:freeze', 'citation:snapshot', 'repository:admin'],
+ expiresAt: '2026-06-15T00:00:00Z',
+ requiresIdentityPosture: ['mfa', 'saml'],
+ },
+ ],
+};
+
+const compliantWorkspace = {
+ workspaceId: 'project-clean-delegation',
+ policy: {
+ nearExpiryWarningDays: 7,
+ },
+ users: [
+ {
+ userId: 'owner-5',
+ name: 'Dr. Maya Sponsor',
+ identity: { mfa: true, saml: true, orcid: true },
+ },
+ {
+ userId: 'delegate-5',
+ name: 'Release Delegate',
+ identity: { mfa: true, saml: true, orcid: true },
+ },
+ {
+ userId: 'approver-8',
+ name: 'Independent Approver',
+ identity: { mfa: true, saml: true, orcid: true },
+ },
+ ],
+ delegationRequests: [
+ {
+ requestId: 'del-clean-1',
+ action: 'publish_release',
+ requestorId: 'owner-5',
+ delegateId: 'delegate-5',
+ approverId: 'approver-8',
+ sponsorId: 'owner-5',
+ sponsorApproval: true,
+ scopes: ['manuscript:approve', 'metadata:update', 'release:submit'],
+ expiresAt: '2026-05-26T00:00:00Z',
+ requiresIdentityPosture: ['mfa', 'saml', 'orcid'],
+ },
+ ],
+};
+
+module.exports = {
+ sampleWorkspace,
+ compliantWorkspace,
+};
diff --git a/project-role-delegation-guard/test.js b/project-role-delegation-guard/test.js
new file mode 100644
index 00000000..00d7ab40
--- /dev/null
+++ b/project-role-delegation-guard/test.js
@@ -0,0 +1,57 @@
+const assert = require('assert');
+const {
+ evaluateDelegationRequests,
+ buildDelegationReviewPacket,
+} = require('./index');
+const { sampleWorkspace, compliantWorkspace } = require('./sample-data');
+
+function testSensitiveDelegationsAreHeld() {
+ const review = evaluateDelegationRequests(sampleWorkspace, {
+ asOf: '2026-05-20T12:00:00Z',
+ });
+
+ assert.equal(review.overallStatus, 'hold');
+ assert.equal(review.summary.blockingHolds, 4);
+ assert.ok(review.holds.some((hold) => hold.code === 'missing_sponsor_approval'));
+ assert.ok(review.holds.some((hold) => hold.code === 'identity_posture_gap'));
+ assert.ok(review.holds.some((hold) => hold.code === 'separation_of_duties_violation'));
+ assert.ok(review.holds.some((hold) => hold.code === 'delegated_scope_exceeds_action'));
+ assert.equal(review.decisions.find((decision) => decision.requestId === 'del-export-7').decision, 'block');
+ assert.equal(review.decisions.find((decision) => decision.requestId === 'del-publish-2').decision, 'needs_sponsor');
+}
+
+function testReviewPacketIsDeterministic() {
+ const packet = buildDelegationReviewPacket(sampleWorkspace, {
+ asOf: '2026-05-20T12:00:00Z',
+ });
+
+ assert.match(packet.auditDigest, /^[a-f0-9]{64}$/);
+ assert.deepEqual(
+ packet.holds.map((hold) => hold.code),
+ [
+ 'delegated_scope_exceeds_action',
+ 'identity_posture_gap',
+ 'missing_sponsor_approval',
+ 'separation_of_duties_violation',
+ ],
+ );
+ assert.equal(packet.approvalChecklist.length, 4);
+ assert.ok(packet.approvalChecklist.every((item) => item.requestId && item.requiredAction));
+}
+
+function testCompliantDelegationsPassWithExpiryWarning() {
+ const review = evaluateDelegationRequests(compliantWorkspace, {
+ asOf: '2026-05-20T12:00:00Z',
+ });
+
+ assert.equal(review.overallStatus, 'ready');
+ assert.equal(review.summary.blockingHolds, 0);
+ assert.equal(review.warnings.length, 1);
+ assert.equal(review.warnings[0].code, 'delegation_near_expiry');
+}
+
+testSensitiveDelegationsAreHeld();
+testReviewPacketIsDeterministic();
+testCompliantDelegationsPassWithExpiryWarning();
+
+console.log('project-role-delegation-guard tests passed');