From 0b7e613f0276101878e211663e3cc4d7b87a22b2 Mon Sep 17 00:00:00 2001 From: Seowoo Han Date: Wed, 20 May 2026 19:40:30 +0900 Subject: [PATCH] Add knowledge graph recommendation visibility guard --- .../README.md | 20 ++ .../acceptance-notes.md | 30 +++ .../demo.js | 85 +++++++ .../demo.mp4 | Bin 0 -> 48510 bytes .../demo.svg | 29 +++ .../index.js | 222 ++++++++++++++++++ .../requirements-map.md | 13 + .../test.js | 152 ++++++++++++ 8 files changed, 551 insertions(+) create mode 100644 knowledge-graph-recommendation-visibility-guard/README.md create mode 100644 knowledge-graph-recommendation-visibility-guard/acceptance-notes.md create mode 100644 knowledge-graph-recommendation-visibility-guard/demo.js create mode 100644 knowledge-graph-recommendation-visibility-guard/demo.mp4 create mode 100644 knowledge-graph-recommendation-visibility-guard/demo.svg create mode 100644 knowledge-graph-recommendation-visibility-guard/index.js create mode 100644 knowledge-graph-recommendation-visibility-guard/requirements-map.md create mode 100644 knowledge-graph-recommendation-visibility-guard/test.js diff --git a/knowledge-graph-recommendation-visibility-guard/README.md b/knowledge-graph-recommendation-visibility-guard/README.md new file mode 100644 index 0000000..ce50556 --- /dev/null +++ b/knowledge-graph-recommendation-visibility-guard/README.md @@ -0,0 +1,20 @@ +# Knowledge Graph Recommendation Visibility Guard + +This module covers a privacy and governance slice of SCIBASE issue #17. + +It checks scientific knowledge graph recommendations before they appear in graph navigation, entity pages, sidebars, or discovery digests. Recommendations that would expose private, embargoed, restricted, or institution-only research artifacts are suppressed until the viewer has the right institution, project, consent, license, and clearance context. + +## What It Does + +- Evaluates graph recommendation paths against node visibility and evidence access. +- Supports public, institutional, project, private, embargoed, and restricted artifacts. +- Checks embargo windows, consent IDs, license IDs, and clearance tags. +- Suppresses unsafe paths while keeping safe recommendations visible. +- Produces curator actions and a deterministic audit digest for explainable discovery behavior. + +## Run + +```bash +node knowledge-graph-recommendation-visibility-guard/test.js +node knowledge-graph-recommendation-visibility-guard/demo.js +``` diff --git a/knowledge-graph-recommendation-visibility-guard/acceptance-notes.md b/knowledge-graph-recommendation-visibility-guard/acceptance-notes.md new file mode 100644 index 0000000..104965d --- /dev/null +++ b/knowledge-graph-recommendation-visibility-guard/acceptance-notes.md @@ -0,0 +1,30 @@ +# Acceptance Notes + +## Review Scenarios + +1. Visible institutional recommendation + - A researcher from the matching institution can see a public concept to institutional dataset route. + - The result includes evidence IDs and a stable audit digest. + +2. Embargoed path suppression + - A non-curator viewer cannot see recommendations that include an embargoed node before the embargo date. + - The suppressed recommendation includes a clear blocker and curator action. + +3. Restricted cohort access + - A viewer with matching consent, license, and clearance can see restricted cohort recommendations. + - Missing consent, license, or clearance suppresses the path. + +4. Expired embargo review + - An expired embargo does not hide the recommendation, but it produces a release-metadata warning. + +## Validation + +```bash +node knowledge-graph-recommendation-visibility-guard/test.js +node knowledge-graph-recommendation-visibility-guard/demo.js +node --check knowledge-graph-recommendation-visibility-guard/index.js +node --check knowledge-graph-recommendation-visibility-guard/test.js +node --check knowledge-graph-recommendation-visibility-guard/demo.js +``` + +The included `demo.mp4` is a five-second visual walkthrough of the visibility guard flow. diff --git a/knowledge-graph-recommendation-visibility-guard/demo.js b/knowledge-graph-recommendation-visibility-guard/demo.js new file mode 100644 index 0000000..7736ea3 --- /dev/null +++ b/knowledge-graph-recommendation-visibility-guard/demo.js @@ -0,0 +1,85 @@ +"use strict" + +const { assessRecommendationVisibility } = require("./index") + +const result = assessRecommendationVisibility({ + now: "2026-05-20T10:00:00Z", + actor: { + id: "researcher-ada", + role: "researcher", + institutionId: "north-lab", + consentIds: ["consent-human-1"], + licenseIds: [], + clearanceTags: ["irb-approved"], + }, + nodes: [ + { id: "concept-crispr", type: "concept", title: "CRISPR screen", visibility: "public" }, + { + id: "dataset-neuro-1", + type: "dataset", + title: "Neuro screen dataset", + visibility: "institutional", + institutionId: "north-lab", + }, + { + id: "paper-embargo", + type: "paper", + title: "Embargoed methods paper", + visibility: "embargoed", + embargoUntil: "2026-06-01T00:00:00Z", + }, + { + id: "restricted-cohort", + type: "dataset", + title: "Human cohort export", + visibility: "restricted", + requiredConsentId: "consent-human-1", + licenseId: "license-cohort-1", + clearanceTag: "irb-approved", + }, + ], + evidence: [ + { id: "ev-open", access: "public" }, + { id: "ev-institution", access: "institutional", institutionId: "north-lab" }, + { + id: "ev-restricted", + access: "restricted", + requiredConsentId: "consent-human-1", + licenseId: "license-cohort-1", + clearanceTag: "irb-approved", + }, + ], + recommendations: [ + { + id: "rec-visible", + title: "Public concept to institutional dataset route", + score: 0.91, + nodeIds: ["concept-crispr", "dataset-neuro-1"], + evidenceIds: ["ev-open", "ev-institution"], + }, + { + id: "rec-embargo", + title: "Embargoed method recommendation", + score: 0.95, + nodeIds: ["paper-embargo"], + evidenceIds: ["ev-open"], + }, + { + id: "rec-restricted", + title: "Restricted cohort reuse recommendation", + score: 0.86, + nodeIds: ["restricted-cohort"], + evidenceIds: ["ev-restricted"], + }, + ], +}) + +console.log("Knowledge Graph Recommendation Visibility Guard Demo") +console.log("===================================================") +console.log(`status: ${result.status}`) +console.log(`visible recommendations: ${result.visibleCount}`) +console.log(`suppressed recommendations: ${result.suppressedCount}`) +console.log(`first visible: ${result.visibleRecommendations[0].title}`) +console.log(`first suppressed reason: ${result.suppressedRecommendations[0].blockers[0]}`) +console.log(`curator action: ${result.curatorActions[0].action}`) +console.log(`digest: ${result.auditDigest.slice(0, 16)}...`) diff --git a/knowledge-graph-recommendation-visibility-guard/demo.mp4 b/knowledge-graph-recommendation-visibility-guard/demo.mp4 new file mode 100644 index 0000000000000000000000000000000000000000..ba1bd6ae57c15562005c5877761dffebf46d36f6 GIT binary patch literal 48510 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 z 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 THHHxdKy!HuG?zRuYx(~G^FI46 literal 0 HcmV?d00001 diff --git a/knowledge-graph-recommendation-visibility-guard/demo.svg b/knowledge-graph-recommendation-visibility-guard/demo.svg new file mode 100644 index 0000000..777a7e7 --- /dev/null +++ b/knowledge-graph-recommendation-visibility-guard/demo.svg @@ -0,0 +1,29 @@ + + Knowledge Graph Recommendation Visibility Guard + A visual summary showing public, institutional, restricted, and embargoed graph recommendations being evaluated before display. + + + Knowledge Graph Recommendation Visibility Guard + Suppress private, embargoed, and restricted graph paths before discovery surfaces expose them. + + + Visible + Public + institution-safe + + + + Review + Expired embargo metadata + + + + Suppressed + Embargo or missing access + + + + Recommendation path + node visibility + evidence access + viewer context + audit digest + + Output: visible recommendations, suppressed paths, curator actions, stable digest + diff --git a/knowledge-graph-recommendation-visibility-guard/index.js b/knowledge-graph-recommendation-visibility-guard/index.js new file mode 100644 index 0000000..b123581 --- /dev/null +++ b/knowledge-graph-recommendation-visibility-guard/index.js @@ -0,0 +1,222 @@ +"use strict" + +const crypto = require("node:crypto") + +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 asSet(value) { + return new Set(Array.isArray(value) ? value : []) +} + +function toTime(value) { + if (!value) return null + const time = new Date(value).getTime() + if (Number.isNaN(time)) return null + return time +} + +function normalizeActor(actor = {}) { + return { + id: actor.id || "anonymous", + role: actor.role || "viewer", + institutionId: actor.institutionId || null, + projectIds: asSet(actor.projectIds), + consentIds: asSet(actor.consentIds), + licenseIds: asSet(actor.licenseIds), + clearanceTags: asSet(actor.clearanceTags), + } +} + +function normalizeNode(node) { + return { + id: node.id, + type: node.type || "artifact", + title: node.title || node.id, + visibility: node.visibility || "public", + institutionId: node.institutionId || null, + projectId: node.projectId || null, + embargoUntil: node.embargoUntil || null, + requiredConsentId: node.requiredConsentId || null, + licenseId: node.licenseId || null, + clearanceTag: node.clearanceTag || null, + status: node.status || "active", + } +} + +function normalizeEvidence(evidence) { + return { + id: evidence.id, + access: evidence.access || "public", + institutionId: evidence.institutionId || null, + projectId: evidence.projectId || null, + embargoUntil: evidence.embargoUntil || null, + requiredConsentId: evidence.requiredConsentId || null, + licenseId: evidence.licenseId || null, + clearanceTag: evidence.clearanceTag || null, + status: evidence.status || "active", + } +} + +function evaluateAccess(item, actor, nowTime, prefix) { + const blockers = [] + const warnings = [] + const label = `${prefix} ${item.id}` + const embargoTime = toTime(item.embargoUntil) + + if (item.status === "retracted" || item.status === "withdrawn") { + blockers.push(`${label} is ${item.status}`) + } + + const visibility = item.visibility || item.access || "public" + if (visibility === "institutional" && item.institutionId && item.institutionId !== actor.institutionId) { + blockers.push(`${label} requires institution ${item.institutionId}`) + } + + if (visibility === "project" && item.projectId && !actor.projectIds.has(item.projectId)) { + blockers.push(`${label} requires project ${item.projectId} membership`) + } + + if (visibility === "private" && !["owner", "admin", "curator"].includes(actor.role)) { + blockers.push(`${label} is private`) + } + + if (visibility === "embargoed") { + if (!embargoTime) { + blockers.push(`${label} has an invalid embargo date`) + } else if (nowTime < embargoTime && !["admin", "curator"].includes(actor.role)) { + blockers.push(`${label} is embargoed until ${item.embargoUntil}`) + } else if (nowTime >= embargoTime) { + warnings.push(`${label} embargo has expired; confirm public release metadata`) + } + } + + if (visibility === "restricted" || item.requiredConsentId || item.licenseId || item.clearanceTag) { + if (item.requiredConsentId && !actor.consentIds.has(item.requiredConsentId)) { + blockers.push(`${label} requires consent ${item.requiredConsentId}`) + } + if (item.licenseId && !actor.licenseIds.has(item.licenseId)) { + blockers.push(`${label} requires license ${item.licenseId}`) + } + if (item.clearanceTag && !actor.clearanceTags.has(item.clearanceTag)) { + blockers.push(`${label} requires clearance ${item.clearanceTag}`) + } + } + + return { blockers, warnings } +} + +function evaluateRecommendation(recommendation, context) { + const blockers = [] + const warnings = [] + const pathNodes = [] + const evidence = [] + + for (const nodeId of recommendation.nodeIds || []) { + const node = context.nodes.get(nodeId) + if (!node) { + blockers.push(`missing graph node ${nodeId}`) + continue + } + pathNodes.push(node) + const access = evaluateAccess(node, context.actor, context.nowTime, "node") + blockers.push(...access.blockers) + warnings.push(...access.warnings) + } + + for (const evidenceId of recommendation.evidenceIds || []) { + const item = context.evidence.get(evidenceId) + if (!item) { + blockers.push(`missing evidence ${evidenceId}`) + continue + } + evidence.push(item) + const access = evaluateAccess(item, context.actor, context.nowTime, "evidence") + blockers.push(...access.blockers) + warnings.push(...access.warnings) + } + + const distinctBlockers = [...new Set(blockers)] + const distinctWarnings = [...new Set(warnings)] + const safeScore = Number(recommendation.score || 0) - distinctWarnings.length * 0.05 + + return { + id: recommendation.id, + title: recommendation.title || recommendation.id, + status: distinctBlockers.length > 0 ? "suppressed" : "visible", + safeScore: Number(Math.max(0, safeScore).toFixed(3)), + nodeIds: pathNodes.map((node) => node.id), + evidenceIds: evidence.map((item) => item.id), + blockers: distinctBlockers, + warnings: distinctWarnings, + explanation: + distinctBlockers.length > 0 + ? "Recommendation is hidden until visibility and evidence access issues are resolved." + : "Recommendation can be shown with the attached visibility explanation.", + } +} + +function assessRecommendationVisibility(input) { + const nowTime = toTime(input.now) || Date.now() + const actor = normalizeActor(input.actor) + const nodes = new Map((input.nodes || []).map((node) => normalizeNode(node)).map((node) => [node.id, node])) + const evidence = new Map( + (input.evidence || []).map((item) => normalizeEvidence(item)).map((item) => [item.id, item]), + ) + + const evaluated = (input.recommendations || []) + .map((recommendation) => evaluateRecommendation(recommendation, { actor, nodes, evidence, nowTime })) + .sort((a, b) => b.safeScore - a.safeScore || a.id.localeCompare(b.id)) + + const visibleRecommendations = evaluated.filter((item) => item.status === "visible") + const suppressedRecommendations = evaluated.filter((item) => item.status === "suppressed") + const curatorActions = suppressedRecommendations.map((item) => ({ + recommendationId: item.id, + action: "resolve-access-before-discovery", + reason: item.blockers[0], + })) + + if (visibleRecommendations.some((item) => item.warnings.length > 0)) { + curatorActions.push({ + recommendationId: "visible-warnings", + action: "confirm-release-metadata", + reason: "some visible recommendations include expired embargo or metadata warnings", + }) + } + + const result = { + status: + suppressedRecommendations.length === 0 + ? visibleRecommendations.some((item) => item.warnings.length > 0) + ? "needs-review" + : "ready" + : "guarded", + actorId: actor.id, + visibleCount: visibleRecommendations.length, + suppressedCount: suppressedRecommendations.length, + visibleRecommendations, + suppressedRecommendations, + curatorActions, + } + + return { + ...result, + auditDigest: digest(result), + } +} + +module.exports = { + assessRecommendationVisibility, +} diff --git a/knowledge-graph-recommendation-visibility-guard/requirements-map.md b/knowledge-graph-recommendation-visibility-guard/requirements-map.md new file mode 100644 index 0000000..72b14af --- /dev/null +++ b/knowledge-graph-recommendation-visibility-guard/requirements-map.md @@ -0,0 +1,13 @@ +# Requirements Map + +| Issue #17 requirement | Coverage in this module | +| --- | --- | +| Knowledge navigation across authors, concepts, tools, datasets, protocols, and funders | Evaluates recommendation paths before they appear in graph navigation or entity pages. | +| Filters by institution, time, reproducibility, and related context | Applies institution, project, embargo-time, and evidence-access checks to each path. | +| AI research recommendations | Guards sidebar, digest, and discovery-mode recommendations so unsafe paths are suppressed. | +| Discover related work and influence pathways | Keeps visible recommendations explainable with attached node and evidence IDs. | +| Build trust in structured scientific intelligence | Emits curator actions and a deterministic audit digest for review and governance. | + +## Non-Overlap Note + +This submission is distinct from broad knowledge graph extractors, graph navigators, link audits, knowledge-gap explorers, ontology drift migration, relationship conflict arbitration, author-affiliation disambiguation, artifact reuse lineage, evidence freshness checks, instrument-method compatibility graphs, and reproducibility route planners. It focuses specifically on visibility, embargo, consent, license, and clearance gating before graph recommendations are displayed. diff --git a/knowledge-graph-recommendation-visibility-guard/test.js b/knowledge-graph-recommendation-visibility-guard/test.js new file mode 100644 index 0000000..46f962d --- /dev/null +++ b/knowledge-graph-recommendation-visibility-guard/test.js @@ -0,0 +1,152 @@ +"use strict" + +const assert = require("node:assert/strict") +const { assessRecommendationVisibility } = require("./index") + +const baseNodes = [ + { id: "concept-crispr", type: "concept", title: "CRISPR screen", visibility: "public" }, + { + id: "dataset-neuro-1", + type: "dataset", + title: "Neuro screen dataset", + visibility: "institutional", + institutionId: "north-lab", + }, + { + id: "paper-embargo", + type: "paper", + title: "Embargoed methods paper", + visibility: "embargoed", + embargoUntil: "2026-06-01T00:00:00Z", + }, + { + id: "restricted-cohort", + type: "dataset", + title: "Human cohort export", + visibility: "restricted", + requiredConsentId: "consent-human-1", + licenseId: "license-cohort-1", + clearanceTag: "irb-approved", + }, +] + +const baseEvidence = [ + { id: "ev-open", access: "public" }, + { id: "ev-institution", access: "institutional", institutionId: "north-lab" }, + { + id: "ev-restricted", + access: "restricted", + requiredConsentId: "consent-human-1", + licenseId: "license-cohort-1", + clearanceTag: "irb-approved", + }, +] + +{ + const result = assessRecommendationVisibility({ + now: "2026-05-20T10:00:00Z", + actor: { id: "ada", role: "researcher", institutionId: "north-lab" }, + nodes: baseNodes, + evidence: baseEvidence, + recommendations: [ + { + id: "rec-safe", + title: "CRISPR dataset to protocol route", + score: 0.91, + nodeIds: ["concept-crispr", "dataset-neuro-1"], + evidenceIds: ["ev-open", "ev-institution"], + }, + { + id: "rec-embargo", + title: "Embargoed method recommendation", + score: 0.95, + nodeIds: ["paper-embargo"], + evidenceIds: ["ev-open"], + }, + ], + }) + + assert.equal(result.status, "guarded") + assert.equal(result.visibleCount, 1) + assert.equal(result.suppressedCount, 1) + assert.equal(result.visibleRecommendations[0].id, "rec-safe") + assert.ok(result.suppressedRecommendations[0].blockers[0].includes("embargoed until")) + assert.match(result.auditDigest, /^[0-9a-f]{64}$/) +} + +{ + const result = assessRecommendationVisibility({ + now: "2026-05-20T10:00:00Z", + actor: { + id: "ben", + role: "researcher", + institutionId: "north-lab", + consentIds: ["consent-human-1"], + licenseIds: ["license-cohort-1"], + clearanceTags: ["irb-approved"], + }, + nodes: baseNodes, + evidence: baseEvidence, + recommendations: [ + { + id: "rec-restricted", + title: "Restricted cohort reuse recommendation", + score: 0.82, + nodeIds: ["restricted-cohort"], + evidenceIds: ["ev-restricted"], + }, + ], + }) + + assert.equal(result.status, "ready") + assert.equal(result.visibleCount, 1) + assert.equal(result.suppressedCount, 0) +} + +{ + const result = assessRecommendationVisibility({ + now: "2026-07-01T10:00:00Z", + actor: { id: "cy", role: "researcher", institutionId: "north-lab" }, + nodes: baseNodes, + evidence: baseEvidence, + recommendations: [ + { + id: "rec-expired-embargo", + title: "Recently released methods route", + score: 0.77, + nodeIds: ["paper-embargo"], + evidenceIds: ["ev-open"], + }, + ], + }) + + assert.equal(result.status, "needs-review") + assert.equal(result.visibleCount, 1) + assert.ok(result.visibleRecommendations[0].warnings.some((warning) => warning.includes("embargo has expired"))) + assert.equal(result.curatorActions[0].action, "confirm-release-metadata") +} + +{ + const result = assessRecommendationVisibility({ + now: "2026-05-20T10:00:00Z", + actor: { id: "dee", role: "researcher", institutionId: "south-lab" }, + nodes: baseNodes, + evidence: baseEvidence, + recommendations: [ + { + id: "rec-missing", + title: "Broken recommendation", + score: 0.9, + nodeIds: ["dataset-neuro-1", "missing-node"], + evidenceIds: ["missing-evidence"], + }, + ], + }) + + assert.equal(result.status, "guarded") + assert.ok(result.suppressedRecommendations[0].blockers.includes("node dataset-neuro-1 requires institution north-lab")) + assert.ok(result.suppressedRecommendations[0].blockers.includes("missing graph node missing-node")) + assert.ok(result.suppressedRecommendations[0].blockers.includes("missing evidence missing-evidence")) +} + +console.log("knowledge-graph-recommendation-visibility-guard tests passed")