Skip to content
Closed
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
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,6 @@
# deepevents.ai
deepevents.ai main codebase

## Enterprise tooling modules

- [Enterprise IRB consent governance](enterprise-irb-consent-governance/README.md) validates human-subjects research projects against IRB approval, consent scope, data-use, export, retention, and webhook evidence requirements.
39 changes: 39 additions & 0 deletions enterprise-irb-consent-governance/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Enterprise IRB Consent Governance

This module adds an Enterprise Tooling slice for institutional review and human-subjects research governance. It is self-contained, dependency-free, and uses synthetic project data only.

It evaluates whether projects can be exported, published, or synced to institutional systems when they include human participants, controlled data, or consent-limited datasets.

## What it checks

- IRB approval presence, status, and expiry.
- Consent scope coverage for each data-use purpose.
- Guardian consent for minor participants.
- De-identification requirements before external export.
- Data-use agreement coverage for PHI, genomic, and private clinical data.
- Export destination restrictions and jurisdiction limits.
- Retention and deletion clock violations.
- Signed webhook-ready governance events for institutional audit systems.

## Files

- `index.js` - governance evaluator and packet generator.
- `sample-data.js` - synthetic institutional policy and project samples.
- `test.js` - deterministic unit tests.
- `demo.js` - writes reviewer artifacts and a short MP4 demo when `ffmpeg` is available.
- `requirements-map.md` - maps the module to issue #19 requirements.
- `acceptance-notes.md` - reviewer notes and scope boundaries.

## Run

```bash
node enterprise-irb-consent-governance/test.js
node enterprise-irb-consent-governance/demo.js
```

The demo writes:

- `irb-consent-report.json`
- `reviewer-packet.md`
- `demo.svg`
- `demo.mp4` when `ffmpeg` is available
19 changes: 19 additions & 0 deletions enterprise-irb-consent-governance/acceptance-notes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Acceptance Notes

## Scope

This PR adds a self-contained governance gate for research involving human participants or consent-limited data. It can be reviewed without external accounts, third-party APIs, or credentials.

## Reviewer workflow

1. Run `node enterprise-irb-consent-governance/test.js`.
2. Run `node enterprise-irb-consent-governance/demo.js`.
3. Inspect `irb-consent-report.json`, `reviewer-packet.md`, `demo.svg`, and `demo.mp4`.

## Safety boundaries

- Uses synthetic sample data only.
- Does not store or request credentials.
- Does not call external services.
- Webhook signatures use a synthetic secret from `sample-data.js`.
- Export decisions are deterministic and auditable through `auditDigest` and per-project `evidenceDigest` fields.
126 changes: 126 additions & 0 deletions enterprise-irb-consent-governance/demo.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
const fs = require("fs")
const path = require("path")
const { spawnSync } = require("child_process")
const { generateGovernancePacket } = require("./index")
const { policy, projects } = require("./sample-data")

const outDir = __dirname
const packet = generateGovernancePacket(projects, policy)

function writeJson() {
fs.writeFileSync(
path.join(outDir, "irb-consent-report.json"),
`${JSON.stringify(packet, null, 2)}\n`,
)
}

function writeReviewerPacket() {
const lines = [
"# Enterprise IRB Consent Governance Review Packet",
"",
`Generated: ${packet.generatedAt}`,
`Audit digest: ${packet.auditDigest}`,
"",
"## Summary",
"",
`- Projects evaluated: ${packet.summary.projectCount}`,
`- Approved: ${packet.summary.approved}`,
`- Review: ${packet.summary.review}`,
`- Blocked: ${packet.summary.blocked}`,
"",
"## Top risks",
"",
...packet.summary.topRisks.map(
(risk) => `- ${risk.projectId}: ${risk.title} - ${risk.status} (${risk.riskScore})`,
),
"",
"## Action queue",
"",
...packet.evaluations.flatMap((evaluation) =>
evaluation.actionQueue.map(
(action) =>
`- ${evaluation.projectId}: ${action.code} assigned to ${action.owner}, due in ${action.dueInDays} day(s)`,
),
),
"",
]

fs.writeFileSync(path.join(outDir, "reviewer-packet.md"), `${lines.join("\n")}\n`)
}

function escapeXml(value) {
return String(value)
.replaceAll("&", "&")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;")
.replaceAll('"', "&quot;")
}

function writeSvg() {
const rows = packet.evaluations
.map((evaluation, index) => {
const y = 150 + index * 54
const color = evaluation.status === "approved" ? "#2f9e44" : evaluation.status === "review" ? "#f08c00" : "#c92a2a"
return `
<g transform="translate(48 ${y})">
<circle cx="12" cy="12" r="9" fill="${color}" />
<text x="34" y="17" font-size="18" font-family="Arial" fill="#17202a">${escapeXml(evaluation.projectId)} - ${escapeXml(evaluation.status)}</text>
<text x="520" y="17" font-size="16" font-family="Arial" fill="#4b5563">risk ${evaluation.riskScore}</text>
</g>`
})
.join("")

const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="900" height="430" viewBox="0 0 900 430">
<rect width="900" height="430" fill="#f8fafc" />
<text x="48" y="62" font-size="34" font-family="Arial" font-weight="700" fill="#111827">Enterprise IRB Consent Governance</text>
<text x="48" y="98" font-size="18" font-family="Arial" fill="#4b5563">Approved ${packet.summary.approved} / Review ${packet.summary.review} / Blocked ${packet.summary.blocked}</text>
${rows}
<text x="48" y="390" font-size="14" font-family="Arial" fill="#6b7280">Audit digest: ${packet.auditDigest.slice(0, 32)}...</text>
</svg>`

fs.writeFileSync(path.join(outDir, "demo.svg"), `${svg}\n`)
}

function writeMp4() {
const mp4Path = path.join(outDir, "demo.mp4")
const title = "Enterprise IRB Consent Governance"
const summary = `Approved ${packet.summary.approved} Review ${packet.summary.review} Blocked ${packet.summary.blocked}`
const topRisk = packet.summary.topRisks[0]
const riskText = `Top risk: ${topRisk.projectId} score ${topRisk.riskScore}`
const font = "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf"

const filter = [
`drawtext=fontfile=${font}:text='${title}':x=60:y=80:fontsize=34:fontcolor=white`,
`drawtext=fontfile=${font}:text='${summary}':x=60:y=150:fontsize=28:fontcolor=white`,
`drawtext=fontfile=${font}:text='${riskText}':x=60:y=220:fontsize=24:fontcolor=white`,
`drawtext=fontfile=${font}:text='Signed webhook events and reviewer packet generated':x=60:y=290:fontsize=22:fontcolor=white`,
].join(",")

const result = spawnSync(
"ffmpeg",
[
"-y",
"-f",
"lavfi",
"-i",
"color=c=0x111827:s=1280x720:d=7:r=24",
"-vf",
filter,
"-pix_fmt",
"yuv420p",
mp4Path,
],
{ stdio: "pipe" },
)

if (result.status !== 0) {
fs.writeFileSync(path.join(outDir, "demo-video-warning.txt"), result.stderr.toString())
}
}

writeJson()
writeReviewerPacket()
writeSvg()
writeMp4()

console.log(`Wrote enterprise IRB consent governance artifacts to ${outDir}`)
Binary file added enterprise-irb-consent-governance/demo.mp4
Binary file not shown.
27 changes: 27 additions & 0 deletions enterprise-irb-consent-governance/demo.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading