diff --git a/project-provisioning-baseline-guard/README.md b/project-provisioning-baseline-guard/README.md
new file mode 100644
index 00000000..1f399367
--- /dev/null
+++ b/project-provisioning-baseline-guard/README.md
@@ -0,0 +1,19 @@
+# Project Provisioning Baseline Guard
+
+This module adds a focused User & Project Management guard for creating new research workspaces safely.
+
+It evaluates whether a project can be provisioned by checking requester authority, verified institution/profile evidence, fresh MFA, required launch metadata, template controls, visibility rules by data classification, initial project roles, object-level grants, external collaborator constraints, and immutable audit evidence.
+
+## Run
+
+```sh
+node project-provisioning-baseline-guard/test.js
+node project-provisioning-baseline-guard/demo.js
+node project-provisioning-baseline-guard/render-video.js
+```
+
+The demo writes JSON, Markdown, and SVG reviewer artifacts to `project-provisioning-baseline-guard/reports/`.
+
+## Review Surface
+
+The implementation is dependency-free, uses synthetic data only, and does not call external APIs or read credentials.
diff --git a/project-provisioning-baseline-guard/acceptance-notes.md b/project-provisioning-baseline-guard/acceptance-notes.md
new file mode 100644
index 00000000..db2e2385
--- /dev/null
+++ b/project-provisioning-baseline-guard/acceptance-notes.md
@@ -0,0 +1,21 @@
+# Acceptance Notes
+
+## Validation
+
+- `node project-provisioning-baseline-guard/test.js`
+- `node project-provisioning-baseline-guard/demo.js`
+- `node project-provisioning-baseline-guard/render-video.js`
+- `node --check project-provisioning-baseline-guard/index.js`
+- `node --check project-provisioning-baseline-guard/test.js`
+- `node --check project-provisioning-baseline-guard/demo.js`
+- `node --check project-provisioning-baseline-guard/render-video.js`
+- `ffprobe -v error -show_entries format=duration,size -show_entries stream=codec_name,width,height -of default=noprint_wrappers=1 project-provisioning-baseline-guard/demo.mp4`
+
+## Acceptance Coverage
+
+- A controlled institutional project with fresh requester MFA, verified affiliation, required metadata, template controls, owner and data-steward roles, scoped external collaboration, and audit events can provision cleanly.
+- Restricted human-subject projects cannot start with public visibility.
+- Missing requester authority and stale MFA block provisioning.
+- External restricted-data grants require data-use agreement evidence.
+- Missing template controls block workspace creation.
+- The output audit digest is deterministic for replay and reviewer comparison.
diff --git a/project-provisioning-baseline-guard/demo.js b/project-provisioning-baseline-guard/demo.js
new file mode 100644
index 00000000..f6b5b27a
--- /dev/null
+++ b/project-provisioning-baseline-guard/demo.js
@@ -0,0 +1,171 @@
+const fs = require("fs");
+const path = require("path");
+const { evaluateProjectProvisioning } = require("./index");
+
+const outputDir = path.join(__dirname, "reports");
+fs.mkdirSync(outputDir, { recursive: true });
+
+function packetForDemo() {
+ return {
+ now: "2026-06-01T12:00:00Z",
+ policy: {
+ maxMfaAgeHours: 12,
+ requiredMetadataFields: ["projectName", "projectPurpose", "discipline", "institutionId", "dataClassification"],
+ requiredProjectRoles: ["owner"],
+ allowedExternalDomains: ["partner-lab.org"],
+ visibilityByClassification: {
+ open: ["public", "institutional", "private"],
+ controlled: ["institutional", "private"],
+ "restricted-human-subjects": ["private"],
+ },
+ templates: {
+ "controlled-study": {
+ id: "controlled-study",
+ allowedClassifications: ["controlled", "restricted-human-subjects"],
+ requiredMetadataFields: ["retentionPlan"],
+ requiredProjectRoles: ["data-steward"],
+ requiredControls: ["auditTrail", "objectGrantReview"],
+ },
+ },
+ },
+ users: [
+ {
+ id: "user-pi-ada",
+ type: "internal",
+ projectCreator: true,
+ email: "ada@northbridge.edu",
+ mfaAt: "2026-06-01T08:00:00Z",
+ verifiedAffiliations: [{ institutionId: "northbridge", status: "verified" }],
+ identifiers: [{ type: "orcid", value: "0000-0002-1825-0097", status: "verified" }],
+ },
+ {
+ id: "user-steward-lin",
+ type: "internal",
+ email: "lin@northbridge.edu",
+ verifiedAffiliations: [{ institutionId: "northbridge", status: "verified" }],
+ identifiers: [{ type: "orcid", value: "0000-0001-5555-1212", status: "verified" }],
+ },
+ {
+ id: "user-ext-ren",
+ type: "external",
+ email: "ren@partner-lab.org",
+ training: { restrictedData: "current" },
+ verifiedAffiliations: [{ institutionId: "partner-lab", status: "verified" }],
+ identifiers: [{ type: "orcid", value: "0000-0003-9999-8888", status: "verified" }],
+ },
+ ],
+ request: {
+ requestId: "provision-1",
+ projectId: "project-neuro-qc",
+ name: "Neuro QC Consortium",
+ requesterId: "user-pi-ada",
+ institutionId: "northbridge",
+ templateId: "controlled-study",
+ dataClassification: "controlled",
+ visibility: "institutional",
+ metadata: {
+ projectName: "Neuro QC Consortium",
+ projectPurpose: "Coordinate reproducible quality-control notebooks for multi-site neuroimaging studies.",
+ discipline: "neuroscience",
+ institutionId: "northbridge",
+ dataClassification: "controlled",
+ retentionPlan: "retain-seven-years",
+ },
+ controls: { auditTrail: true, objectGrantReview: true },
+ roleAssignments: [
+ { userId: "user-pi-ada", role: "owner", scope: "project" },
+ { userId: "user-steward-lin", role: "data-steward", scope: "project" },
+ { userId: "user-ext-ren", role: "viewer", scope: "project" },
+ ],
+ objectGrants: [
+ { principalId: "user-pi-ada", objectType: "workspace", permissions: ["admin"] },
+ { principalId: "user-steward-lin", objectType: "controlled-dataset", permissions: ["read", "review"] },
+ { principalId: "user-ext-ren", objectType: "manuscript", permissions: ["read"] },
+ ],
+ externalCollaborators: ["user-ext-ren"],
+ approvals: [{ type: "sponsor-review", status: "approved", subjectId: "project-neuro-qc" }],
+ auditEvents: [
+ { type: "provision-requested", actorId: "user-pi-ada", at: "2026-06-01T08:03:00Z" },
+ { type: "baseline-evaluated", actorId: "system", at: "2026-06-01T08:04:00Z" },
+ ],
+ },
+ };
+}
+
+function markdownReport(report) {
+ return [
+ "# Project Provisioning Baseline Guard Demo",
+ "",
+ `Decision: ${report.decision}`,
+ `Audit digest: ${report.auditDigest}`,
+ "",
+ "## Public Summary",
+ "",
+ `- Project: ${report.publicSummary.projectName} (${report.projectId})`,
+ `- Template: ${report.publicSummary.templateId}`,
+ `- Visibility: ${report.publicSummary.visibility}`,
+ `- Data classification: ${report.publicSummary.dataClassification}`,
+ "",
+ "## Finding Counts",
+ "",
+ `- Blockers: ${report.counts.blocker}`,
+ `- Warnings: ${report.counts.warning}`,
+ `- Info: ${report.counts.info}`,
+ "",
+ "## Action Queue",
+ "",
+ ...(report.actionQueue.length
+ ? report.actionQueue.map((action) => `- ${action.severity} ${action.code} (${action.subject}): ${action.remediation}`)
+ : ["- No remediation actions required."]),
+ "",
+ ].join("\n");
+}
+
+function svgReport(report) {
+ const blocked = report.counts.blocker;
+ const warnings = report.counts.warning;
+ const readyColor = report.decision === "provision-ready" ? "#68d391" : "#f6ad55";
+
+ return `
+`;
+}
+
+const report = evaluateProjectProvisioning(packetForDemo());
+const jsonPath = path.join(outputDir, "provisioning-baseline-packet.json");
+const markdownPath = path.join(outputDir, "provisioning-baseline-report.md");
+const svgPath = path.join(outputDir, "summary.svg");
+
+fs.writeFileSync(jsonPath, JSON.stringify(report, null, 2));
+fs.writeFileSync(markdownPath, markdownReport(report));
+fs.writeFileSync(svgPath, svgReport(report));
+
+console.log(`Wrote ${jsonPath}`);
+console.log(`Wrote ${markdownPath}`);
+console.log(`Wrote ${svgPath}`);
+console.log(`${report.decision}: ${report.counts.blocker} blocker(s), ${report.counts.warning} warning(s), ${report.auditDigest}`);
diff --git a/project-provisioning-baseline-guard/demo.mp4 b/project-provisioning-baseline-guard/demo.mp4
new file mode 100644
index 00000000..39fd8b1e
Binary files /dev/null and b/project-provisioning-baseline-guard/demo.mp4 differ
diff --git a/project-provisioning-baseline-guard/demo.svg b/project-provisioning-baseline-guard/demo.svg
new file mode 100644
index 00000000..15ce3242
--- /dev/null
+++ b/project-provisioning-baseline-guard/demo.svg
@@ -0,0 +1,27 @@
+
diff --git a/project-provisioning-baseline-guard/index.js b/project-provisioning-baseline-guard/index.js
new file mode 100644
index 00000000..e1328286
--- /dev/null
+++ b/project-provisioning-baseline-guard/index.js
@@ -0,0 +1,484 @@
+const crypto = require("crypto");
+
+const HOUR_MS = 60 * 60 * 1000;
+
+function asArray(value) {
+ if (!value) return [];
+ return Array.isArray(value) ? value : [value];
+}
+
+function normalize(value) {
+ return String(value || "").trim().toLowerCase();
+}
+
+function unique(values) {
+ return [...new Set(asArray(values).map(normalize).filter(Boolean))];
+}
+
+function uniqueFields(values) {
+ const seen = new Set();
+ const fields = [];
+ for (const value of asArray(values).filter(Boolean)) {
+ const key = normalize(value);
+ if (seen.has(key)) continue;
+ seen.add(key);
+ fields.push(value);
+ }
+ return fields;
+}
+
+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 parseDate(value) {
+ const time = Date.parse(value || "");
+ return Number.isNaN(time) ? 0 : time;
+}
+
+function hoursSince(value, now) {
+ const time = parseDate(value);
+ if (!time || !now) return Infinity;
+ return Math.floor((now - time) / HOUR_MS);
+}
+
+function addFinding(findings, severity, code, subject, message, remediation) {
+ findings.push({ severity, code, subject, message, remediation });
+}
+
+function byId(records) {
+ return asArray(records).reduce((acc, record) => {
+ if (record && record.id) acc[record.id] = record;
+ return acc;
+ }, {});
+}
+
+function hasVerifiedAffiliation(user, institutionId) {
+ const target = normalize(institutionId);
+ return asArray(user.verifiedAffiliations).some((affiliation) => {
+ if (normalize(affiliation.institutionId || affiliation.id) !== target) return false;
+ return normalize(affiliation.status || "verified") === "verified";
+ });
+}
+
+function hasVerifiedIdentifier(user, type) {
+ return asArray(user.identifiers).some(
+ (identifier) => normalize(identifier.type) === normalize(type) && normalize(identifier.status || "verified") === "verified",
+ );
+}
+
+function roleAssignmentsFor(assignments, userId) {
+ const target = normalize(userId);
+ return asArray(assignments).filter((assignment) => normalize(assignment.userId) === target);
+}
+
+function hasAnyProjectRole(assignments, roles) {
+ const wanted = unique(roles);
+ return asArray(assignments).some((assignment) => wanted.includes(normalize(assignment.role)));
+}
+
+function approvalExists(approvals, type, subjectId) {
+ const targetType = normalize(type);
+ const targetSubject = normalize(subjectId);
+ return asArray(approvals).some((approval) => {
+ if (normalize(approval.type) !== targetType) return false;
+ if (targetSubject && normalize(approval.subjectId || approval.userId || approval.projectId) !== targetSubject) return false;
+ return normalize(approval.status || "approved") === "approved";
+ });
+}
+
+function expectedMetadataFields(policy, request, template) {
+ const required = [
+ ...asArray(policy.requiredMetadataFields),
+ ...asArray(template.requiredMetadataFields),
+ ];
+
+ if (normalize(request.dataClassification).includes("restricted")) {
+ required.push("irbProtocolId", "dataUseAgreementId", "retentionPlan");
+ }
+
+ return uniqueFields(required);
+}
+
+function readField(record, field) {
+ const target = normalize(field);
+ for (const [key, value] of Object.entries(record || {})) {
+ if (normalize(key) === target) return value;
+ }
+ return undefined;
+}
+
+function visibilityAllowed(policy, request) {
+ const classification = normalize(request.dataClassification);
+ const visibility = normalize(request.visibility);
+ const allowedByClass = policy.visibilityByClassification || {};
+ const allowed = unique(allowedByClass[classification] || policy.allowedVisibilities || []);
+
+ if (allowed.length === 0) return true;
+ return allowed.includes(visibility);
+}
+
+function grantTouchesRestrictedData(grant) {
+ const objectType = normalize(grant.objectType || grant.resourceType);
+ const permissions = unique(grant.permissions || grant.permission);
+ return objectType.includes("restricted") || objectType.includes("human-subject") || permissions.some((permission) => ["download", "export", "share"].includes(permission));
+}
+
+function publicProvisioningSummary(request, decision, counts) {
+ return {
+ projectId: request.projectId,
+ projectName: request.name,
+ templateId: request.templateId,
+ visibility: request.visibility,
+ dataClassification: request.dataClassification,
+ decision,
+ findingCounts: counts,
+ };
+}
+
+function evaluateRequester(request, usersById, policy, findings, now) {
+ const requester = usersById[request.requesterId] || {};
+ const requesterAssignments = roleAssignmentsFor(request.roleAssignments, request.requesterId);
+ const authorityRoles = policy.creatorRoles || ["principal-investigator", "project-admin", "institution-admin"];
+ const maxMfaAgeHours = Number(policy.maxMfaAgeHours || 24);
+
+ if (!requester.id) {
+ addFinding(
+ findings,
+ "blocker",
+ "REQUESTER_PROFILE_MISSING",
+ request.requesterId || "requester",
+ "Project provisioning requires a resolvable requester profile.",
+ "Load the requester profile before creating the workspace.",
+ );
+ return;
+ }
+
+ if (!requester.projectCreator && !hasAnyProjectRole(requesterAssignments, authorityRoles)) {
+ addFinding(
+ findings,
+ "blocker",
+ "REQUESTER_AUTHORITY_MISSING",
+ requester.id,
+ "Requester lacks a creator flag or approved project creation role.",
+ "Require a principal investigator, project admin, institution admin, or explicit projectCreator grant.",
+ );
+ }
+
+ if (!hasVerifiedAffiliation(requester, request.institutionId)) {
+ addFinding(
+ findings,
+ "blocker",
+ "REQUESTER_AFFILIATION_UNVERIFIED",
+ requester.id,
+ "Requester does not have a verified affiliation for the target institution.",
+ "Verify institutional SAML, domain, or administrator evidence before provisioning.",
+ );
+ }
+
+ if (!hasVerifiedIdentifier(requester, "orcid")) {
+ addFinding(
+ findings,
+ "warning",
+ "REQUESTER_ORCID_UNVERIFIED",
+ requester.id,
+ "Requester profile lacks a verified ORCID identifier.",
+ "Allow steward review or require ORCID linkage before public researcher profile exposure.",
+ );
+ }
+
+ if (hoursSince(requester.mfaAt, now) > maxMfaAgeHours) {
+ addFinding(
+ findings,
+ "blocker",
+ "REQUESTER_MFA_STALE",
+ requester.id,
+ `Requester MFA is older than ${maxMfaAgeHours} hour(s).`,
+ "Require a fresh MFA challenge before opening the project space.",
+ );
+ }
+}
+
+function evaluateMetadata(request, policy, template, findings) {
+ const metadata = request.metadata || {};
+ for (const field of expectedMetadataFields(policy, request, template)) {
+ const value = readField(metadata, field) || readField(request, field);
+ if (!value) {
+ addFinding(
+ findings,
+ "blocker",
+ "REQUIRED_METADATA_MISSING",
+ field,
+ `Required project provisioning metadata is missing: ${field}.`,
+ "Collect the missing metadata before the workspace is created.",
+ );
+ }
+ }
+
+ const projectName = String(request.name || metadata.projectName || "");
+ if (projectName.trim().length < Number(policy.minimumProjectNameLength || 6)) {
+ addFinding(
+ findings,
+ "warning",
+ "PROJECT_NAME_TOO_SHORT",
+ request.projectId || "project",
+ "Project name is too short for reviewer and audit records.",
+ "Require a descriptive title before showing the workspace in project lists.",
+ );
+ }
+}
+
+function evaluateVisibility(request, policy, findings) {
+ if (!visibilityAllowed(policy, request)) {
+ addFinding(
+ findings,
+ "blocker",
+ "VISIBILITY_CLASSIFICATION_CONFLICT",
+ request.visibility,
+ `Visibility ${request.visibility} is not allowed for ${request.dataClassification} projects.`,
+ "Lower visibility or reclassify the project before provisioning.",
+ );
+ }
+
+ if (normalize(request.visibility) === "public" && asArray(request.externalCollaborators).length > 0) {
+ addFinding(
+ findings,
+ "info",
+ "PUBLIC_PROJECT_EXTERNAL_COLLABORATORS",
+ request.projectId || "project",
+ "Public project includes external collaborators at creation time.",
+ "Keep object-level grants narrow and retain collaborator audit evidence.",
+ );
+ }
+}
+
+function evaluateTemplate(request, policy, findings) {
+ const template = (policy.templates || {})[request.templateId] || {};
+ if (!template.id && request.templateId) {
+ addFinding(
+ findings,
+ "warning",
+ "UNKNOWN_TEMPLATE_ID",
+ request.templateId,
+ "Project references a template that is not registered in the provisioning policy.",
+ "Route to steward review or register the template baseline before launch.",
+ );
+ }
+
+ const allowedClassifications = unique(template.allowedClassifications);
+ if (allowedClassifications.length > 0 && !allowedClassifications.includes(normalize(request.dataClassification))) {
+ addFinding(
+ findings,
+ "blocker",
+ "TEMPLATE_CLASSIFICATION_MISMATCH",
+ request.templateId,
+ "Selected project template is not approved for this data classification.",
+ "Choose an approved template or add steward approval for the classification.",
+ );
+ }
+
+ for (const control of asArray(template.requiredControls)) {
+ if (!request.controls || request.controls[control] !== true) {
+ addFinding(
+ findings,
+ "blocker",
+ "TEMPLATE_CONTROL_MISSING",
+ control,
+ `Template control ${control} has not been enabled.`,
+ "Enable the required template control before creating the workspace.",
+ );
+ }
+ }
+
+ return template;
+}
+
+function evaluateInitialRoles(request, policy, template, usersById, findings) {
+ const roleAssignments = asArray(request.roleAssignments);
+ const requiredRoles = unique([...(policy.requiredProjectRoles || ["owner"]), ...asArray(template.requiredProjectRoles)]);
+
+ for (const role of requiredRoles) {
+ if (!hasAnyProjectRole(roleAssignments, [role])) {
+ addFinding(
+ findings,
+ "blocker",
+ "REQUIRED_PROJECT_ROLE_MISSING",
+ role,
+ `Project is missing required initial role: ${role}.`,
+ "Add the required role assignment before provisioning.",
+ );
+ }
+ }
+
+ for (const assignment of roleAssignments) {
+ const user = usersById[assignment.userId] || {};
+ if (!user.id) {
+ addFinding(
+ findings,
+ "blocker",
+ "ROLE_ASSIGNEE_MISSING",
+ assignment.userId || "unknown",
+ "Initial role assignment references a missing user profile.",
+ "Resolve every role assignee before committing the role matrix.",
+ );
+ continue;
+ }
+
+ if (normalize(user.type) === "external" && ["owner", "project-admin", "data-steward"].includes(normalize(assignment.role))) {
+ addFinding(
+ findings,
+ "blocker",
+ "EXTERNAL_COLLABORATOR_PRIVILEGED_ROLE",
+ user.id,
+ "External collaborator is assigned a privileged launch role.",
+ "Use a viewer, contributor, or scoped collaborator role until sponsor approval is recorded.",
+ );
+ }
+ }
+}
+
+function evaluateExternalCollaborators(request, policy, usersById, findings) {
+ const approvals = asArray(request.approvals);
+ const objectGrants = asArray(request.objectGrants);
+ const allowedDomains = unique(policy.allowedExternalDomains);
+
+ for (const collaboratorRef of asArray(request.externalCollaborators)) {
+ const collaboratorId = typeof collaboratorRef === "string" ? collaboratorRef : collaboratorRef.userId || collaboratorRef.id;
+ const collaborator = usersById[collaboratorId] || {};
+ if (!collaborator.id) {
+ addFinding(
+ findings,
+ "blocker",
+ "EXTERNAL_COLLABORATOR_PROFILE_MISSING",
+ collaboratorId,
+ "External collaborator profile is missing.",
+ "Invite or verify the collaborator profile before provisioning.",
+ );
+ continue;
+ }
+
+ const emailDomain = normalize(collaborator.email || "").split("@")[1] || normalize(collaborator.domain);
+ if (allowedDomains.length > 0 && !allowedDomains.includes(emailDomain)) {
+ addFinding(
+ findings,
+ "warning",
+ "EXTERNAL_DOMAIN_NOT_ALLOWLISTED",
+ collaborator.id,
+ `External collaborator domain ${emailDomain || "unknown"} is not allowlisted.`,
+ "Require sponsor review before retaining this collaborator at project launch.",
+ );
+ }
+
+ const restrictedGrant = objectGrants.some(
+ (grant) => normalize(grant.principalId || grant.userId) === normalize(collaborator.id) && grantTouchesRestrictedData(grant),
+ );
+ if (restrictedGrant && !approvalExists(approvals, "data-use-agreement", collaborator.id)) {
+ addFinding(
+ findings,
+ "blocker",
+ "EXTERNAL_RESTRICTED_DATA_APPROVAL_MISSING",
+ collaborator.id,
+ "External collaborator has a restricted data grant without data-use agreement approval.",
+ "Add data-use agreement evidence or remove the restricted grant.",
+ );
+ }
+
+ const hasCurrentRestrictedTraining = collaborator.training && normalize(collaborator.training.restrictedData) === "current";
+ if (restrictedGrant && !hasCurrentRestrictedTraining) {
+ addFinding(
+ findings,
+ "warning",
+ "EXTERNAL_RESTRICTED_DATA_TRAINING_GAP",
+ collaborator.id,
+ "External collaborator restricted-data training is missing or stale.",
+ "Require current training evidence before enabling restricted data access.",
+ );
+ }
+ }
+}
+
+function evaluateAuditEvidence(request, findings) {
+ const eventTypes = unique(asArray(request.auditEvents).map((event) => event.type));
+ for (const expected of ["provision-requested", "baseline-evaluated"]) {
+ if (!eventTypes.includes(expected)) {
+ addFinding(
+ findings,
+ "warning",
+ "AUDIT_EVENT_MISSING",
+ expected,
+ `Audit event ${expected} is missing from the provisioning packet.`,
+ "Record immutable launch evidence before approving the workspace.",
+ );
+ }
+ }
+}
+
+function findingCounts(findings) {
+ return findings.reduce(
+ (acc, finding) => {
+ acc[finding.severity] = (acc[finding.severity] || 0) + 1;
+ return acc;
+ },
+ { blocker: 0, warning: 0, info: 0 },
+ );
+}
+
+function evaluateProjectProvisioning(packet = {}) {
+ const policy = packet.policy || {};
+ const request = packet.request || {};
+ const usersById = byId(packet.users);
+ const now = parseDate(packet.now || new Date().toISOString());
+ const findings = [];
+ const template = evaluateTemplate(request, policy, findings);
+
+ evaluateRequester(request, usersById, policy, findings, now);
+ evaluateMetadata(request, policy, template, findings);
+ evaluateVisibility(request, policy, findings);
+ evaluateInitialRoles(request, policy, template, usersById, findings);
+ evaluateExternalCollaborators(request, policy, usersById, findings);
+ evaluateAuditEvidence(request, findings);
+
+ const counts = findingCounts(findings);
+ const decision =
+ counts.blocker > 0
+ ? "hold-provisioning"
+ : counts.warning > 0
+ ? "provision-with-steward-review"
+ : "provision-ready";
+ const actionQueue = findings.map((finding) => ({
+ severity: finding.severity,
+ code: finding.code,
+ subject: finding.subject,
+ remediation: finding.remediation,
+ }));
+ const result = {
+ generatedAt: packet.now || new Date().toISOString(),
+ decision,
+ projectId: request.projectId,
+ counts,
+ findings,
+ actionQueue,
+ publicSummary: publicProvisioningSummary(request, decision, counts),
+ };
+
+ return {
+ ...result,
+ auditDigest: digest(result),
+ };
+}
+
+module.exports = {
+ evaluateProjectProvisioning,
+ stableStringify,
+};
diff --git a/project-provisioning-baseline-guard/render-video.js b/project-provisioning-baseline-guard/render-video.js
new file mode 100644
index 00000000..824091d8
--- /dev/null
+++ b/project-provisioning-baseline-guard/render-video.js
@@ -0,0 +1,151 @@
+const childProcess = require("child_process");
+const fs = require("fs");
+const path = require("path");
+
+const frames = [
+ ["Problem", "New research workspaces can start with weak requester evidence, unsafe visibility, missing metadata, and overbroad initial grants."],
+ ["Implementation", "The guard validates identity posture, template controls, metadata, visibility policy, initial roles, object grants, and audit events."],
+ ["Acceptance", "Unsafe launch packets are held when restricted data, external collaborators, missing controls, or stale MFA create project risk."],
+ ["Demo output", "decision: provision-ready | blockers: 0 | actionQueue: empty | auditDigest: deterministic"],
+];
+
+function ffmpegCandidates() {
+ const localAppData = process.env.LOCALAPPDATA;
+ const candidates = [process.env.FFMPEG_PATH, "ffmpeg.exe", "ffmpeg"].filter(Boolean);
+
+ if (localAppData) {
+ candidates.unshift(
+ path.join(
+ localAppData,
+ "Microsoft",
+ "WinGet",
+ "Packages",
+ "Gyan.FFmpeg.Essentials_Microsoft.Winget.Source_8wekyb3d8bbwe",
+ "ffmpeg-8.1.1-essentials_build",
+ "bin",
+ "ffmpeg.exe",
+ ),
+ );
+ }
+
+ return candidates;
+}
+
+function findFfmpeg() {
+ for (const candidate of ffmpegCandidates()) {
+ if (candidate.includes(path.sep) && fs.existsSync(candidate)) return candidate;
+ try {
+ childProcess.execFileSync(candidate, ["-version"], { stdio: "ignore" });
+ return candidate;
+ } catch {
+ // Try the next candidate.
+ }
+ }
+
+ throw new Error("FFmpeg was not found. Install it or set FFMPEG_PATH.");
+}
+
+function quoteText(text) {
+ return String(text)
+ .replace(/\\/g, "\\\\")
+ .replace(/:/g, "\\:")
+ .replace(/'/g, "\\'")
+ .replace(/%/g, "\\%");
+}
+
+function drawText(text, x, y, size, color, weight = "") {
+ const style = weight ? `:font=${weight}` : "";
+ return `drawtext=text='${quoteText(text)}'${style}:x=${x}:y=${y}:fontsize=${size}:fontcolor=${color}`;
+}
+
+function wrapText(text, maxChars) {
+ const words = String(text).split(/\s+/);
+ const lines = [];
+ let line = "";
+
+ for (const word of words) {
+ const next = line ? `${line} ${word}` : word;
+ if (next.length > maxChars && line) {
+ lines.push(line);
+ line = word;
+ } else {
+ line = next;
+ }
+ }
+
+ if (line) lines.push(line);
+ return lines;
+}
+
+function textBlock(text, x, y, size, color, maxChars, lineHeight) {
+ return wrapText(text, maxChars).map((line, index) => drawText(line, x, y + index * lineHeight, size, color));
+}
+
+function slideFilter(frame, slideIndex, inputLabel) {
+ const filters = [];
+ const label = frame[0].toUpperCase();
+ const body = frame[1];
+
+ filters.push("drawbox=x=0:y=0:w=iw:h=ih:color=0d1726@1:t=fill");
+ filters.push("drawbox=x=0:y=0:w=iw:h=130:color=10243a@1:t=fill");
+ filters.push("drawbox=x=64:y=214:w=540:h=318:color=ffffff@0.93:t=fill");
+ filters.push("drawbox=x=676:y=214:w=540:h=318:color=07101c@0.84:t=fill");
+ filters.push(drawText("SCIBASE bounty demo artifact", 64, 44, 22, "d8e8f7"));
+ filters.push(drawText("Project Provisioning Baseline Guard", 64, 92, 42, "ffffff"));
+ filters.push(drawText("Issue #11: user and project management", 66, 150, 24, "b8cbe1"));
+ filters.push(drawText(label, 96, 250, 18, "16405f"));
+ filters.push(...textBlock(body, 96, 302, 23, "152036", 34, 34));
+ filters.push(drawText("$ node project-provisioning-baseline-guard/test.js", 708, 254, 18, "68d391"));
+ filters.push(...textBlock("tests passed: requester evidence, metadata, visibility, external grants, template controls, deterministic digest", 708, 304, 19, "e9f7ef", 40, 28));
+ filters.push(drawText("Validation path", 708, 404, 18, "a8bdd1"));
+ filters.push(drawText("1. Focused tests", 708, 438, 21, "ffffff"));
+ filters.push(drawText("2. Demo JSON and Markdown", 708, 468, 21, "ffffff"));
+ filters.push(drawText("3. Requirement map and acceptance notes", 708, 498, 21, "ffffff"));
+
+ for (let index = 0; index < frames.length; index += 1) {
+ const color = index === slideIndex ? "68d391@1" : "ffffff@0.30";
+ filters.push(`drawbox=x=${64 + index * 54}:y=590:w=38:h=8:color=${color}:t=fill`);
+ }
+
+ filters.push(drawText("Committed video demo + focused tests + synthetic evidence", 64, 650, 21, "d8e8f7"));
+ filters.push(drawText(`Slide ${slideIndex + 1} / ${frames.length}`, 1096, 650, 18, "b8cbe1"));
+
+ return `${inputLabel}${filters.join(",")}[v${slideIndex}]`;
+}
+
+function renderDemo(ffmpeg, outputPath) {
+ const args = ["-y"];
+ const slideDuration = "2.1";
+
+ for (let index = 0; index < frames.length; index += 1) {
+ args.push("-f", "lavfi", "-t", slideDuration, "-i", "color=c=0d1726:s=1280x720:r=30");
+ }
+
+ const slideFilters = frames.map((frame, index) => slideFilter(frame, index, `[${index}:v]`));
+ const concatInputs = frames.map((_, index) => `[v${index}]`).join("");
+ const filterComplex = `${slideFilters.join(";")};${concatInputs}concat=n=${frames.length}:v=1:a=0[outv]`;
+
+ args.push(
+ "-filter_complex",
+ filterComplex,
+ "-map",
+ "[outv]",
+ "-c:v",
+ "libx264",
+ "-preset",
+ "veryfast",
+ "-crf",
+ "23",
+ "-pix_fmt",
+ "yuv420p",
+ "-movflags",
+ "+faststart",
+ outputPath,
+ );
+
+ childProcess.execFileSync(ffmpeg, args, { stdio: "inherit" });
+}
+
+const outputPath = path.join(__dirname, "demo.mp4");
+renderDemo(findFfmpeg(), outputPath);
+console.log(`Wrote ${outputPath} (${fs.statSync(outputPath).size} bytes)`);
diff --git a/project-provisioning-baseline-guard/reports/provisioning-baseline-packet.json b/project-provisioning-baseline-guard/reports/provisioning-baseline-packet.json
new file mode 100644
index 00000000..71a72be0
--- /dev/null
+++ b/project-provisioning-baseline-guard/reports/provisioning-baseline-packet.json
@@ -0,0 +1,26 @@
+{
+ "generatedAt": "2026-06-01T12:00:00Z",
+ "decision": "provision-ready",
+ "projectId": "project-neuro-qc",
+ "counts": {
+ "blocker": 0,
+ "warning": 0,
+ "info": 0
+ },
+ "findings": [],
+ "actionQueue": [],
+ "publicSummary": {
+ "projectId": "project-neuro-qc",
+ "projectName": "Neuro QC Consortium",
+ "templateId": "controlled-study",
+ "visibility": "institutional",
+ "dataClassification": "controlled",
+ "decision": "provision-ready",
+ "findingCounts": {
+ "blocker": 0,
+ "warning": 0,
+ "info": 0
+ }
+ },
+ "auditDigest": "4d892fa7ef881f2261ac7b2c22774bc59439d85b74ed1a3688af60be7c04b083"
+}
\ No newline at end of file
diff --git a/project-provisioning-baseline-guard/reports/provisioning-baseline-report.md b/project-provisioning-baseline-guard/reports/provisioning-baseline-report.md
new file mode 100644
index 00000000..169d2262
--- /dev/null
+++ b/project-provisioning-baseline-guard/reports/provisioning-baseline-report.md
@@ -0,0 +1,21 @@
+# Project Provisioning Baseline Guard Demo
+
+Decision: provision-ready
+Audit digest: 4d892fa7ef881f2261ac7b2c22774bc59439d85b74ed1a3688af60be7c04b083
+
+## Public Summary
+
+- Project: Neuro QC Consortium (project-neuro-qc)
+- Template: controlled-study
+- Visibility: institutional
+- Data classification: controlled
+
+## Finding Counts
+
+- Blockers: 0
+- Warnings: 0
+- Info: 0
+
+## Action Queue
+
+- No remediation actions required.
diff --git a/project-provisioning-baseline-guard/reports/summary.svg b/project-provisioning-baseline-guard/reports/summary.svg
new file mode 100644
index 00000000..b37ea547
--- /dev/null
+++ b/project-provisioning-baseline-guard/reports/summary.svg
@@ -0,0 +1,27 @@
+
diff --git a/project-provisioning-baseline-guard/requirements-map.md b/project-provisioning-baseline-guard/requirements-map.md
new file mode 100644
index 00000000..cb44ca6c
--- /dev/null
+++ b/project-provisioning-baseline-guard/requirements-map.md
@@ -0,0 +1,17 @@
+# Requirements Map
+
+Issue #11 asks for User & Project Management features, including secure authentication, user profiles, project workspaces, role-based access, collaboration, and audit trails.
+
+This submission focuses on a distinct project provisioning baseline lane:
+
+- Verifies that the project requester has creation authority, institutional affiliation evidence, ORCID linkage, and fresh MFA.
+- Checks required project metadata before a new workspace appears in user project lists.
+- Enforces visibility constraints by data classification so restricted projects cannot start public.
+- Validates registered project templates and their required launch controls.
+- Requires owner and data-steward role coverage for controlled research projects.
+- Blocks privileged launch roles for external collaborators.
+- Requires data-use agreement evidence before external collaborators can receive restricted data export grants.
+- Preserves immutable provisioning audit events and emits deterministic audit digests.
+- Produces public reviewer summaries without exposing private collaborator evidence.
+
+The scope intentionally avoids the existing broad RBAC/workspace ledger, privacy access review, member lifecycle/offboarding, institutional recertification, anonymous-review escrow, identity merge/export, data-room consent, researcher profile sync, archive handoff, access-audit anomaly, role delegation, invitation-domain/MFA, funding-attribution, service-token governance, deletion/erasure, break-glass, and visibility-transition submissions.
diff --git a/project-provisioning-baseline-guard/test.js b/project-provisioning-baseline-guard/test.js
new file mode 100644
index 00000000..79b85adc
--- /dev/null
+++ b/project-provisioning-baseline-guard/test.js
@@ -0,0 +1,197 @@
+const assert = require("assert");
+const { evaluateProjectProvisioning } = require("./index");
+
+function basePacket(overrides = {}) {
+ const packet = {
+ now: "2026-06-01T12:00:00Z",
+ policy: {
+ maxMfaAgeHours: 12,
+ minimumProjectNameLength: 6,
+ requiredMetadataFields: ["projectName", "projectPurpose", "discipline", "institutionId", "dataClassification"],
+ requiredProjectRoles: ["owner"],
+ creatorRoles: ["principal-investigator", "project-admin", "institution-admin"],
+ allowedExternalDomains: ["partner-lab.org"],
+ visibilityByClassification: {
+ open: ["public", "institutional", "private"],
+ controlled: ["institutional", "private"],
+ "restricted-human-subjects": ["private"],
+ },
+ templates: {
+ "controlled-study": {
+ id: "controlled-study",
+ allowedClassifications: ["controlled", "restricted-human-subjects"],
+ requiredMetadataFields: ["retentionPlan"],
+ requiredProjectRoles: ["data-steward"],
+ requiredControls: ["auditTrail", "objectGrantReview"],
+ },
+ },
+ },
+ users: [
+ {
+ id: "user-pi-ada",
+ type: "internal",
+ projectCreator: true,
+ email: "ada@northbridge.edu",
+ mfaAt: "2026-06-01T08:00:00Z",
+ verifiedAffiliations: [{ institutionId: "northbridge", status: "verified" }],
+ identifiers: [{ type: "orcid", value: "0000-0002-1825-0097", status: "verified" }],
+ },
+ {
+ id: "user-steward-lin",
+ type: "internal",
+ email: "lin@northbridge.edu",
+ verifiedAffiliations: [{ institutionId: "northbridge", status: "verified" }],
+ identifiers: [{ type: "orcid", value: "0000-0001-5555-1212", status: "verified" }],
+ },
+ {
+ id: "user-ext-ren",
+ type: "external",
+ email: "ren@partner-lab.org",
+ training: { restrictedData: "current" },
+ verifiedAffiliations: [{ institutionId: "partner-lab", status: "verified" }],
+ identifiers: [{ type: "orcid", value: "0000-0003-9999-8888", status: "verified" }],
+ },
+ ],
+ request: {
+ requestId: "provision-1",
+ projectId: "project-neuro-qc",
+ name: "Neuro QC Consortium",
+ requesterId: "user-pi-ada",
+ institutionId: "northbridge",
+ templateId: "controlled-study",
+ dataClassification: "controlled",
+ visibility: "institutional",
+ metadata: {
+ projectName: "Neuro QC Consortium",
+ projectPurpose: "Coordinate reproducible quality-control notebooks for multi-site neuroimaging studies.",
+ discipline: "neuroscience",
+ institutionId: "northbridge",
+ dataClassification: "controlled",
+ retentionPlan: "retain-seven-years",
+ },
+ controls: {
+ auditTrail: true,
+ objectGrantReview: true,
+ },
+ roleAssignments: [
+ { userId: "user-pi-ada", role: "owner", scope: "project" },
+ { userId: "user-steward-lin", role: "data-steward", scope: "project" },
+ { userId: "user-ext-ren", role: "viewer", scope: "project" },
+ ],
+ objectGrants: [
+ { principalId: "user-pi-ada", objectType: "workspace", permissions: ["admin"] },
+ { principalId: "user-steward-lin", objectType: "controlled-dataset", permissions: ["read", "review"] },
+ { principalId: "user-ext-ren", objectType: "manuscript", permissions: ["read"] },
+ ],
+ externalCollaborators: ["user-ext-ren"],
+ approvals: [{ type: "sponsor-review", status: "approved", subjectId: "project-neuro-qc" }],
+ auditEvents: [
+ { type: "provision-requested", actorId: "user-pi-ada", at: "2026-06-01T08:03:00Z" },
+ { type: "baseline-evaluated", actorId: "system", at: "2026-06-01T08:04:00Z" },
+ ],
+ },
+ };
+
+ return {
+ ...packet,
+ ...overrides,
+ policy: { ...packet.policy, ...(overrides.policy || {}) },
+ request: { ...packet.request, ...(overrides.request || {}) },
+ };
+}
+
+function testReadyControlledProject() {
+ const result = evaluateProjectProvisioning(basePacket());
+
+ assert.equal(result.decision, "provision-ready");
+ assert.equal(result.counts.blocker, 0);
+ assert.equal(result.publicSummary.visibility, "institutional");
+}
+
+function testRestrictedProjectCannotStartPublic() {
+ const packet = basePacket({
+ request: {
+ dataClassification: "restricted-human-subjects",
+ visibility: "public",
+ metadata: {
+ projectName: "Neuro QC Consortium",
+ projectPurpose: "Coordinate reproducible quality-control notebooks for multi-site neuroimaging studies.",
+ discipline: "neuroscience",
+ institutionId: "northbridge",
+ dataClassification: "restricted-human-subjects",
+ retentionPlan: "retain-seven-years",
+ irbProtocolId: "IRB-2026-17",
+ dataUseAgreementId: "DUA-99",
+ },
+ },
+ });
+ const result = evaluateProjectProvisioning(packet);
+
+ assert.equal(result.decision, "hold-provisioning");
+ assert.ok(result.findings.some((finding) => finding.code === "VISIBILITY_CLASSIFICATION_CONFLICT"));
+}
+
+function testRequesterAuthorityAndMfaAreRequired() {
+ const packet = basePacket();
+ packet.users[0].projectCreator = false;
+ packet.users[0].mfaAt = "2026-05-01T08:00:00Z";
+ packet.request.roleAssignments = packet.request.roleAssignments.filter((assignment) => assignment.userId !== "user-pi-ada");
+
+ const result = evaluateProjectProvisioning(packet);
+
+ assert.equal(result.decision, "hold-provisioning");
+ assert.ok(result.findings.some((finding) => finding.code === "REQUESTER_AUTHORITY_MISSING"));
+ assert.ok(result.findings.some((finding) => finding.code === "REQUESTER_MFA_STALE"));
+}
+
+function testExternalRestrictedGrantNeedsAgreement() {
+ const packet = basePacket({
+ request: {
+ dataClassification: "restricted-human-subjects",
+ visibility: "private",
+ metadata: {
+ projectName: "Neuro QC Consortium",
+ projectPurpose: "Coordinate reproducible quality-control notebooks for multi-site neuroimaging studies.",
+ discipline: "neuroscience",
+ institutionId: "northbridge",
+ dataClassification: "restricted-human-subjects",
+ retentionPlan: "retain-seven-years",
+ irbProtocolId: "IRB-2026-17",
+ dataUseAgreementId: "DUA-99",
+ },
+ objectGrants: [
+ { principalId: "user-ext-ren", objectType: "restricted-dataset", permissions: ["read", "export"] },
+ ],
+ approvals: [],
+ },
+ });
+ const result = evaluateProjectProvisioning(packet);
+
+ assert.equal(result.decision, "hold-provisioning");
+ assert.ok(result.findings.some((finding) => finding.code === "EXTERNAL_RESTRICTED_DATA_APPROVAL_MISSING"));
+}
+
+function testMissingTemplateControlBlocksProvisioning() {
+ const packet = basePacket();
+ packet.request.controls.objectGrantReview = false;
+ const result = evaluateProjectProvisioning(packet);
+
+ assert.equal(result.decision, "hold-provisioning");
+ assert.ok(result.findings.some((finding) => finding.code === "TEMPLATE_CONTROL_MISSING"));
+}
+
+function testDeterministicDigest() {
+ const first = evaluateProjectProvisioning(basePacket());
+ const second = evaluateProjectProvisioning(basePacket());
+
+ assert.equal(first.auditDigest, second.auditDigest);
+}
+
+testReadyControlledProject();
+testRestrictedProjectCannotStartPublic();
+testRequesterAuthorityAndMfaAreRequired();
+testExternalRestrictedGrantNeedsAgreement();
+testMissingTemplateControlBlocksProvisioning();
+testDeterministicDigest();
+
+console.log("project-provisioning-baseline-guard tests passed");