diff --git a/knowledge-graph-ethics-provenance-guard/README.md b/knowledge-graph-ethics-provenance-guard/README.md
new file mode 100644
index 00000000..5af49336
--- /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 00000000..815bed2d
--- /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 00000000..eeff0d47
--- /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 = `
+`
+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 00000000..53befda8
--- /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 00000000..406aafb1
--- /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 00000000..c4ff23dd
Binary files /dev/null and b/knowledge-graph-ethics-provenance-guard/reports/demo.mp4 differ
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 00000000..17948981
--- /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 00000000..7c8b416e
--- /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 00000000..2d19bd4b
--- /dev/null
+++ b/knowledge-graph-ethics-provenance-guard/reports/summary.svg
@@ -0,0 +1,16 @@
+
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 00000000..5e901387
--- /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 00000000..1ac8cb97
--- /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 00000000..bb6836d0
--- /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")