From 3be083e34508c2fe8fdb056d90cb45d799380a6b Mon Sep 17 00:00:00 2001 From: zergzorg Date: Thu, 21 May 2026 16:38:53 +0300 Subject: [PATCH] Add knowledge graph ethics provenance guard --- .../README.md | 35 +++ .../acceptance-notes.md | 7 + .../demo.js | 81 +++++++ .../index.js | 185 +++++++++++++++ .../package.json | 11 + .../reports/demo.mp4 | Bin 0 -> 5760 bytes .../reports/ethics-provenance-packet.json | 215 ++++++++++++++++++ .../reports/ethics-provenance-report.md | 24 ++ .../reports/summary.svg | 16 ++ .../requirements-map.md | 16 ++ .../sample-data.js | 130 +++++++++++ .../test.js | 41 ++++ 12 files changed, 761 insertions(+) create mode 100644 knowledge-graph-ethics-provenance-guard/README.md create mode 100644 knowledge-graph-ethics-provenance-guard/acceptance-notes.md create mode 100644 knowledge-graph-ethics-provenance-guard/demo.js create mode 100644 knowledge-graph-ethics-provenance-guard/index.js create mode 100644 knowledge-graph-ethics-provenance-guard/package.json create mode 100644 knowledge-graph-ethics-provenance-guard/reports/demo.mp4 create mode 100644 knowledge-graph-ethics-provenance-guard/reports/ethics-provenance-packet.json create mode 100644 knowledge-graph-ethics-provenance-guard/reports/ethics-provenance-report.md create mode 100644 knowledge-graph-ethics-provenance-guard/reports/summary.svg create mode 100644 knowledge-graph-ethics-provenance-guard/requirements-map.md create mode 100644 knowledge-graph-ethics-provenance-guard/sample-data.js create mode 100644 knowledge-graph-ethics-provenance-guard/test.js diff --git a/knowledge-graph-ethics-provenance-guard/README.md b/knowledge-graph-ethics-provenance-guard/README.md new file mode 100644 index 0000000..5af4933 --- /dev/null +++ b/knowledge-graph-ethics-provenance-guard/README.md @@ -0,0 +1,35 @@ +# Knowledge Graph Ethics Provenance Guard + +This module adds a focused ethics-provenance gate for Scientific Knowledge Graph recommendations. It evaluates whether graph recommendations and entity-page packets can be shown safely when the path references human-subject data, animal studies, restricted datasets, biosafety-controlled protocols, or dual-use methods. + +The implementation is dependency-free, uses synthetic data only, and does not call external services. + +## Local Verification + +```bash +npm run check +npm test +npm run demo +``` + +The demo writes reviewer artifacts to `reports/`: + +- `ethics-provenance-packet.json` +- `ethics-provenance-report.md` +- `summary.svg` +- `demo.mp4` when `ffmpeg` is available + +## What It Guards + +- Human-subject graph paths require active IRB evidence and a consent scope matching the intended reuse. +- Restricted evidence requires a data-use agreement before recommendation display. +- Biosafety-controlled protocols are held when user context lacks sufficient clearance. +- Animal-study routes require animal-care protocol evidence. +- Dual-use paths stay visible only with a review warning when no hard blocker exists. + +## Reviewer Path + +1. Start with `requirements-map.md` for issue coverage. +2. Read `index.js` for the guard decisions and risk scoring. +3. Run `test.js` to verify blocker behavior. +4. Run `demo.js` to regenerate deterministic review artifacts. diff --git a/knowledge-graph-ethics-provenance-guard/acceptance-notes.md b/knowledge-graph-ethics-provenance-guard/acceptance-notes.md new file mode 100644 index 0000000..815bed2 --- /dev/null +++ b/knowledge-graph-ethics-provenance-guard/acceptance-notes.md @@ -0,0 +1,7 @@ +# Acceptance Notes + +- No private data, credentials, payment information, or external accounts are used. +- All evidence and recommendations are synthetic fixtures. +- The guard is deterministic and dependency-free. +- Tests cover visible, held, warning, blocker, curator-action, digest, and JSON-LD packet behavior. +- The generated report artifacts provide a compact reviewer path without requiring a live SCIBASE deployment. diff --git a/knowledge-graph-ethics-provenance-guard/demo.js b/knowledge-graph-ethics-provenance-guard/demo.js new file mode 100644 index 0000000..eeff0d4 --- /dev/null +++ b/knowledge-graph-ethics-provenance-guard/demo.js @@ -0,0 +1,81 @@ +const fs = require("node:fs") +const path = require("node:path") +const { spawnSync } = require("node:child_process") +const { evaluateEthicsProvenance } = require("./index") +const { sampleRecommendations, sampleUserContext } = require("./sample-data") + +const reportsDir = path.join(__dirname, "reports") +fs.mkdirSync(reportsDir, { recursive: true }) + +const packet = evaluateEthicsProvenance({ + recommendations: sampleRecommendations, + userContext: sampleUserContext, +}) + +fs.writeFileSync( + path.join(reportsDir, "ethics-provenance-packet.json"), + `${JSON.stringify(packet, null, 2)}\n`, +) + +const held = packet.decisions.filter((decision) => decision.status === "hold") +const visible = packet.decisions.filter((decision) => decision.status === "show") + +const markdown = [ + "# Knowledge Graph Ethics Provenance Guard Report", + "", + `Generated decisions: ${packet.decisions.length}`, + `Visible recommendations: ${visible.length}`, + `Held recommendations: ${held.length}`, + `Curator actions: ${packet.audit.curatorActions}`, + `Audit digest: \`${packet.audit.digest}\``, + "", + "## Held Recommendations", + ...held.flatMap((decision) => [ + "", + `### ${decision.title}`, + `- Risk score: ${decision.riskScore}`, + `- Blockers: ${decision.blockers.map((blocker) => blocker.code).join(", ")}`, + `- First action: ${decision.curatorActions[0]?.message || "none"}`, + ]), + "", +] + +fs.writeFileSync(path.join(reportsDir, "ethics-provenance-report.md"), markdown.join("\n")) + +const svg = ` + + Knowledge Graph Ethics Provenance Guard + Synthetic recommendation review packet for SCIBASE issue #17 + + ${visible.length} + visible + + ${held.length} + held for ethics review + + ${packet.audit.curatorActions} + curator actions + Checks: IRB, consent scope, biosafety level, animal-care evidence, restricted data use, dual-use review. + Digest ${packet.audit.digest.slice(0, 24)}... + +` +fs.writeFileSync(path.join(reportsDir, "summary.svg"), svg) + +const ffmpeg = spawnSync("ffmpeg", [ + "-y", + "-f", + "lavfi", + "-i", + "color=c=0x111827:s=960x540:d=6:r=15", + "-vf", + "drawbox=x=48:y=170:w=250:h=150:color=0x0f766e@1:t=fill,drawbox=x=355:y=170:w=250:h=150:color=0xb45309@1:t=fill,drawbox=x=662:y=170:w=250:h=150:color=0x1d4ed8@1:t=fill,drawbox=x=48:y=370:w=864:h=18:color=0xfbbf24@1:t=fill", + "-pix_fmt", + "yuv420p", + path.join(reportsDir, "demo.mp4"), +], { stdio: "ignore" }) + +if (ffmpeg.status !== 0) { + console.warn("ffmpeg video generation failed; summary.svg and JSON/Markdown reports were still generated.") +} + +console.log(`Wrote review artifacts to ${reportsDir}`) diff --git a/knowledge-graph-ethics-provenance-guard/index.js b/knowledge-graph-ethics-provenance-guard/index.js new file mode 100644 index 0000000..53befda --- /dev/null +++ b/knowledge-graph-ethics-provenance-guard/index.js @@ -0,0 +1,185 @@ +const crypto = require("node:crypto") + +const BLOCKER_WEIGHTS = { + missing_irb: 35, + consent_scope_mismatch: 30, + restricted_data_without_dua: 25, + biosafety_clearance_gap: 30, + missing_animal_protocol: 28, + dual_use_review: 18, + stale_or_retracted_evidence: 40, +} + +function normalizeList(value) { + if (!value) return [] + return Array.isArray(value) ? value.filter(Boolean) : [value] +} + +function hasScope(evidence, requestedScope) { + const scopes = normalizeList(evidence.consentScope) + return scopes.includes(requestedScope) || scopes.includes("open_reuse") +} + +function evaluateEvidence(evidence, userContext) { + const blockers = [] + const warnings = [] + + if (["retracted", "stale", "superseded"].includes(evidence.status)) { + blockers.push({ + code: "stale_or_retracted_evidence", + message: `Evidence ${evidence.id} is ${evidence.status}.`, + }) + } + + if (evidence.subjectType === "human") { + if (!evidence.irbApproval || !userContext.hasActiveIrbProtocol) { + blockers.push({ + code: "missing_irb", + message: `Human-subject evidence ${evidence.id} requires active IRB approval before graph reuse.`, + }) + } + + if (!hasScope(evidence, evidence.requestedScope || userContext.intendedUse)) { + blockers.push({ + code: "consent_scope_mismatch", + message: `Consent scope for ${evidence.id} does not cover ${evidence.requestedScope || userContext.intendedUse}.`, + }) + } + } + + if (evidence.dataUse === "restricted" && !userContext.hasDataUseAgreement) { + blockers.push({ + code: "restricted_data_without_dua", + message: `Restricted evidence ${evidence.id} requires a data-use agreement before recommendation display.`, + }) + } + + if ((evidence.biosafetyLevel || 1) > (userContext.biosafetyClearanceLevel || 1)) { + blockers.push({ + code: "biosafety_clearance_gap", + message: `Evidence ${evidence.id} requires BSL-${evidence.biosafetyLevel}, user context has BSL-${userContext.biosafetyClearanceLevel || 1}.`, + }) + } + + if (evidence.subjectType === "animal" && !(evidence.animalProtocol || userContext.hasAnimalCareProtocol)) { + blockers.push({ + code: "missing_animal_protocol", + message: `Animal-study evidence ${evidence.id} needs IACUC/animal-care protocol evidence.`, + }) + } + + if (evidence.dualUseRisk === "review") { + warnings.push({ + code: "dual_use_review", + message: `Evidence ${evidence.id} should be reviewed for dual-use context before broad discovery placement.`, + }) + } + + return { blockers, warnings } +} + +function scoreRisk(blockers, warnings) { + const blockerScore = blockers.reduce((sum, blocker) => sum + (BLOCKER_WEIGHTS[blocker.code] || 15), 0) + const warningScore = warnings.reduce((sum, warning) => sum + (BLOCKER_WEIGHTS[warning.code] || 8), 0) + return Math.min(100, blockerScore + warningScore) +} + +function buildCuratorActions(recommendation, blockers, warnings) { + return [...blockers, ...warnings].map((item) => ({ + recommendationId: recommendation.id, + action: item.code.startsWith("missing") ? "attach_required_ethics_evidence" : "review_recommendation_path", + code: item.code, + message: item.message, + })) +} + +function buildJsonLdPacket(recommendation, decision) { + return { + "@context": "https://schema.org", + "@type": "Recommendation", + identifier: recommendation.id, + name: recommendation.title, + description: recommendation.rationale, + ethicsReview: { + "@type": "Review", + reviewBody: decision.blockers.length + ? "Held until ethics provenance gaps are resolved." + : "Visible with current ethics provenance.", + reviewRating: { + "@type": "Rating", + ratingValue: String(100 - decision.riskScore), + bestRating: "100", + worstRating: "0", + }, + }, + about: recommendation.nodes.map((node) => ({ + "@type": node.type, + identifier: node.id, + name: node.name, + })), + } +} + +function evaluateRecommendation(recommendation, userContext) { + const evidenceResults = recommendation.evidence.map((evidence) => evaluateEvidence(evidence, userContext)) + const blockers = evidenceResults.flatMap((result) => result.blockers) + const warnings = evidenceResults.flatMap((result) => result.warnings) + const riskScore = scoreRisk(blockers, warnings) + const status = blockers.length > 0 ? "hold" : "show" + + const decision = { + recommendationId: recommendation.id, + title: recommendation.title, + status, + riskScore, + blockers, + warnings, + curatorActions: buildCuratorActions(recommendation, blockers, warnings), + } + + return { + ...decision, + jsonLd: buildJsonLdPacket(recommendation, decision), + } +} + +function makeAuditDigest(decisions) { + const canonical = JSON.stringify(decisions.map((decision) => ({ + id: decision.recommendationId, + status: decision.status, + riskScore: decision.riskScore, + blockers: decision.blockers.map((blocker) => blocker.code).sort(), + warnings: decision.warnings.map((warning) => warning.code).sort(), + }))) + + return { + visible: decisions.filter((decision) => decision.status === "show").length, + held: decisions.filter((decision) => decision.status === "hold").length, + curatorActions: decisions.reduce((sum, decision) => sum + decision.curatorActions.length, 0), + digest: crypto.createHash("sha256").update(canonical).digest("hex"), + } +} + +function evaluateEthicsProvenance(input) { + const decisions = input.recommendations.map((recommendation) => + evaluateRecommendation(recommendation, input.userContext), + ) + + return { + generatedAt: new Date("2026-05-21T00:00:00.000Z").toISOString(), + userContext: { + institutionId: input.userContext.institutionId, + intendedUse: input.userContext.intendedUse, + biosafetyClearanceLevel: input.userContext.biosafetyClearanceLevel, + }, + decisions, + audit: makeAuditDigest(decisions), + } +} + +module.exports = { + evaluateEthicsProvenance, + evaluateEvidence, + buildJsonLdPacket, + makeAuditDigest, +} diff --git a/knowledge-graph-ethics-provenance-guard/package.json b/knowledge-graph-ethics-provenance-guard/package.json new file mode 100644 index 0000000..406aafb --- /dev/null +++ b/knowledge-graph-ethics-provenance-guard/package.json @@ -0,0 +1,11 @@ +{ + "name": "knowledge-graph-ethics-provenance-guard", + "version": "1.0.0", + "private": true, + "type": "commonjs", + "scripts": { + "check": "node --check index.js && node --check sample-data.js && node --check test.js && node --check demo.js", + "test": "node test.js", + "demo": "node demo.js" + } +} diff --git a/knowledge-graph-ethics-provenance-guard/reports/demo.mp4 b/knowledge-graph-ethics-provenance-guard/reports/demo.mp4 new file mode 100644 index 0000000000000000000000000000000000000000..c4ff23dd549e556d02dbbd421e3ed448077fdee8 GIT binary patch literal 5760 zcmeHLe^3**2Sv@MP!YT61V% z6k0$aYsFKGmuA`t*VBtYlI0j+ERy6}f%9sF&1%igv{`LA(CO1O*^!l1Q&VH9=2=b> zXum~L%Cj(v#ixk^$VjrrOMcZsFtmqeD3VY(FO^NOoJWwD3W{`C9ae(&(?YGvQPx1V z!x|vTY(nHH9~U6hDi2V3gsj$rD!knaOOX~UD8Y*m`2fedu|*Oz+zRb4=O{9lV0?-s z(r!>C2~FVyfmeab59G6q1_VLJOhIXcUTF(qJuEJNNac?ykFxKfd(Py_Xt%A zUF&8fQKmI$GY}(9p?N=;06~SuGG2uiITfPiam%$p^DG6E<)&F$#%_4r9-dY)2(NIu zV>KLK?$bQLlVr~CE|+BB8l5tzR&cdoo3iCtL!?{e{S=u)s0`=lm@193Tk$Xz8pl-O zR3E4-jC;^s%vTtSQM`ocfxu$7ph4NPEmoouJ40D5xj@R;uwK&vDt7^Ns2V3zc7m5- zmLMZw62TT-2?>Cw6V*e?CUY+~pc%n|i=DTdO4ml8bDkjf6m}E^b{v@>_|EH2+gJ95 z9lZ!ehJT6EW+)p9-fNRy0_{kY<&4Zn+v+okxccb)$OhA0(~0fK<534=w>r9ZRo&`7JFskh z;`J-f-9kGZEqi}n_~cDbNuTMNE_A5*&dCcuJXP60zwCPDT;l8t4X0ASWinlo7T$aA z_YTy&toz|N@6~_G6~A)vS@Xe9`^s#}Y|j^G+t=9pm+x!nQSJrnV)ORhO`rYl&P`j6 zJ^IaSMb6_JKmR2ne|Guu>Z9uB8{Y2gQ6C@jG&Xm}_fB7V_nqP;7h63)z2= z@v`-0mYDS?cRMyo?aoK5$-Bfy$KKz3ZS@liR;UO2*1fVuse55V);sM>kGy8jI9+jm zb4&H5?RE3^Y+SRxI2A>w*rzs&)7uuDe%EIN-PF?+|JIh%hmNLSh&^?nSWHd*^6`fc zB=0*xRqg*|LFxC>3%utp?_T|jj-vCw!y2YQWBRmu=ijfcSSi2!c>INVtFA`0&o}?G z?;9mQ+MWm!rYHWgdF95Fw^z(FzidE^{cc$Ydt8dEB~5U}4ZK;R3oTjuZ7VuoT3n{L zq85TgqN}wa&=}M^4&PXt7BZH*4S2Imt+ZC4^Gr(%Yr1sawATy{^L&`o`ecvJiRl1N znybZE^lO)|d3nrZ!9m@*Z@N#f*j?UQd;vB*;0;@01@fJzQ%f<;H!|QT{VOS)S>+;|2K9lQm za$SzQ72rejf3z;w1_yRG>8J1DnfL=IY-$hiyc`SCr9(cj^KvXm!^;P@E~VhWu7=@( z-LcN_^1pgsrnt6tgwn?y9N6^sFvy5TgdWmFNvZ~9L9F(%P!B1Q@Zt4{j7$js?w4T= z=pNB_-j^JUia;p36>dcpzv6>205+|r| zht~^Oz;{6oD}4DeF|tBLIC^)zt8WV3~dR4+^Mp z8ERASi0lNn3xUKBE~c5wK|a@JvE^GxD@h2v2mg~`F!gt05I(R`9!M=Rv1m^43pBeN z>b+;2z2>@JGtLx%i9{)JiDZ)rMf)y&`O)0YdwqRadxlJ_c!uym@RI;H*ie+?yP%Cr zqN?dLfN3mKicl1$6TmS%Gq{BGBYK$ovLJ{1VV3kdAykLkxQY3?EvP5?Mm#3tW5fZc z$Ny7)=<|{UInU@L)5oUgeJIaOKLOsV5D!;)oqP=STzK$aHaqmBk7%b(wui;%KaB^D zFYHB(1T`*rFS5epsbV!6>Qa%@Yar2MLr(xWH56Kw@g_Z3GI`^enYUY$u)$>b=NKEo QMiFeqUf7CB_*uz+0i+KsFaQ7m literal 0 HcmV?d00001 diff --git a/knowledge-graph-ethics-provenance-guard/reports/ethics-provenance-packet.json b/knowledge-graph-ethics-provenance-guard/reports/ethics-provenance-packet.json new file mode 100644 index 0000000..1794898 --- /dev/null +++ b/knowledge-graph-ethics-provenance-guard/reports/ethics-provenance-packet.json @@ -0,0 +1,215 @@ +{ + "generatedAt": "2026-05-21T00:00:00.000Z", + "userContext": { + "institutionId": "north-atlas-university", + "intendedUse": "cross_project_reuse", + "biosafetyClearanceLevel": 1 + }, + "decisions": [ + { + "recommendationId": "rec-safe-open-protocol", + "title": "Reuse the public microscopy segmentation protocol", + "status": "show", + "riskScore": 0, + "blockers": [], + "warnings": [], + "curatorActions": [], + "jsonLd": { + "@context": "https://schema.org", + "@type": "Recommendation", + "identifier": "rec-safe-open-protocol", + "name": "Reuse the public microscopy segmentation protocol", + "description": "Protocol and dataset are public, non-human, non-animal, and low biosafety risk.", + "ethicsReview": { + "@type": "Review", + "reviewBody": "Visible with current ethics provenance.", + "reviewRating": { + "@type": "Rating", + "ratingValue": "100", + "bestRating": "100", + "worstRating": "0" + } + }, + "about": [ + { + "@type": "Protocol", + "identifier": "protocol-micro-001", + "name": "Open microscopy segmentation workflow" + }, + { + "@type": "Dataset", + "identifier": "dataset-cells-public", + "name": "Public yeast cell image set" + } + ] + } + }, + { + "recommendationId": "rec-human-cohort-transfer", + "title": "Connect cohort assay data to an external replication notebook", + "status": "hold", + "riskScore": 55, + "blockers": [ + { + "code": "consent_scope_mismatch", + "message": "Consent scope for ev-human-consent does not cover cross_project_reuse." + }, + { + "code": "restricted_data_without_dua", + "message": "Restricted evidence ev-human-consent requires a data-use agreement before recommendation display." + } + ], + "warnings": [], + "curatorActions": [ + { + "recommendationId": "rec-human-cohort-transfer", + "action": "review_recommendation_path", + "code": "consent_scope_mismatch", + "message": "Consent scope for ev-human-consent does not cover cross_project_reuse." + }, + { + "recommendationId": "rec-human-cohort-transfer", + "action": "review_recommendation_path", + "code": "restricted_data_without_dua", + "message": "Restricted evidence ev-human-consent requires a data-use agreement before recommendation display." + } + ], + "jsonLd": { + "@context": "https://schema.org", + "@type": "Recommendation", + "identifier": "rec-human-cohort-transfer", + "name": "Connect cohort assay data to an external replication notebook", + "description": "The graph finds a strong concept-method match, but reuse expands beyond the consent scope.", + "ethicsReview": { + "@type": "Review", + "reviewBody": "Held until ethics provenance gaps are resolved.", + "reviewRating": { + "@type": "Rating", + "ratingValue": "45", + "bestRating": "100", + "worstRating": "0" + } + }, + "about": [ + { + "@type": "Dataset", + "identifier": "dataset-human-cohort", + "name": "Longitudinal immune response cohort" + }, + { + "@type": "Notebook", + "identifier": "notebook-replication", + "name": "External immune model replication notebook" + } + ] + } + }, + { + "recommendationId": "rec-bsl2-protocol", + "title": "Suggest BSL-2 viral vector protocol for wet-lab follow-up", + "status": "hold", + "riskScore": 48, + "blockers": [ + { + "code": "biosafety_clearance_gap", + "message": "Evidence ev-bsl2-protocol requires BSL-2, user context has BSL-1." + } + ], + "warnings": [ + { + "code": "dual_use_review", + "message": "Evidence ev-bsl2-protocol should be reviewed for dual-use context before broad discovery placement." + } + ], + "curatorActions": [ + { + "recommendationId": "rec-bsl2-protocol", + "action": "review_recommendation_path", + "code": "biosafety_clearance_gap", + "message": "Evidence ev-bsl2-protocol requires BSL-2, user context has BSL-1." + }, + { + "recommendationId": "rec-bsl2-protocol", + "action": "review_recommendation_path", + "code": "dual_use_review", + "message": "Evidence ev-bsl2-protocol should be reviewed for dual-use context before broad discovery placement." + } + ], + "jsonLd": { + "@context": "https://schema.org", + "@type": "Recommendation", + "identifier": "rec-bsl2-protocol", + "name": "Suggest BSL-2 viral vector protocol for wet-lab follow-up", + "description": "The graph route is scientifically relevant, but the user context lacks the required clearance.", + "ethicsReview": { + "@type": "Review", + "reviewBody": "Held until ethics provenance gaps are resolved.", + "reviewRating": { + "@type": "Rating", + "ratingValue": "52", + "bestRating": "100", + "worstRating": "0" + } + }, + "about": [ + { + "@type": "Protocol", + "identifier": "protocol-bsl2-vector", + "name": "BSL-2 viral vector transduction protocol" + } + ] + } + }, + { + "recommendationId": "rec-animal-study", + "title": "Recommend animal-model replication path", + "status": "hold", + "riskScore": 28, + "blockers": [ + { + "code": "missing_animal_protocol", + "message": "Animal-study evidence ev-animal-care needs IACUC/animal-care protocol evidence." + } + ], + "warnings": [], + "curatorActions": [ + { + "recommendationId": "rec-animal-study", + "action": "attach_required_ethics_evidence", + "code": "missing_animal_protocol", + "message": "Animal-study evidence ev-animal-care needs IACUC/animal-care protocol evidence." + } + ], + "jsonLd": { + "@context": "https://schema.org", + "@type": "Recommendation", + "identifier": "rec-animal-study", + "name": "Recommend animal-model replication path", + "description": "The method is reusable only after animal-care protocol evidence is attached.", + "ethicsReview": { + "@type": "Review", + "reviewBody": "Held until ethics provenance gaps are resolved.", + "reviewRating": { + "@type": "Rating", + "ratingValue": "72", + "bestRating": "100", + "worstRating": "0" + } + }, + "about": [ + { + "@type": "Paper", + "identifier": "study-animal-model", + "name": "Rodent wound healing replication study" + } + ] + } + } + ], + "audit": { + "visible": 1, + "held": 3, + "curatorActions": 5, + "digest": "6fb761f774ad91b10067975ce13ce7aa1210586a9a1e34700978e696e2e09b22" + } +} diff --git a/knowledge-graph-ethics-provenance-guard/reports/ethics-provenance-report.md b/knowledge-graph-ethics-provenance-guard/reports/ethics-provenance-report.md new file mode 100644 index 0000000..7c8b416 --- /dev/null +++ b/knowledge-graph-ethics-provenance-guard/reports/ethics-provenance-report.md @@ -0,0 +1,24 @@ +# Knowledge Graph Ethics Provenance Guard Report + +Generated decisions: 4 +Visible recommendations: 1 +Held recommendations: 3 +Curator actions: 5 +Audit digest: `6fb761f774ad91b10067975ce13ce7aa1210586a9a1e34700978e696e2e09b22` + +## Held Recommendations + +### Connect cohort assay data to an external replication notebook +- Risk score: 55 +- Blockers: consent_scope_mismatch, restricted_data_without_dua +- First action: Consent scope for ev-human-consent does not cover cross_project_reuse. + +### Suggest BSL-2 viral vector protocol for wet-lab follow-up +- Risk score: 48 +- Blockers: biosafety_clearance_gap +- First action: Evidence ev-bsl2-protocol requires BSL-2, user context has BSL-1. + +### Recommend animal-model replication path +- Risk score: 28 +- Blockers: missing_animal_protocol +- First action: Animal-study evidence ev-animal-care needs IACUC/animal-care protocol evidence. diff --git a/knowledge-graph-ethics-provenance-guard/reports/summary.svg b/knowledge-graph-ethics-provenance-guard/reports/summary.svg new file mode 100644 index 0000000..2d19bd4 --- /dev/null +++ b/knowledge-graph-ethics-provenance-guard/reports/summary.svg @@ -0,0 +1,16 @@ + + + Knowledge Graph Ethics Provenance Guard + Synthetic recommendation review packet for SCIBASE issue #17 + + 1 + visible + + 3 + held for ethics review + + 5 + curator actions + Checks: IRB, consent scope, biosafety level, animal-care evidence, restricted data use, dual-use review. + Digest 6fb761f774ad91b10067975c... + diff --git a/knowledge-graph-ethics-provenance-guard/requirements-map.md b/knowledge-graph-ethics-provenance-guard/requirements-map.md new file mode 100644 index 0000000..5e90138 --- /dev/null +++ b/knowledge-graph-ethics-provenance-guard/requirements-map.md @@ -0,0 +1,16 @@ +# Requirements Map + +| Issue #17 capability | Coverage in this slice | +| --- | --- | +| Entity pages with aggregated context | `buildJsonLdPacket()` emits recommendation/entity review packets with ethics review metadata. | +| Knowledge navigation and filters | Recommendations are evaluated against user context before graph routes are displayed. | +| AI research recommendations | `evaluateEthicsProvenance()` returns `show` or `hold` decisions for recommendation payloads. | +| Tools, datasets, protocols, and papers | Sample data includes datasets, notebooks, protocols, and papers as graph nodes. | +| Evidence-backed relationships | Each recommendation is evaluated through attached evidence records. | +| Safe discovery workflows | Curator actions explain why unsafe paths are held and what evidence is required. | + +## Scope Boundaries + +This is not a broad extractor, graph navigator, ontology drift guard, relationship conflict arbiter, author-affiliation disambiguator, artifact lineage tracker, evidence freshness module, instrument/method compatibility checker, reproducibility route planner, recommendation visibility/diversity guard, negative-result replication graph, measurement harmonization guard, or claim qualifier guard. + +The slice focuses only on ethics provenance before graph recommendations and entity-page packets are shown. diff --git a/knowledge-graph-ethics-provenance-guard/sample-data.js b/knowledge-graph-ethics-provenance-guard/sample-data.js new file mode 100644 index 0000000..1ac8cb9 --- /dev/null +++ b/knowledge-graph-ethics-provenance-guard/sample-data.js @@ -0,0 +1,130 @@ +const sampleUserContext = { + userId: "researcher-demo", + institutionId: "north-atlas-university", + intendedUse: "cross_project_reuse", + hasActiveIrbProtocol: true, + hasDataUseAgreement: false, + biosafetyClearanceLevel: 1, + hasAnimalCareProtocol: false, + ethicsTraining: ["human-subjects"], +} + +const sampleRecommendations = [ + { + id: "rec-safe-open-protocol", + title: "Reuse the public microscopy segmentation protocol", + rationale: "Protocol and dataset are public, non-human, non-animal, and low biosafety risk.", + nodes: [ + { + id: "protocol-micro-001", + type: "Protocol", + name: "Open microscopy segmentation workflow", + }, + { + id: "dataset-cells-public", + type: "Dataset", + name: "Public yeast cell image set", + }, + ], + evidence: [ + { + id: "ev-open-protocol", + source: "protocol DOI 10.1000/open-micro", + status: "active", + subjectType: "non-human", + consentScope: ["open_reuse", "methods_training"], + requestedScope: "open_reuse", + biosafetyLevel: 1, + dualUseRisk: "none", + dataUse: "open", + }, + ], + }, + { + id: "rec-human-cohort-transfer", + title: "Connect cohort assay data to an external replication notebook", + rationale: "The graph finds a strong concept-method match, but reuse expands beyond the consent scope.", + nodes: [ + { + id: "dataset-human-cohort", + type: "Dataset", + name: "Longitudinal immune response cohort", + }, + { + id: "notebook-replication", + type: "Notebook", + name: "External immune model replication notebook", + }, + ], + evidence: [ + { + id: "ev-human-consent", + source: "IRB packet IRB-2025-1442", + status: "active", + subjectType: "human", + irbApproval: "IRB-2025-1442", + consentScope: ["internal_validation"], + requestedScope: "cross_project_reuse", + biosafetyLevel: 1, + dualUseRisk: "none", + dataUse: "restricted", + }, + ], + }, + { + id: "rec-bsl2-protocol", + title: "Suggest BSL-2 viral vector protocol for wet-lab follow-up", + rationale: "The graph route is scientifically relevant, but the user context lacks the required clearance.", + nodes: [ + { + id: "protocol-bsl2-vector", + type: "Protocol", + name: "BSL-2 viral vector transduction protocol", + }, + ], + evidence: [ + { + id: "ev-bsl2-protocol", + source: "protocol DOI 10.1000/bsl2-vector", + status: "active", + subjectType: "non-human", + consentScope: ["methods_training"], + requestedScope: "methods_training", + biosafetyLevel: 2, + dualUseRisk: "review", + dataUse: "open", + }, + ], + }, + { + id: "rec-animal-study", + title: "Recommend animal-model replication path", + rationale: "The method is reusable only after animal-care protocol evidence is attached.", + nodes: [ + { + id: "study-animal-model", + type: "Paper", + name: "Rodent wound healing replication study", + }, + ], + evidence: [ + { + id: "ev-animal-care", + source: "IACUC pending packet", + status: "active", + subjectType: "animal", + consentScope: ["replication"], + requestedScope: "replication", + biosafetyLevel: 1, + animalProtocol: null, + dualUseRisk: "none", + dataUse: "open", + }, + ], + }, +] + +module.exports = { + sampleRecommendations, + sampleUserContext, +} diff --git a/knowledge-graph-ethics-provenance-guard/test.js b/knowledge-graph-ethics-provenance-guard/test.js new file mode 100644 index 0000000..bb6836d --- /dev/null +++ b/knowledge-graph-ethics-provenance-guard/test.js @@ -0,0 +1,41 @@ +const assert = require("node:assert/strict") +const { evaluateEthicsProvenance, evaluateEvidence, buildJsonLdPacket } = require("./index") +const { sampleRecommendations, sampleUserContext } = require("./sample-data") + +const packet = evaluateEthicsProvenance({ + recommendations: sampleRecommendations, + userContext: sampleUserContext, +}) + +const byId = Object.fromEntries(packet.decisions.map((decision) => [decision.recommendationId, decision])) + +assert.equal(byId["rec-safe-open-protocol"].status, "show") +assert.equal(byId["rec-safe-open-protocol"].blockers.length, 0) + +assert.equal(byId["rec-human-cohort-transfer"].status, "hold") +assert.ok(byId["rec-human-cohort-transfer"].blockers.some((blocker) => blocker.code === "consent_scope_mismatch")) +assert.ok(byId["rec-human-cohort-transfer"].blockers.some((blocker) => blocker.code === "restricted_data_without_dua")) + +assert.equal(byId["rec-bsl2-protocol"].status, "hold") +assert.ok(byId["rec-bsl2-protocol"].blockers.some((blocker) => blocker.code === "biosafety_clearance_gap")) +assert.ok(byId["rec-bsl2-protocol"].warnings.some((warning) => warning.code === "dual_use_review")) + +assert.equal(byId["rec-animal-study"].status, "hold") +assert.ok(byId["rec-animal-study"].curatorActions.some((action) => action.code === "missing_animal_protocol")) + +assert.equal(packet.audit.visible, 1) +assert.equal(packet.audit.held, 3) +assert.match(packet.audit.digest, /^[a-f0-9]{64}$/) + +const humanEvidenceCheck = evaluateEvidence(sampleRecommendations[1].evidence[0], { + ...sampleUserContext, + hasActiveIrbProtocol: false, +}) +assert.ok(humanEvidenceCheck.blockers.some((blocker) => blocker.code === "missing_irb")) + +const jsonLd = buildJsonLdPacket(sampleRecommendations[0], byId["rec-safe-open-protocol"]) +assert.equal(jsonLd["@type"], "Recommendation") +assert.equal(jsonLd.ethicsReview["@type"], "Review") +assert.equal(jsonLd.about.length, 2) + +console.log("knowledge-graph-ethics-provenance-guard tests passed")