From ba86bfaeacc95f03d0f80ed6fbc74e401d7eb4dc Mon Sep 17 00:00:00 2001 From: cw <88217123+2bf@users.noreply.github.com> Date: Tue, 19 May 2026 18:18:10 -0700 Subject: [PATCH] Add knowledge graph reproducibility routes --- .../README.md | 48 ++++ .../demo.js | 121 +++++++++ .../demo.mp4 | Bin 0 -> 60733 bytes .../demo.svg | 13 + .../index.js | 257 ++++++++++++++++++ .../test.js | 196 +++++++++++++ 6 files changed, 635 insertions(+) create mode 100644 knowledge-graph-reproducibility-routes/README.md create mode 100644 knowledge-graph-reproducibility-routes/demo.js create mode 100644 knowledge-graph-reproducibility-routes/demo.mp4 create mode 100644 knowledge-graph-reproducibility-routes/demo.svg create mode 100644 knowledge-graph-reproducibility-routes/index.js create mode 100644 knowledge-graph-reproducibility-routes/test.js diff --git a/knowledge-graph-reproducibility-routes/README.md b/knowledge-graph-reproducibility-routes/README.md new file mode 100644 index 0000000..4656010 --- /dev/null +++ b/knowledge-graph-reproducibility-routes/README.md @@ -0,0 +1,48 @@ +# Knowledge Graph Reproducibility Routes + +Self-contained milestone for SCIBASE.AI issue #17, Scientific Knowledge Graph +Integration. + +This module turns typed knowledge-graph entities into reproducibility-oriented +navigation paths. A route connects a concept or method to protocols, datasets, +notebooks, software, and result artifacts, then scores whether the path is +ready for a researcher to rerun or reuse. + +## What It Covers + +- Typed graph nodes for concepts, protocols, datasets, notebooks, software, + results, projects, and authors. +- Evidence-backed relationships with freshness, access, citation, and + reproducibility metadata. +- Route scoring that rewards strong evidence and executable rerun readiness. +- Blocker and curator action output for weak, stale, private, or non-runnable + graph paths. +- Recommendation digests for project sidebars, discovery mode, and weekly + knowledge graph summaries. +- Synthetic sample data only; no network calls, credentials, or external + services. + +## Files + +- `index.js` - route scoring and recommendation engine. +- `demo.js` - terminal demo for candidate route ranking. +- `test.js` - dependency-free regression tests. +- `demo.mp4` - short demo artifact for bounty review. + +## Run + +```sh +node knowledge-graph-reproducibility-routes/test.js +node knowledge-graph-reproducibility-routes/demo.js +``` + +## Requirement Map + +| Issue #17 Requirement | Implementation | +| --- | --- | +| Entity extraction and linked data | The module consumes typed entity nodes with evidence and ontology metadata, then emits route packets with stable audit digests. | +| Knowledge navigation | `findReproducibilityRoutes()` supports concept-to-result and notebook-reuse style graph journeys with domain/time/access filters. | +| Dynamic node types | Sample and tests include concepts, protocols, datasets, notebooks, software, results, projects, and authors. | +| Filters by domain, time, citation count, reproducibility | Route filtering uses `domain`, `minCitationCount`, `maxEvidenceAgeDays`, `access`, and `minScore`. | +| AI research recommendations | `buildRecommendationDigest()` converts ranked graph paths into sidebar/discovery recommendations with reasons and blockers. | +| Entity pages and usage contexts | Route packets include `path`, `usageContexts`, `evidence`, and `curatorActions` for entity pages and graph drilldowns. | diff --git a/knowledge-graph-reproducibility-routes/demo.js b/knowledge-graph-reproducibility-routes/demo.js new file mode 100644 index 0000000..c96a77f --- /dev/null +++ b/knowledge-graph-reproducibility-routes/demo.js @@ -0,0 +1,121 @@ +const { buildRecommendationDigest, findReproducibilityRoutes } = require("./index") + +const graph = { + nodes: [ + { id: "concept-crispr", type: "concept", label: "CRISPR perturbation", domain: "biology" }, + { id: "protocol-10x", type: "protocol", label: "10x guide capture protocol", domain: "biology" }, + { id: "dataset-neuro", type: "dataset", label: "Neuroscience perturb-seq dataset", domain: "biology" }, + { id: "notebook-cluster", type: "notebook", label: "Clustering replay notebook", domain: "biology" }, + { id: "software-scanpy", type: "software", label: "Scanpy workflow", domain: "biology" }, + { id: "result-cell-state", type: "result", label: "Cell-state transition map", domain: "biology" }, + { id: "dataset-private", type: "dataset", label: "Restricted validation cohort", domain: "biology" }, + { id: "result-private", type: "result", label: "Embargoed validation result", domain: "biology" }, + ], + edges: [ + { + id: "e1", + from: "concept-crispr", + to: "protocol-10x", + relation: "uses_protocol", + evidenceStrength: 0.92, + citationCount: 180, + evidenceAgeDays: 45, + access: "open", + usageContexts: ["protocol entity page", "project sidebar"], + }, + { + id: "e2", + from: "protocol-10x", + to: "dataset-neuro", + relation: "generated_dataset", + evidenceStrength: 0.9, + citationCount: 120, + evidenceAgeDays: 80, + access: "open", + reproducibilityVerified: true, + usageContexts: ["dataset reuse"], + }, + { + id: "e3", + from: "dataset-neuro", + to: "notebook-cluster", + relation: "reproduced_by", + evidenceStrength: 0.88, + citationCount: 90, + evidenceAgeDays: 22, + access: "open", + executable: true, + requiresRerun: true, + reproducibilityVerified: true, + usageContexts: ["run analysis"], + }, + { + id: "e4", + from: "notebook-cluster", + to: "software-scanpy", + relation: "depends_on", + evidenceStrength: 0.8, + citationCount: 60, + evidenceAgeDays: 18, + access: "open", + executable: true, + usageContexts: ["environment plan"], + }, + { + id: "e5", + from: "software-scanpy", + to: "result-cell-state", + relation: "produces_result", + evidenceStrength: 0.87, + citationCount: 75, + evidenceAgeDays: 18, + access: "open", + executable: true, + reproducibilityVerified: true, + usageContexts: ["result entity page"], + }, + { + id: "e6", + from: "concept-crispr", + to: "dataset-private", + relation: "validated_by", + evidenceStrength: 0.56, + citationCount: 12, + evidenceAgeDays: 500, + access: "restricted", + usageContexts: ["curator review"], + }, + { + id: "e7", + from: "dataset-private", + to: "result-private", + relation: "supports_result", + evidenceStrength: 0.48, + citationCount: 8, + evidenceAgeDays: 510, + access: "private", + requiresRerun: true, + executable: false, + usageContexts: ["curator review"], + }, + ], +} + +const routes = findReproducibilityRoutes(graph, { + startId: "concept-crispr", + targetTypes: ["result"], + maxDepth: 5, + limit: 5, +}) + +const digest = buildRecommendationDigest(graph, { + startId: "concept-crispr", + targetTypes: ["result"], + maxDepth: 5, + context: "project_sidebar", +}) + +console.log("Ranked reproducibility routes") +console.log(JSON.stringify(routes, null, 2)) +console.log("\nRecommendation digest") +console.log(JSON.stringify(digest, null, 2)) diff --git a/knowledge-graph-reproducibility-routes/demo.mp4 b/knowledge-graph-reproducibility-routes/demo.mp4 new file mode 100644 index 0000000000000000000000000000000000000000..179cd2963260f97c2f8ba3dcc30aa1d42b147b58 GIT binary patch literal 60733 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 + Knowledge graph reproducibility routes demo slide + Demo summary for reproducibility route ranking and recommendation digest output. + + + SCIBASE Knowledge Graph + Reproducibility Route Planner + Ranks concept -> protocol -> dataset -> notebook -> result paths + Scores evidence strength, freshness, access, and rerun readiness + Produces sidebar and discovery-mode recommendation digests + Validation - node test.js - 6 tests passed + Dependency-free, synthetic graph data only, deterministic audit digests. + diff --git a/knowledge-graph-reproducibility-routes/index.js b/knowledge-graph-reproducibility-routes/index.js new file mode 100644 index 0000000..92156e6 --- /dev/null +++ b/knowledge-graph-reproducibility-routes/index.js @@ -0,0 +1,257 @@ +const crypto = require("node:crypto") + +const NODE_WEIGHTS = { + concept: 0.7, + protocol: 0.9, + dataset: 1, + notebook: 1, + software: 0.85, + result: 1, + project: 0.75, + author: 0.55, +} + +function stableJson(value) { + if (Array.isArray(value)) return `[${value.map(stableJson).join(",")}]` + if (value && typeof value === "object") { + return `{${Object.keys(value) + .sort() + .map((key) => `${JSON.stringify(key)}:${stableJson(value[key])}`) + .join(",")}}` + } + return JSON.stringify(value) +} + +function digest(value) { + return crypto.createHash("sha256").update(stableJson(value)).digest("hex") +} + +function indexById(items) { + return new Map(items.map((item) => [item.id, item])) +} + +function edgeFreshnessScore(edge) { + if (edge.retracted) return 0 + if (edge.evidenceAgeDays == null) return 0.65 + if (edge.evidenceAgeDays <= 30) return 1 + if (edge.evidenceAgeDays <= 180) return 0.85 + if (edge.evidenceAgeDays <= 365) return 0.65 + return 0.4 +} + +function edgeAccessScore(edge) { + if (edge.access === "open") return 1 + if (edge.access === "registered") return 0.8 + if (edge.access === "restricted") return 0.45 + if (edge.access === "private") return 0.2 + return 0.6 +} + +function edgeEvidenceScore(edge) { + const base = (edge.evidenceStrength ?? 0.5) * 0.72 + const citationBoost = Math.min((edge.citationCount ?? 0) / 250, 1) * 0.1 + const reproducibilityBoost = edge.reproducibilityVerified ? 0.11 : 0 + const executableBoost = edge.executable ? 0.07 : 0 + return Math.min(base + citationBoost + reproducibilityBoost + executableBoost, 1) +} + +function scoreEdge(edge) { + const evidence = edgeEvidenceScore(edge) + const freshness = edgeFreshnessScore(edge) + const access = edgeAccessScore(edge) + return Number((evidence * 0.5 + freshness * 0.3 + access * 0.2).toFixed(4)) +} + +function edgeBlockers(edge) { + const blockers = [] + if (edge.retracted) blockers.push("evidence_retracted") + if (edge.evidenceAgeDays > 365) blockers.push("stale_evidence") + if (edge.access === "private" || edge.access === "restricted") blockers.push("access_limited") + if (edge.requiresRerun && !edge.executable) blockers.push("rerun_not_executable") + if ((edge.evidenceStrength ?? 0) < 0.5) blockers.push("weak_evidence") + return blockers +} + +function buildAdjacency(edges) { + const adjacency = new Map() + for (const edge of edges) { + if (!adjacency.has(edge.from)) adjacency.set(edge.from, []) + adjacency.get(edge.from).push(edge) + } + return adjacency +} + +function walkRoutes(graph, options) { + const nodes = indexById(graph.nodes) + const adjacency = buildAdjacency(graph.edges) + const maxDepth = options.maxDepth ?? 5 + const queue = [{ nodeId: options.startId, edges: [], seen: new Set([options.startId]) }] + const routes = [] + + while (queue.length > 0) { + const current = queue.shift() + const node = nodes.get(current.nodeId) + if (!node) continue + + const isTarget = + current.edges.length > 0 && + (!options.targetTypes || options.targetTypes.includes(node.type)) && + (!options.targetId || current.nodeId === options.targetId) + + if (isTarget) routes.push(current) + if (current.edges.length >= maxDepth) continue + + for (const edge of adjacency.get(current.nodeId) || []) { + if (current.seen.has(edge.to)) continue + const next = nodes.get(edge.to) + if (!next) continue + if (options.domain && next.domain && next.domain !== options.domain) continue + queue.push({ + nodeId: edge.to, + edges: [...current.edges, edge], + seen: new Set([...current.seen, edge.to]), + }) + } + } + + return routes +} + +function scoreRoute(route, nodes) { + const edgeScores = route.edges.map(scoreEdge) + const nodeScores = route.edges.map((edge) => NODE_WEIGHTS[nodes.get(edge.to)?.type] ?? 0.6) + const meanEdgeScore = edgeScores.reduce((sum, score) => sum + score, 0) / edgeScores.length + const meanNodeScore = nodeScores.reduce((sum, score) => sum + score, 0) / nodeScores.length + const lengthPenalty = Math.max(0, (route.edges.length - 3) * 0.04) + const blockers = route.edges.flatMap(edgeBlockers) + const blockerPenalty = Math.min(blockers.length * 0.08, 0.4) + return Number(Math.max(meanEdgeScore * 0.72 + meanNodeScore * 0.28 - lengthPenalty - blockerPenalty, 0).toFixed(4)) +} + +function routeCuratorActions(route) { + const actions = [] + for (const edge of route.edges) { + for (const blocker of edgeBlockers(edge)) { + if (blocker === "evidence_retracted") { + actions.push(`Suppress ${edge.from}->${edge.to} until retraction is reviewed.`) + } + if (blocker === "stale_evidence") { + actions.push(`Refresh evidence for ${edge.from}->${edge.to}.`) + } + if (blocker === "access_limited") { + actions.push(`Request access policy review for ${edge.to}.`) + } + if (blocker === "rerun_not_executable") { + actions.push(`Add executable environment metadata for ${edge.to}.`) + } + if (blocker === "weak_evidence") { + actions.push(`Add stronger supporting citation or artifact evidence for ${edge.from}->${edge.to}.`) + } + } + } + return [...new Set(actions)] +} + +function formatRoute(route, graph) { + const nodes = indexById(graph.nodes) + const pathIds = [route.edges[0].from, ...route.edges.map((edge) => edge.to)] + const path = pathIds.map((id) => { + const node = nodes.get(id) + return { + id, + type: node?.type, + label: node?.label, + domain: node?.domain, + } + }) + const blockers = [...new Set(route.edges.flatMap(edgeBlockers))] + const packet = { + routeId: digest({ pathIds, edges: route.edges.map((edge) => edge.id) }).slice(0, 16), + path, + score: scoreRoute(route, nodes), + blockers, + usageContexts: route.edges.flatMap((edge) => edge.usageContexts || []), + evidence: route.edges.map((edge) => ({ + id: edge.id, + relation: edge.relation, + from: edge.from, + to: edge.to, + score: scoreEdge(edge), + citationCount: edge.citationCount ?? 0, + evidenceAgeDays: edge.evidenceAgeDays ?? null, + access: edge.access || "unknown", + executable: Boolean(edge.executable), + reproducibilityVerified: Boolean(edge.reproducibilityVerified), + })), + curatorActions: routeCuratorActions(route), + } + + return { + ...packet, + auditDigest: digest(packet), + } +} + +function findReproducibilityRoutes(graph, options) { + if (!options?.startId) throw new Error("startId is required") + const filters = options.filters || {} + const rawRoutes = walkRoutes(graph, { + startId: options.startId, + targetId: options.targetId, + targetTypes: options.targetTypes || ["result", "notebook", "dataset", "project"], + maxDepth: options.maxDepth, + domain: filters.domain, + }) + + return rawRoutes + .map((route) => formatRoute(route, graph)) + .filter((route) => route.score >= (filters.minScore ?? 0)) + .filter((route) => + filters.access ? route.evidence.every((evidence) => evidence.access === filters.access) : true, + ) + .filter((route) => + filters.minCitationCount + ? route.evidence.some((evidence) => evidence.citationCount >= filters.minCitationCount) + : true, + ) + .filter((route) => + filters.maxEvidenceAgeDays + ? route.evidence.every( + (evidence) => + evidence.evidenceAgeDays == null || evidence.evidenceAgeDays <= filters.maxEvidenceAgeDays, + ) + : true, + ) + .sort((a, b) => b.score - a.score) + .slice(0, options.limit ?? 10) +} + +function buildRecommendationDigest(graph, options) { + const routes = findReproducibilityRoutes(graph, options) + return { + startId: options.startId, + generatedFor: options.context || "discovery_mode", + totalRoutes: routes.length, + recommendations: routes.map((route) => ({ + routeId: route.routeId, + headline: `${route.path[0].label} -> ${route.path.at(-1).label}`, + score: route.score, + reason: + route.blockers.length === 0 + ? "High-confidence reproducibility path with reusable evidence." + : "Useful path with curator actions before full reuse.", + blockers: route.blockers, + curatorActions: route.curatorActions, + path: route.path.map((node) => `${node.type}:${node.label}`), + })), + auditDigest: digest(routes.map((route) => route.auditDigest)), + } +} + +module.exports = { + buildRecommendationDigest, + digest, + findReproducibilityRoutes, + scoreEdge, + stableJson, +} diff --git a/knowledge-graph-reproducibility-routes/test.js b/knowledge-graph-reproducibility-routes/test.js new file mode 100644 index 0000000..f3a63b3 --- /dev/null +++ b/knowledge-graph-reproducibility-routes/test.js @@ -0,0 +1,196 @@ +const assert = require("node:assert/strict") +const { + buildRecommendationDigest, + digest, + findReproducibilityRoutes, + scoreEdge, +} = require("./index") + +function sampleGraph() { + return { + nodes: [ + { id: "concept-a", type: "concept", label: "Graph neural biomarker", domain: "biology" }, + { id: "dataset-open", type: "dataset", label: "Open cohort", domain: "biology" }, + { id: "notebook-open", type: "notebook", label: "Executable notebook", domain: "biology" }, + { id: "result-open", type: "result", label: "Validated signature", domain: "biology" }, + { id: "dataset-stale", type: "dataset", label: "Stale cohort", domain: "biology" }, + { id: "result-stale", type: "result", label: "Stale finding", domain: "biology" }, + { id: "dataset-physics", type: "dataset", label: "Physics cohort", domain: "physics" }, + { id: "result-physics", type: "result", label: "Physics result", domain: "physics" }, + ], + edges: [ + { + id: "open-1", + from: "concept-a", + to: "dataset-open", + relation: "supported_by", + evidenceStrength: 0.9, + evidenceAgeDays: 20, + citationCount: 300, + access: "open", + reproducibilityVerified: true, + }, + { + id: "open-2", + from: "dataset-open", + to: "notebook-open", + relation: "reproduced_by", + evidenceStrength: 0.88, + evidenceAgeDays: 15, + citationCount: 110, + access: "open", + executable: true, + requiresRerun: true, + reproducibilityVerified: true, + }, + { + id: "open-3", + from: "notebook-open", + to: "result-open", + relation: "produces", + evidenceStrength: 0.86, + evidenceAgeDays: 10, + citationCount: 80, + access: "open", + executable: true, + reproducibilityVerified: true, + }, + { + id: "stale-1", + from: "concept-a", + to: "dataset-stale", + relation: "supported_by", + evidenceStrength: 0.45, + evidenceAgeDays: 800, + citationCount: 4, + access: "restricted", + }, + { + id: "stale-2", + from: "dataset-stale", + to: "result-stale", + relation: "produces", + evidenceStrength: 0.4, + evidenceAgeDays: 820, + citationCount: 1, + access: "private", + requiresRerun: true, + executable: false, + }, + { + id: "physics-1", + from: "concept-a", + to: "dataset-physics", + relation: "analogous_to", + evidenceStrength: 0.9, + evidenceAgeDays: 10, + citationCount: 50, + access: "open", + }, + { + id: "physics-2", + from: "dataset-physics", + to: "result-physics", + relation: "produces", + evidenceStrength: 0.9, + evidenceAgeDays: 10, + citationCount: 50, + access: "open", + executable: true, + }, + ], + } +} + +function testScoresStrongExecutableOpenEdgesHighest() { + const strong = scoreEdge({ + evidenceStrength: 0.9, + evidenceAgeDays: 10, + citationCount: 200, + access: "open", + executable: true, + reproducibilityVerified: true, + }) + const weak = scoreEdge({ + evidenceStrength: 0.35, + evidenceAgeDays: 900, + citationCount: 0, + access: "private", + executable: false, + }) + + assert.ok(strong > 0.9) + assert.ok(weak < 0.45) +} + +function testFindsRankedReproducibilityRoute() { + const [best] = findReproducibilityRoutes(sampleGraph(), { + startId: "concept-a", + targetTypes: ["result"], + maxDepth: 4, + }) + + assert.equal(best.path.at(-1).id, "result-open") + assert.equal(best.blockers.length, 0) + assert.ok(best.score > 0.85) +} + +function testWeakPrivateRouteProducesCuratorActions() { + const routes = findReproducibilityRoutes(sampleGraph(), { + startId: "concept-a", + targetId: "result-stale", + targetTypes: ["result"], + maxDepth: 3, + }) + + assert.equal(routes.length, 1) + assert.ok(routes[0].blockers.includes("stale_evidence")) + assert.ok(routes[0].blockers.includes("access_limited")) + assert.ok(routes[0].blockers.includes("rerun_not_executable")) + assert.ok(routes[0].curatorActions.some((action) => action.includes("Refresh evidence"))) +} + +function testDomainFilterSuppressesOtherDomains() { + const routes = findReproducibilityRoutes(sampleGraph(), { + startId: "concept-a", + targetTypes: ["result"], + filters: { domain: "biology" }, + maxDepth: 3, + }) + + assert.equal(routes.some((route) => route.path.at(-1).id === "result-physics"), false) +} + +function testRecommendationDigestExplainsRoutes() { + const digestPacket = buildRecommendationDigest(sampleGraph(), { + startId: "concept-a", + targetTypes: ["result"], + maxDepth: 4, + context: "weekly_digest", + }) + + assert.equal(digestPacket.generatedFor, "weekly_digest") + assert.ok(digestPacket.recommendations.length >= 2) + assert.match(digestPacket.recommendations[0].headline, /Graph neural biomarker/) + assert.equal(typeof digestPacket.auditDigest, "string") +} + +function testStableDigestIgnoresObjectKeyOrder() { + assert.equal(digest({ b: 2, a: 1 }), digest({ a: 1, b: 2 })) +} + +const tests = [ + testScoresStrongExecutableOpenEdgesHighest, + testFindsRankedReproducibilityRoute, + testWeakPrivateRouteProducesCuratorActions, + testDomainFilterSuppressesOtherDomains, + testRecommendationDigestExplainsRoutes, + testStableDigestIgnoresObjectKeyOrder, +] + +for (const test of tests) { + test() + console.log(`ok - ${test.name}`) +} + +console.log(`${tests.length} tests passed`)