Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions project-visibility-transition-guard/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Project Visibility Transition Guard

Self-contained User & Project Management slice for
`SCIBASE-AI/SCIBASE.AI#11`.

The guard evaluates private, institutional-only, or invitation-only scientific
workspaces before they are made public. It checks required approvals,
collaborator profile consent, object-level document/code/data permissions,
sensitive labels, public readiness, active IRB/funder holds, external partner
access, and immutable audit evidence.

This is intentionally separate from workspace/RBAC ledgers, privacy access
reviews, identity recovery, 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 access, and contribution-credit gates. Its job is
to stop unsafe public visibility transitions.

## Run

```bash
npm run check
npm test
npm run demo
npm run demo:video
```

## Outputs

- `reports/summary.json`
- `reports/reviewer-packet.md`
- `reports/summary.svg`
- `reports/demo.webm`

All data is synthetic. The module does not call OAuth, SAML, ORCID, storage,
profile, permission, email, audit-log, or external services.
173 changes: 173 additions & 0 deletions project-visibility-transition-guard/demo-video.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
const fs = require("fs");
const os = require("os");
const path = require("path");
const { execFileSync } = require("child_process");

const reportDir = path.join(__dirname, "reports");
const outputPath = path.join(reportDir, "demo.webm");

const chromeCandidates = [
process.env.CHROME_PATH,
"C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe",
"C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe",
"C:\\Program Files\\Microsoft\\Edge\\Application\\msedge.exe",
"C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe"
].filter(Boolean);

function findBrowser() {
const found = chromeCandidates.find((candidate) => fs.existsSync(candidate));
if (!found) {
throw new Error("Chrome or Edge was not found. Set CHROME_PATH to generate reports/demo.webm.");
}
return found;
}

function fileUrl(filePath) {
return `file:///${filePath.replace(/\\/g, "/")}`;
}

const html = String.raw`<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>Project visibility transition guard demo</title>
<style>
html, body { margin: 0; background: #f8fafc; }
canvas { width: 960px; height: 540px; }
pre { white-space: pre-wrap; word-break: break-all; color: #f8fafc; font-size: 1px; }
</style>
</head>
<body>
<canvas id="stage" width="960" height="540"></canvas>
<pre id="out">recording</pre>
<script>
const canvas = document.getElementById("stage");
const ctx = canvas.getContext("2d");
const out = document.getElementById("out");
const checks = [
["Object permissions", "Public transitions downgrade or block edit and download grants."],
["Sensitive labels", "Restricted data and anonymous-review comments stay out of public releases."],
["Consent and holds", "Collaborator approvals, IRB holds, and funder embargoes gate visibility."],
["Audit evidence", "Requester, approver, object review, and final decision events are required."]
];

function roundRect(x, y, w, h, r) {
ctx.beginPath();
ctx.moveTo(x + r, y);
ctx.arcTo(x + w, y, x + w, y + h, r);
ctx.arcTo(x + w, y + h, x, y + h, r);
ctx.arcTo(x, y + h, x, y, r);
ctx.arcTo(x, y, x + w, y, r);
ctx.closePath();
}

function draw(frame) {
const t = frame / 52;
ctx.fillStyle = "#f8fafc";
ctx.fillRect(0, 0, 960, 540);
ctx.fillStyle = "#111827";
ctx.font = "bold 34px Arial";
ctx.fillText("Project Visibility Transition Guard", 48, 64);
ctx.font = "19px Arial";
ctx.fillStyle = "#475569";
ctx.fillText("SCIBASE #11 workspace public-release governance demo", 48, 98);

ctx.fillStyle = "#e5e7eb";
roundRect(48, 126, 864, 30, 8);
ctx.fill();
ctx.fillStyle = "#7c3aed";
roundRect(48, 126, 864 * Math.min(1, t), 30, 8);
ctx.fill();

checks.forEach(([title, text], index) => {
const y = 194 + index * 70;
const active = Math.floor(t * 4.4) >= index;
ctx.fillStyle = active ? "#ffffff" : "#eef2f7";
ctx.strokeStyle = active ? "#7c3aed" : "#cbd5e1";
ctx.lineWidth = active ? 3 : 1;
roundRect(48, y, 864, 54, 8);
ctx.fill();
ctx.stroke();
ctx.fillStyle = active ? "#6d28d9" : "#64748b";
ctx.font = "bold 18px Arial";
ctx.fillText(title, 70, y + 22);
ctx.fillStyle = "#334155";
ctx.font = "16px Arial";
ctx.fillText(text, 70, y + 44);
});

ctx.fillStyle = "#64748b";
ctx.font = "15px Arial";
ctx.fillText("Synthetic data only. No OAuth, SAML, ORCID, storage, profile, permission, email, or audit-log network calls.", 48, 504);
}

async function main() {
if (!window.MediaRecorder) {
out.textContent = "ERROR: MediaRecorder unavailable";
return;
}
draw(0);
const stream = canvas.captureStream(12);
const recorder = new MediaRecorder(stream, { mimeType: "video/webm;codecs=vp8" });
const chunks = [];
recorder.ondataavailable = (event) => {
if (event.data && event.data.size > 0) {
chunks.push(event.data);
}
};
recorder.onstop = () => {
const blob = new Blob(chunks, { type: "video/webm" });
const reader = new FileReader();
reader.onloadend = () => {
out.textContent = reader.result;
};
reader.readAsDataURL(blob);
};
recorder.start();
let frame = 0;
const timer = setInterval(() => {
draw(frame);
frame += 1;
if (frame >= 52) {
clearInterval(timer);
recorder.stop();
stream.getTracks().forEach((track) => track.stop());
}
}, 83);
}

main();
</script>
</body>
</html>`;

fs.mkdirSync(reportDir, { recursive: true });

const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "visibility-transition-demo-"));
const htmlPath = path.join(tempDir, "demo.html");
const profileDir = path.join(tempDir, "profile");
fs.writeFileSync(htmlPath, html, "utf8");

const stdout = execFileSync(
findBrowser(),
[
"--headless=new",
"--disable-gpu",
"--disable-dev-shm-usage",
"--autoplay-policy=no-user-gesture-required",
"--run-all-compositor-stages-before-draw",
"--virtual-time-budget=7500",
`--user-data-dir=${profileDir}`,
"--dump-dom",
fileUrl(htmlPath)
],
{ encoding: "utf8", maxBuffer: 30 * 1024 * 1024 }
);

const match = stdout.match(/data:video\/webm;base64,([A-Za-z0-9+/=]+)/);
if (!match) {
throw new Error(`Demo video generation failed. Browser output ended with: ${stdout.slice(-600)}`);
}

fs.writeFileSync(outputPath, Buffer.from(match[1], "base64"));
console.log(`Generated ${path.relative(process.cwd(), outputPath)}`);
18 changes: 18 additions & 0 deletions project-visibility-transition-guard/demo.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
const fs = require("fs");
const path = require("path");
const { project } = require("./sample-data");
const { buildReviewPacket, renderMarkdownReport, renderSvgSummary } = require("./index");

const reportDir = path.join(__dirname, "reports");
fs.mkdirSync(reportDir, { recursive: true });

const packet = buildReviewPacket(project);

fs.writeFileSync(path.join(reportDir, "summary.json"), `${JSON.stringify(packet, null, 2)}\n`, "utf8");
fs.writeFileSync(path.join(reportDir, "reviewer-packet.md"), renderMarkdownReport(packet), "utf8");
fs.writeFileSync(path.join(reportDir, "summary.svg"), renderSvgSummary(packet), "utf8");

console.log(`Generated reports for ${packet.guard}`);
console.log(`Decision: ${packet.decision}`);
console.log(`Score: ${packet.score}`);
console.log(`Findings: ${packet.findings.length}`);
Loading