diff --git a/user-project-management/project-deletion-escrow-auditor/README.md b/user-project-management/project-deletion-escrow-auditor/README.md
new file mode 100644
index 00000000..4ef6c22c
--- /dev/null
+++ b/user-project-management/project-deletion-escrow-auditor/README.md
@@ -0,0 +1,23 @@
+# Project Deletion Escrow Auditor
+
+This module adds a deterministic, dependency-free destructive-action escrow auditor for SCIBASE issue #11. It checks whether project archive/delete requests are safe to schedule without losing provenance, citations, collaborator access, exports, or compliance records.
+
+## Scope
+
+- Requester membership, owner/admin role, and fresh MFA evidence
+- Reversible escrow window and rollback checkpoint checks
+- Pre-delete export bundle, DOI tombstone, and indexed-project redirect readiness
+- Collaborator notice coverage and notice window enforcement
+- Legal hold, active review, downstream fork attribution, and unsettled compute charge checks
+
+All fixtures are synthetic. The module does not access live projects, private files, identity providers, billing providers, credentials, or external APIs, and it never executes destructive actions.
+
+## Run
+
+```bash
+node user-project-management/project-deletion-escrow-auditor/test.js
+node user-project-management/project-deletion-escrow-auditor/demo.js
+node user-project-management/project-deletion-escrow-auditor/make-demo-video.js
+```
+
+Generated deletion escrow audit artifacts are written to `reports/`.
diff --git a/user-project-management/project-deletion-escrow-auditor/demo.js b/user-project-management/project-deletion-escrow-auditor/demo.js
new file mode 100644
index 00000000..9ee83f16
--- /dev/null
+++ b/user-project-management/project-deletion-escrow-auditor/demo.js
@@ -0,0 +1,15 @@
+const fs = require("fs");
+const path = require("path");
+const { auditDeletionEscrow, renderMarkdownReport, renderSvgSummary } = require("./index");
+const { auditPolicy, riskyProject, riskyRequest } = require("./sample-data");
+
+const outputDir = path.join(__dirname, "reports");
+fs.mkdirSync(outputDir, { recursive: true });
+
+const result = auditDeletionEscrow(riskyProject, riskyRequest, auditPolicy);
+fs.writeFileSync(path.join(outputDir, "project-deletion-escrow-audit.json"), `${JSON.stringify(result, null, 2)}\n`);
+fs.writeFileSync(path.join(outputDir, "project-deletion-escrow-audit.md"), renderMarkdownReport(result));
+fs.writeFileSync(path.join(outputDir, "project-deletion-escrow-summary.svg"), renderSvgSummary(result));
+
+console.log(`decision=${result.decision} riskScore=${result.riskScore} findings=${result.findings.length}`);
+console.log(`reports=${outputDir}`);
diff --git a/user-project-management/project-deletion-escrow-auditor/index.js b/user-project-management/project-deletion-escrow-auditor/index.js
new file mode 100644
index 00000000..695feeb7
--- /dev/null
+++ b/user-project-management/project-deletion-escrow-auditor/index.js
@@ -0,0 +1,343 @@
+const crypto = require("crypto");
+
+const SEVERITY_WEIGHT = {
+ blocker: 34,
+ high: 17,
+ medium: 8,
+ low: 3,
+};
+
+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").slice(0, 16);
+}
+
+function finding(severity, code, title, evidence, action) {
+ return { severity, code, title, evidence, action };
+}
+
+function hoursBetween(startIso, endIso) {
+ return Math.max(0, Math.round((new Date(endIso).getTime() - new Date(startIso).getTime()) / 36e5));
+}
+
+function verifyRequester(project, request) {
+ const findings = [];
+ const requester = project.members.find((member) => member.id === request.requestedBy);
+
+ if (!requester) {
+ findings.push(finding(
+ "blocker",
+ "REQUESTER_NOT_PROJECT_MEMBER",
+ "Deletion requester is not a project member",
+ `${request.requestedBy} is not listed in project ${project.id}.`,
+ "Reject the destructive action until it is initiated by a current project member.",
+ ));
+ return findings;
+ }
+
+ if (!["owner", "admin"].includes(requester.role)) {
+ findings.push(finding(
+ "blocker",
+ "REQUESTER_LACKS_DESTRUCTIVE_ROLE",
+ "Requester lacks permission for destructive project action",
+ `${requester.id} role=${requester.role} attempted ${request.action}.`,
+ "Require an owner/admin role and a fresh authorization event before deletion or archive.",
+ ));
+ }
+
+ if (!requester.mfaConfirmedAt) {
+ findings.push(finding(
+ "high",
+ "REQUESTER_MFA_MISSING",
+ "Requester has not confirmed MFA for the destructive action",
+ `${requester.id} has no mfaConfirmedAt value.`,
+ "Require fresh MFA or passkey confirmation before continuing the escrow workflow.",
+ ));
+ }
+
+ return findings;
+}
+
+function verifyEscrowWindow(project, request, policy) {
+ const findings = [];
+ const minimumHours = policy.minimumEscrowHours || 72;
+
+ if (!request.escrowCreatedAt || !request.scheduledAt) {
+ findings.push(finding(
+ "blocker",
+ "ESCROW_WINDOW_MISSING",
+ "Deletion request lacks an escrow window",
+ `${request.action} has escrowCreatedAt=${request.escrowCreatedAt || "missing"} scheduledAt=${request.scheduledAt || "missing"}.`,
+ "Create a reversible escrow window before executing destructive project actions.",
+ ));
+ return findings;
+ }
+
+ const hours = hoursBetween(request.escrowCreatedAt, request.scheduledAt);
+ if (hours < minimumHours) {
+ findings.push(finding(
+ "blocker",
+ "ESCROW_WINDOW_TOO_SHORT",
+ "Deletion escrow window is too short",
+ `${project.id} escrow window is ${hours}h, below ${minimumHours}h.`,
+ "Extend the scheduled deletion time or route to manual institutional review.",
+ ));
+ }
+
+ if (!request.rollbackCheckpointId) {
+ findings.push(finding(
+ "high",
+ "ROLLBACK_CHECKPOINT_MISSING",
+ "No rollback checkpoint is attached",
+ `${project.id} has no rollback checkpoint for ${request.action}.`,
+ "Create a recovery snapshot before irreversible archive/delete execution.",
+ ));
+ }
+
+ return findings;
+}
+
+function verifyExportAndCitation(project, request) {
+ const findings = [];
+
+ if (!request.exportBundleId) {
+ findings.push(finding(
+ "high",
+ "PRE_DELETE_EXPORT_MISSING",
+ "No pre-delete export bundle is attached",
+ `${project.id} has no export bundle for ${request.action}.`,
+ "Generate a manifest-backed export bundle before allowing destructive project changes.",
+ ));
+ }
+
+ if (project.publication?.doi && !request.doiTombstonePlanned) {
+ findings.push(finding(
+ "blocker",
+ "DOI_TOMBSTONE_MISSING",
+ "Published DOI project lacks a tombstone plan",
+ `${project.id} DOI=${project.publication.doi} without tombstone metadata.`,
+ "Prepare DOI tombstone and citation-preservation metadata before deletion.",
+ ));
+ }
+
+ if (project.publication?.indexed && !request.redirectUrl) {
+ findings.push(finding(
+ "high",
+ "INDEXED_PROJECT_REDIRECT_MISSING",
+ "Indexed project lacks a redirect URL",
+ `${project.id} is indexed with no redirectUrl.`,
+ "Add a persistent redirect for search engines, institutional repositories, and citations.",
+ ));
+ }
+
+ return findings;
+}
+
+function verifyCollaboratorNotices(project, request, policy) {
+ const findings = [];
+ const requiredNoticeHours = policy.minimumNoticeHours || 48;
+ const notified = new Set(request.notifiedMemberIds || []);
+ const activeMembers = project.members.filter((member) => member.status === "active");
+ const missingNotices = activeMembers.filter((member) => member.id !== request.requestedBy && !notified.has(member.id));
+
+ if (missingNotices.length) {
+ findings.push(finding(
+ "high",
+ "COLLABORATOR_NOTICE_MISSING",
+ "Not all active collaborators were notified",
+ `${missingNotices.map((member) => member.id).join(", ")} did not receive deletion notice.`,
+ "Notify all active collaborators and give them a chance to export, fork, or dispute the action.",
+ ));
+ }
+
+ if (request.noticeSentAt && request.scheduledAt) {
+ const noticeHours = hoursBetween(request.noticeSentAt, request.scheduledAt);
+ if (noticeHours < requiredNoticeHours) {
+ findings.push(finding(
+ "medium",
+ "COLLABORATOR_NOTICE_TOO_SHORT",
+ "Collaborator notice window is shorter than policy",
+ `${project.id} notice window is ${noticeHours}h, below ${requiredNoticeHours}h.`,
+ "Delay execution until the configured collaborator notice period has elapsed.",
+ ));
+ }
+ }
+
+ return findings;
+}
+
+function verifyHoldsAndDependencies(project, request) {
+ const findings = [];
+
+ if (project.legalHold) {
+ findings.push(finding(
+ "blocker",
+ "LEGAL_HOLD_ACTIVE",
+ "Project is under legal or compliance hold",
+ `${project.id} legalHold=${project.legalHold.reason}.`,
+ "Block deletion until legal/compliance owners release the hold.",
+ ));
+ }
+
+ if (project.activeReviews?.length) {
+ findings.push(finding(
+ "high",
+ "ACTIVE_REVIEW_DEPENDENCY",
+ "Project has active review dependencies",
+ `${project.id} has active reviews: ${project.activeReviews.join(", ")}.`,
+ "Pause deletion until open peer review, grant, or institutional review dependencies are resolved.",
+ ));
+ }
+
+ if (project.childForks?.some((fork) => fork.visibility === "private" && !fork.attributionMigrated)) {
+ findings.push(finding(
+ "medium",
+ "PRIVATE_FORK_ATTRIBUTION_PENDING",
+ "Private fork attribution has not been migrated",
+ `${project.id} has private forks without migrated attribution.`,
+ "Migrate attribution records so downstream forks keep provenance after deletion.",
+ ));
+ }
+
+ if (request.action === "delete" && project.billing?.unsettledComputeCents > 0) {
+ findings.push(finding(
+ "high",
+ "UNSETTLED_COMPUTE_CHARGES",
+ "Project has unsettled compute charges",
+ `${project.id} has $${(project.billing.unsettledComputeCents / 100).toFixed(2)} unsettled.`,
+ "Settle or waive compute charges before project deletion.",
+ ));
+ }
+
+ return findings;
+}
+
+function auditDeletionEscrow(project, request, policy = {}) {
+ const findings = [
+ ...verifyRequester(project, request),
+ ...verifyEscrowWindow(project, request, policy),
+ ...verifyExportAndCitation(project, request),
+ ...verifyCollaboratorNotices(project, request, policy),
+ ...verifyHoldsAndDependencies(project, request),
+ ];
+
+ const riskScore = Math.min(100, findings.reduce((sum, item) => sum + SEVERITY_WEIGHT[item.severity], 0));
+ const blockers = findings.filter((item) => item.severity === "blocker").length;
+ const high = findings.filter((item) => item.severity === "high").length;
+ const decision = blockers
+ ? "block-destructive-project-action"
+ : high
+ ? "manual-project-admin-review"
+ : findings.length
+ ? "schedule-with-escrow-caveats"
+ : "ready-for-escrowed-execution";
+
+ const packet = {
+ projectId: project.id,
+ action: request.action,
+ requestedBy: request.requestedBy,
+ reviewedAt: policy.reviewDate,
+ decision,
+ riskScore,
+ scheduledAt: request.scheduledAt || null,
+ activeMemberCount: project.members.filter((member) => member.status === "active").length,
+ findings,
+ remediationActions: findings.map((item) => ({ code: item.code, action: item.action })),
+ generatedFrom: "synthetic-project-admin-data-only",
+ };
+
+ return {
+ ...packet,
+ auditDigest: digest(packet),
+ };
+}
+
+function renderMarkdownReport(result) {
+ const lines = [
+ "# Project Deletion Escrow Audit",
+ "",
+ `Project: ${result.projectId}`,
+ `Action: ${result.action}`,
+ `Decision: ${result.decision}`,
+ `Risk score: ${result.riskScore}`,
+ `Scheduled at: ${result.scheduledAt || "not scheduled"}`,
+ `Audit digest: ${result.auditDigest}`,
+ "",
+ "## Findings",
+ ];
+
+ if (!result.findings.length) {
+ lines.push("- No deletion escrow issues detected.");
+ } else {
+ result.findings.forEach((item) => {
+ lines.push(`- [${item.severity}] ${item.title}: ${item.evidence}`);
+ lines.push(` Action: ${item.action}`);
+ });
+ }
+
+ lines.push("", "## Safety", "- Synthetic project administration data only; no live projects, private files, identity providers, billing providers, credentials, or external APIs.");
+ return `${lines.join("\n")}\n`;
+}
+
+function escapeXml(value) {
+ return String(value)
+ .replace(/&/g, "&")
+ .replace(//g, ">")
+ .replace(/"/g, """);
+}
+
+function renderSvgSummary(result) {
+ const color = result.decision === "block-destructive-project-action"
+ ? "#b42318"
+ : result.decision === "ready-for-escrowed-execution"
+ ? "#067647"
+ : "#b54708";
+ const severityCounts = ["blocker", "high", "medium", "low"].map((severity) => ({
+ severity,
+ count: result.findings.filter((item) => item.severity === severity).length,
+ }));
+ const maxCount = Math.max(...severityCounts.map((item) => item.count), 1);
+ const bars = severityCounts.map((item, index) => {
+ const y = 205 + index * 52;
+ const width = Math.round(460 * item.count / maxCount);
+ const fill = item.severity === "blocker" ? "#7a271a" : item.severity === "high" ? "#b42318" : item.severity === "medium" ? "#f79009" : "#667085";
+ return `${escapeXml(item.severity)}
+
+
+ ${item.count}`;
+ }).join("\n");
+
+ return `
+`;
+}
+
+module.exports = {
+ auditDeletionEscrow,
+ digest,
+ hoursBetween,
+ renderMarkdownReport,
+ renderSvgSummary,
+};
diff --git a/user-project-management/project-deletion-escrow-auditor/make-demo-video.js b/user-project-management/project-deletion-escrow-auditor/make-demo-video.js
new file mode 100644
index 00000000..bbf0f4ab
--- /dev/null
+++ b/user-project-management/project-deletion-escrow-auditor/make-demo-video.js
@@ -0,0 +1,99 @@
+const fs = require("fs");
+const path = require("path");
+const { execFileSync } = require("child_process");
+const { auditDeletionEscrow } = require("./index");
+const { auditPolicy, riskyProject, riskyRequest } = require("./sample-data");
+
+const WIDTH = 1280;
+const HEIGHT = 720;
+const outputDir = path.join(__dirname, "reports");
+const frameDir = path.join(outputDir, "ppm-frames");
+fs.mkdirSync(frameDir, { recursive: true });
+
+function rgb(hex) {
+ const clean = hex.replace("#", "");
+ return [
+ Number.parseInt(clean.slice(0, 2), 16),
+ Number.parseInt(clean.slice(2, 4), 16),
+ Number.parseInt(clean.slice(4, 6), 16),
+ ];
+}
+
+function canvas(color) {
+ const data = Buffer.alloc(WIDTH * HEIGHT * 3);
+ const [r, g, b] = rgb(color);
+ for (let offset = 0; offset < data.length; offset += 3) {
+ data[offset] = r;
+ data[offset + 1] = g;
+ data[offset + 2] = b;
+ }
+ return data;
+}
+
+function rect(data, x, y, width, height, color) {
+ const [r, g, b] = rgb(color);
+ for (let row = Math.max(0, y); row < Math.min(HEIGHT, y + height); row += 1) {
+ for (let col = Math.max(0, x); col < Math.min(WIDTH, x + width); col += 1) {
+ const offset = (row * WIDTH + col) * 3;
+ data[offset] = r;
+ data[offset + 1] = g;
+ data[offset + 2] = b;
+ }
+ }
+}
+
+function writePpm(filePath, data) {
+ fs.writeFileSync(filePath, Buffer.concat([Buffer.from(`P6\n${WIDTH} ${HEIGHT}\n255\n`), data]));
+}
+
+function renderFrame(filePath, result, frame) {
+ const data = canvas("#f7f8fa");
+ rect(data, 36, 36, 1208, 648, "#ffffff");
+ rect(data, 36, 36, 1208, 3, "#d0d5dd");
+ rect(data, 36, 681, 1208, 3, "#d0d5dd");
+ rect(data, 36, 36, 3, 648, "#d0d5dd");
+ rect(data, 1241, 36, 3, 648, "#d0d5dd");
+
+ rect(data, 80, 82, 800, 34, "#101828");
+ rect(data, 80, 132, Math.round(800 * result.riskScore / 100), 42, "#b42318");
+ rect(data, 900, 82, 260, 92, "#fee4e2");
+ rect(data, 936, 112, 188, 32, "#b42318");
+
+ const severities = ["blocker", "high", "medium", "low"];
+ const counts = severities.map((severity) => result.findings.filter((item) => item.severity === severity).length);
+ const maxCount = Math.max(...counts, 1);
+ counts.forEach((count, index) => {
+ const y = 228 + index * 78;
+ const width = Math.round(820 * count / maxCount);
+ const color = severities[index] === "blocker" ? "#7a271a" : severities[index] === "high" ? "#b42318" : severities[index] === "medium" ? "#f79009" : "#667085";
+ rect(data, 120, y, 820, 42, "#eaecf0");
+ rect(data, 120, y, Math.max(10, width - frame * 4), 42, color);
+ rect(data, 980, y - 8, 96, 58, "#fee4e2");
+ rect(data, 1012, y + 12, 36, 18, color);
+ });
+
+ rect(data, 80, 650, 1080, 12, "#98a2b3");
+ writePpm(filePath, data);
+}
+
+const result = auditDeletionEscrow(riskyProject, riskyRequest, auditPolicy);
+for (let frame = 0; frame < 4; frame += 1) {
+ renderFrame(path.join(frameDir, `frame-${String(frame + 1).padStart(3, "0")}.ppm`), result, frame);
+}
+
+const outputPath = path.join(outputDir, "demo.mp4");
+execFileSync("ffmpeg", [
+ "-y",
+ "-framerate",
+ "1",
+ "-i",
+ path.join(frameDir, "frame-%03d.ppm"),
+ "-vf",
+ "fps=12,format=yuv420p",
+ "-movflags",
+ "+faststart",
+ outputPath,
+], { stdio: "inherit" });
+
+fs.rmSync(frameDir, { recursive: true, force: true });
+console.log(`demo video=${outputPath}`);
diff --git a/user-project-management/project-deletion-escrow-auditor/reports/demo.mp4 b/user-project-management/project-deletion-escrow-auditor/reports/demo.mp4
new file mode 100644
index 00000000..6d5b4ac8
Binary files /dev/null and b/user-project-management/project-deletion-escrow-auditor/reports/demo.mp4 differ
diff --git a/user-project-management/project-deletion-escrow-auditor/reports/project-deletion-escrow-audit.json b/user-project-management/project-deletion-escrow-auditor/reports/project-deletion-escrow-audit.json
new file mode 100644
index 00000000..b9750149
--- /dev/null
+++ b/user-project-management/project-deletion-escrow-auditor/reports/project-deletion-escrow-audit.json
@@ -0,0 +1,148 @@
+{
+ "projectId": "project-quantum-cell-archive",
+ "action": "delete",
+ "requestedBy": "user-editor",
+ "reviewedAt": "2026-05-28T00:00:00Z",
+ "decision": "block-destructive-project-action",
+ "riskScore": 100,
+ "scheduledAt": "2026-05-29T12:00:00Z",
+ "activeMemberCount": 3,
+ "findings": [
+ {
+ "severity": "blocker",
+ "code": "REQUESTER_LACKS_DESTRUCTIVE_ROLE",
+ "title": "Requester lacks permission for destructive project action",
+ "evidence": "user-editor role=editor attempted delete.",
+ "action": "Require an owner/admin role and a fresh authorization event before deletion or archive."
+ },
+ {
+ "severity": "blocker",
+ "code": "ESCROW_WINDOW_TOO_SHORT",
+ "title": "Deletion escrow window is too short",
+ "evidence": "project-quantum-cell-archive escrow window is 36h, below 72h.",
+ "action": "Extend the scheduled deletion time or route to manual institutional review."
+ },
+ {
+ "severity": "high",
+ "code": "ROLLBACK_CHECKPOINT_MISSING",
+ "title": "No rollback checkpoint is attached",
+ "evidence": "project-quantum-cell-archive has no rollback checkpoint for delete.",
+ "action": "Create a recovery snapshot before irreversible archive/delete execution."
+ },
+ {
+ "severity": "high",
+ "code": "PRE_DELETE_EXPORT_MISSING",
+ "title": "No pre-delete export bundle is attached",
+ "evidence": "project-quantum-cell-archive has no export bundle for delete.",
+ "action": "Generate a manifest-backed export bundle before allowing destructive project changes."
+ },
+ {
+ "severity": "blocker",
+ "code": "DOI_TOMBSTONE_MISSING",
+ "title": "Published DOI project lacks a tombstone plan",
+ "evidence": "project-quantum-cell-archive DOI=10.1234/scibase.synthetic.11 without tombstone metadata.",
+ "action": "Prepare DOI tombstone and citation-preservation metadata before deletion."
+ },
+ {
+ "severity": "high",
+ "code": "INDEXED_PROJECT_REDIRECT_MISSING",
+ "title": "Indexed project lacks a redirect URL",
+ "evidence": "project-quantum-cell-archive is indexed with no redirectUrl.",
+ "action": "Add a persistent redirect for search engines, institutional repositories, and citations."
+ },
+ {
+ "severity": "high",
+ "code": "COLLABORATOR_NOTICE_MISSING",
+ "title": "Not all active collaborators were notified",
+ "evidence": "user-reviewer did not receive deletion notice.",
+ "action": "Notify all active collaborators and give them a chance to export, fork, or dispute the action."
+ },
+ {
+ "severity": "medium",
+ "code": "COLLABORATOR_NOTICE_TOO_SHORT",
+ "title": "Collaborator notice window is shorter than policy",
+ "evidence": "project-quantum-cell-archive notice window is 34h, below 48h.",
+ "action": "Delay execution until the configured collaborator notice period has elapsed."
+ },
+ {
+ "severity": "blocker",
+ "code": "LEGAL_HOLD_ACTIVE",
+ "title": "Project is under legal or compliance hold",
+ "evidence": "project-quantum-cell-archive legalHold=institutional records retention review.",
+ "action": "Block deletion until legal/compliance owners release the hold."
+ },
+ {
+ "severity": "high",
+ "code": "ACTIVE_REVIEW_DEPENDENCY",
+ "title": "Project has active review dependencies",
+ "evidence": "project-quantum-cell-archive has active reviews: peer-review-17, grant-closeout-04.",
+ "action": "Pause deletion until open peer review, grant, or institutional review dependencies are resolved."
+ },
+ {
+ "severity": "medium",
+ "code": "PRIVATE_FORK_ATTRIBUTION_PENDING",
+ "title": "Private fork attribution has not been migrated",
+ "evidence": "project-quantum-cell-archive has private forks without migrated attribution.",
+ "action": "Migrate attribution records so downstream forks keep provenance after deletion."
+ },
+ {
+ "severity": "high",
+ "code": "UNSETTLED_COMPUTE_CHARGES",
+ "title": "Project has unsettled compute charges",
+ "evidence": "project-quantum-cell-archive has $18.75 unsettled.",
+ "action": "Settle or waive compute charges before project deletion."
+ }
+ ],
+ "remediationActions": [
+ {
+ "code": "REQUESTER_LACKS_DESTRUCTIVE_ROLE",
+ "action": "Require an owner/admin role and a fresh authorization event before deletion or archive."
+ },
+ {
+ "code": "ESCROW_WINDOW_TOO_SHORT",
+ "action": "Extend the scheduled deletion time or route to manual institutional review."
+ },
+ {
+ "code": "ROLLBACK_CHECKPOINT_MISSING",
+ "action": "Create a recovery snapshot before irreversible archive/delete execution."
+ },
+ {
+ "code": "PRE_DELETE_EXPORT_MISSING",
+ "action": "Generate a manifest-backed export bundle before allowing destructive project changes."
+ },
+ {
+ "code": "DOI_TOMBSTONE_MISSING",
+ "action": "Prepare DOI tombstone and citation-preservation metadata before deletion."
+ },
+ {
+ "code": "INDEXED_PROJECT_REDIRECT_MISSING",
+ "action": "Add a persistent redirect for search engines, institutional repositories, and citations."
+ },
+ {
+ "code": "COLLABORATOR_NOTICE_MISSING",
+ "action": "Notify all active collaborators and give them a chance to export, fork, or dispute the action."
+ },
+ {
+ "code": "COLLABORATOR_NOTICE_TOO_SHORT",
+ "action": "Delay execution until the configured collaborator notice period has elapsed."
+ },
+ {
+ "code": "LEGAL_HOLD_ACTIVE",
+ "action": "Block deletion until legal/compliance owners release the hold."
+ },
+ {
+ "code": "ACTIVE_REVIEW_DEPENDENCY",
+ "action": "Pause deletion until open peer review, grant, or institutional review dependencies are resolved."
+ },
+ {
+ "code": "PRIVATE_FORK_ATTRIBUTION_PENDING",
+ "action": "Migrate attribution records so downstream forks keep provenance after deletion."
+ },
+ {
+ "code": "UNSETTLED_COMPUTE_CHARGES",
+ "action": "Settle or waive compute charges before project deletion."
+ }
+ ],
+ "generatedFrom": "synthetic-project-admin-data-only",
+ "auditDigest": "dc9c67424bd6a288"
+}
diff --git a/user-project-management/project-deletion-escrow-auditor/reports/project-deletion-escrow-audit.md b/user-project-management/project-deletion-escrow-auditor/reports/project-deletion-escrow-audit.md
new file mode 100644
index 00000000..4b85567b
--- /dev/null
+++ b/user-project-management/project-deletion-escrow-auditor/reports/project-deletion-escrow-audit.md
@@ -0,0 +1,37 @@
+# Project Deletion Escrow Audit
+
+Project: project-quantum-cell-archive
+Action: delete
+Decision: block-destructive-project-action
+Risk score: 100
+Scheduled at: 2026-05-29T12:00:00Z
+Audit digest: dc9c67424bd6a288
+
+## Findings
+- [blocker] Requester lacks permission for destructive project action: user-editor role=editor attempted delete.
+ Action: Require an owner/admin role and a fresh authorization event before deletion or archive.
+- [blocker] Deletion escrow window is too short: project-quantum-cell-archive escrow window is 36h, below 72h.
+ Action: Extend the scheduled deletion time or route to manual institutional review.
+- [high] No rollback checkpoint is attached: project-quantum-cell-archive has no rollback checkpoint for delete.
+ Action: Create a recovery snapshot before irreversible archive/delete execution.
+- [high] No pre-delete export bundle is attached: project-quantum-cell-archive has no export bundle for delete.
+ Action: Generate a manifest-backed export bundle before allowing destructive project changes.
+- [blocker] Published DOI project lacks a tombstone plan: project-quantum-cell-archive DOI=10.1234/scibase.synthetic.11 without tombstone metadata.
+ Action: Prepare DOI tombstone and citation-preservation metadata before deletion.
+- [high] Indexed project lacks a redirect URL: project-quantum-cell-archive is indexed with no redirectUrl.
+ Action: Add a persistent redirect for search engines, institutional repositories, and citations.
+- [high] Not all active collaborators were notified: user-reviewer did not receive deletion notice.
+ Action: Notify all active collaborators and give them a chance to export, fork, or dispute the action.
+- [medium] Collaborator notice window is shorter than policy: project-quantum-cell-archive notice window is 34h, below 48h.
+ Action: Delay execution until the configured collaborator notice period has elapsed.
+- [blocker] Project is under legal or compliance hold: project-quantum-cell-archive legalHold=institutional records retention review.
+ Action: Block deletion until legal/compliance owners release the hold.
+- [high] Project has active review dependencies: project-quantum-cell-archive has active reviews: peer-review-17, grant-closeout-04.
+ Action: Pause deletion until open peer review, grant, or institutional review dependencies are resolved.
+- [medium] Private fork attribution has not been migrated: project-quantum-cell-archive has private forks without migrated attribution.
+ Action: Migrate attribution records so downstream forks keep provenance after deletion.
+- [high] Project has unsettled compute charges: project-quantum-cell-archive has $18.75 unsettled.
+ Action: Settle or waive compute charges before project deletion.
+
+## Safety
+- Synthetic project administration data only; no live projects, private files, identity providers, billing providers, credentials, or external APIs.
diff --git a/user-project-management/project-deletion-escrow-auditor/reports/project-deletion-escrow-summary.svg b/user-project-management/project-deletion-escrow-auditor/reports/project-deletion-escrow-summary.svg
new file mode 100644
index 00000000..606f9e4a
--- /dev/null
+++ b/user-project-management/project-deletion-escrow-auditor/reports/project-deletion-escrow-summary.svg
@@ -0,0 +1,26 @@
+
diff --git a/user-project-management/project-deletion-escrow-auditor/sample-data.js b/user-project-management/project-deletion-escrow-auditor/sample-data.js
new file mode 100644
index 00000000..6f23caf1
--- /dev/null
+++ b/user-project-management/project-deletion-escrow-auditor/sample-data.js
@@ -0,0 +1,82 @@
+const auditPolicy = {
+ reviewDate: "2026-05-28T00:00:00Z",
+ minimumEscrowHours: 72,
+ minimumNoticeHours: 48,
+};
+
+const riskyProject = {
+ id: "project-quantum-cell-archive",
+ members: [
+ { id: "user-owner", role: "owner", status: "active", mfaConfirmedAt: "" },
+ { id: "user-editor", role: "editor", status: "active", mfaConfirmedAt: "2026-05-27T18:00:00Z" },
+ { id: "user-reviewer", role: "reviewer", status: "active", mfaConfirmedAt: "2026-05-26T11:00:00Z" },
+ ],
+ publication: {
+ doi: "10.1234/scibase.synthetic.11",
+ indexed: true,
+ },
+ legalHold: {
+ reason: "institutional records retention review",
+ },
+ activeReviews: ["peer-review-17", "grant-closeout-04"],
+ childForks: [
+ { id: "fork-private-1", visibility: "private", attributionMigrated: false },
+ ],
+ billing: {
+ unsettledComputeCents: 1875,
+ },
+};
+
+const riskyRequest = {
+ action: "delete",
+ requestedBy: "user-editor",
+ escrowCreatedAt: "2026-05-28T00:00:00Z",
+ scheduledAt: "2026-05-29T12:00:00Z",
+ rollbackCheckpointId: "",
+ exportBundleId: "",
+ doiTombstonePlanned: false,
+ redirectUrl: "",
+ noticeSentAt: "2026-05-28T02:00:00Z",
+ notifiedMemberIds: ["user-owner"],
+};
+
+const readyProject = {
+ id: "project-open-catalyst-archive",
+ members: [
+ { id: "user-owner", role: "owner", status: "active", mfaConfirmedAt: "2026-05-28T10:00:00Z" },
+ { id: "user-editor", role: "editor", status: "active", mfaConfirmedAt: "2026-05-27T18:00:00Z" },
+ ],
+ publication: {
+ doi: "10.1234/scibase.synthetic.ready",
+ indexed: true,
+ },
+ legalHold: null,
+ activeReviews: [],
+ childForks: [
+ { id: "fork-public-1", visibility: "public", attributionMigrated: true },
+ ],
+ billing: {
+ unsettledComputeCents: 0,
+ },
+};
+
+const readyRequest = {
+ action: "archive",
+ requestedBy: "user-owner",
+ escrowCreatedAt: "2026-05-28T00:00:00Z",
+ scheduledAt: "2026-06-01T00:00:00Z",
+ rollbackCheckpointId: "checkpoint-archive-v3",
+ exportBundleId: "export-bundle-archive-v3",
+ doiTombstonePlanned: true,
+ redirectUrl: "https://scibase.example/projects/project-open-catalyst-archive/tombstone",
+ noticeSentAt: "2026-05-28T00:00:00Z",
+ notifiedMemberIds: ["user-editor"],
+};
+
+module.exports = {
+ auditPolicy,
+ readyProject,
+ readyRequest,
+ riskyProject,
+ riskyRequest,
+};
diff --git a/user-project-management/project-deletion-escrow-auditor/test.js b/user-project-management/project-deletion-escrow-auditor/test.js
new file mode 100644
index 00000000..9138c074
--- /dev/null
+++ b/user-project-management/project-deletion-escrow-auditor/test.js
@@ -0,0 +1,48 @@
+const assert = require("assert");
+const {
+ auditDeletionEscrow,
+ digest,
+ hoursBetween,
+ renderMarkdownReport,
+ renderSvgSummary,
+} = require("./index");
+const {
+ auditPolicy,
+ readyProject,
+ readyRequest,
+ riskyProject,
+ riskyRequest,
+} = require("./sample-data");
+
+const risky = auditDeletionEscrow(riskyProject, riskyRequest, auditPolicy);
+assert.strictEqual(risky.decision, "block-destructive-project-action");
+assert.ok(risky.riskScore >= 90, "risky deletion should produce a strong block score");
+assert.ok(risky.findings.some((item) => item.code === "REQUESTER_LACKS_DESTRUCTIVE_ROLE"));
+assert.ok(risky.findings.some((item) => item.code === "ESCROW_WINDOW_TOO_SHORT"));
+assert.ok(risky.findings.some((item) => item.code === "ROLLBACK_CHECKPOINT_MISSING"));
+assert.ok(risky.findings.some((item) => item.code === "PRE_DELETE_EXPORT_MISSING"));
+assert.ok(risky.findings.some((item) => item.code === "DOI_TOMBSTONE_MISSING"));
+assert.ok(risky.findings.some((item) => item.code === "COLLABORATOR_NOTICE_MISSING"));
+assert.ok(risky.findings.some((item) => item.code === "LEGAL_HOLD_ACTIVE"));
+assert.ok(risky.findings.some((item) => item.code === "UNSETTLED_COMPUTE_CHARGES"));
+
+const repeat = auditDeletionEscrow(riskyProject, riskyRequest, auditPolicy);
+assert.strictEqual(risky.auditDigest, repeat.auditDigest, "audit digest must be deterministic");
+
+const ready = auditDeletionEscrow(readyProject, readyRequest, auditPolicy);
+assert.strictEqual(ready.decision, "ready-for-escrowed-execution");
+assert.strictEqual(ready.findings.length, 0);
+
+assert.strictEqual(hoursBetween("2026-05-28T00:00:00Z", "2026-05-29T12:00:00Z"), 36);
+assert.strictEqual(digest({ b: 2, a: 1 }), digest({ a: 1, b: 2 }));
+
+const markdown = renderMarkdownReport(risky);
+assert.ok(markdown.includes("Project Deletion Escrow Audit"));
+assert.ok(markdown.includes("Synthetic project administration data only"));
+assert.ok(markdown.includes("block-destructive-project-action"));
+
+const svg = renderSvgSummary(risky);
+assert.ok(svg.includes("