From bbf243da7785805d022258367cc555c172493349 Mon Sep 17 00:00:00 2001 From: Kyle Tree Date: Wed, 20 May 2026 03:42:10 -0700 Subject: [PATCH] Add negative evidence replication graph --- README.md | 4 + negative-evidence-replication-graph/README.md | 32 ++ .../demo-output.json | 216 +++++++++ negative-evidence-replication-graph/demo.js | 31 ++ negative-evidence-replication-graph/demo.mp4 | Bin 0 -> 36366 bytes negative-evidence-replication-graph/demo.svg | 1 + negative-evidence-replication-graph/index.js | 410 ++++++++++++++++++ .../requirements-map.md | 21 + .../sample-data.js | 194 +++++++++ negative-evidence-replication-graph/test.js | 56 +++ 10 files changed, 965 insertions(+) create mode 100644 negative-evidence-replication-graph/README.md create mode 100644 negative-evidence-replication-graph/demo-output.json create mode 100644 negative-evidence-replication-graph/demo.js create mode 100644 negative-evidence-replication-graph/demo.mp4 create mode 100644 negative-evidence-replication-graph/demo.svg create mode 100644 negative-evidence-replication-graph/index.js create mode 100644 negative-evidence-replication-graph/requirements-map.md create mode 100644 negative-evidence-replication-graph/sample-data.js create mode 100644 negative-evidence-replication-graph/test.js diff --git a/README.md b/README.md index d338cf68..64f31b0f 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,6 @@ # deepevents.ai deepevents.ai main codebase + +## Scientific Knowledge Graph Integration + +- `negative-evidence-replication-graph/` adds a self-contained #17 slice for failed replications, null results, and inconclusive studies as first-class knowledge-graph signals. diff --git a/negative-evidence-replication-graph/README.md b/negative-evidence-replication-graph/README.md new file mode 100644 index 00000000..f5de652e --- /dev/null +++ b/negative-evidence-replication-graph/README.md @@ -0,0 +1,32 @@ +# Negative Evidence Replication Graph + +This module is a focused Scientific Knowledge Graph Integration slice for SCIBASE issue #17. It treats failed replications, null results, and inconclusive studies as first-class graph signals instead of leaving them as unstructured notes attached to a paper. + +## What It Adds + +- Typed graph nodes for claims, concepts, methods, datasets, protocols, papers, and replication signals. +- Deterministic scoring for positive support and negative replication pressure. +- Recommendation treatments that promote replicated claims, show uncertain claims with caution, or suppress recommendations when failed replication evidence is strong. +- Entity-page packets with schema.org-compatible JSON-LD and reviewer-visible replication actions. +- Publication-bias alerts when a domain has confident claims but no registered negative-result records. +- Offline JSON and SVG demo output generated from synthetic data. + +## Why This Is Distinct + +Existing submissions for #17 cover broad extraction/navigation, link audits, ontology drift, conflict arbitration, author disambiguation, artifact reuse lineage, evidence freshness, reproducibility routes, and visibility filtering. This slice focuses specifically on negative evidence: failed replication attempts, null results, and inconclusive runs that should change graph navigation and AI recommendations before researchers rely on a claim. + +## Run + +```bash +node negative-evidence-replication-graph/test.js +node negative-evidence-replication-graph/demo.js +``` + +The demo writes: + +- `negative-evidence-replication-graph/demo-output.json` +- `negative-evidence-replication-graph/demo.svg` + +## Core Policy + +When negative replication pressure is high, the module returns `suppress_recommendation` and requires curator review plus a visible entity-page replication note. When evidence is inconclusive, the module keeps the claim discoverable but requires method detail before it is promoted in recommendation digests. diff --git a/negative-evidence-replication-graph/demo-output.json b/negative-evidence-replication-graph/demo-output.json new file mode 100644 index 00000000..1f5a3300 --- /dev/null +++ b/negative-evidence-replication-graph/demo-output.json @@ -0,0 +1,216 @@ +{ + "summary": { + "nodeCount": 21, + "edgeCount": 27, + "claimCount": 3, + "replicationSignalCount": 4, + "suppressedRecommendations": 1, + "cautionRecommendations": 1 + }, + "suppressedRecommendations": [ + { + "claimId": "claim:beta-organoid-rescue", + "title": "Beta compound rescues organoid viability at low dose", + "domain": "organoid-pharmacology", + "referenceScore": 0.541, + "positiveSupport": 0, + "negativePressure": 1.227, + "netScore": -0.502, + "treatment": "suppress_recommendation", + "signals": [ + { + "id": "rep:beta-null-dose", + "outcome": "negative_result", + "strength": -0.427, + "quality": 0.776, + "lab": "Organoid Core West", + "reportedAt": "2026-04-21" + }, + { + "id": "rep:beta-failed-media", + "outcome": "failed_replication", + "strength": -0.8, + "quality": 0.8, + "lab": "Consortium Lab 4", + "reportedAt": "2026-05-02" + } + ], + "requiredActions": [ + "open_curator_review", + "attach_failed_replication_to_entity_page", + "remove_from_ai_recommendation_digest" + ] + } + ], + "cautionRecommendations": [ + { + "claimId": "claim:graphene-ultra-sensitive", + "title": "Graphene biosensor detects femtomolar protein concentrations", + "domain": "materials-biosensing", + "referenceScore": 0.671, + "positiveSupport": 0, + "negativePressure": 0.066, + "netScore": 0.615, + "treatment": "show_with_replication_caution", + "signals": [ + { + "id": "rep:graphene-inconclusive", + "outcome": "inconclusive", + "strength": -0.066, + "quality": 0.439, + "lab": "Materials Lab North", + "reportedAt": "2026-05-05" + } + ], + "requiredActions": [ + "request_method_detail_before_digesting" + ] + } + ], + "exampleEntityPage": { + "id": "claim:beta-organoid-rescue", + "title": "Beta compound rescues organoid viability at low dose", + "type": "ScientificClaim", + "domain": "organoid-pharmacology", + "treatment": "suppress_recommendation", + "replicationScore": -0.502, + "requiredActions": [ + "open_curator_review", + "attach_failed_replication_to_entity_page", + "remove_from_ai_recommendation_digest" + ], + "relationships": [ + { + "id": "signal:rep:beta-null-dose:evaluates_claim:claim:beta-organoid-rescue:5", + "from": "signal:rep:beta-null-dose", + "to": "claim:beta-organoid-rescue", + "type": "evaluates_claim", + "evidence": { + "outcome": "negative_result", + "quality": 0.776, + "strength": -0.427 + } + }, + { + "id": "signal:rep:beta-failed-media:evaluates_claim:claim:beta-organoid-rescue:9", + "from": "signal:rep:beta-failed-media", + "to": "claim:beta-organoid-rescue", + "type": "evaluates_claim", + "evidence": { + "outcome": "failed_replication", + "quality": 0.8, + "strength": -0.8 + } + }, + { + "id": "paper:beta-2025:asserts_claim:claim:beta-organoid-rescue:21", + "from": "paper:beta-2025", + "to": "claim:beta-organoid-rescue", + "type": "asserts_claim", + "evidence": {} + }, + { + "id": "claim:beta-organoid-rescue:mentions_concept:concept:organoid-dose-response:22", + "from": "claim:beta-organoid-rescue", + "to": "concept:organoid-dose-response", + "type": "mentions_concept", + "evidence": {} + }, + { + "id": "claim:beta-organoid-rescue:uses_method:method:live-cell-imaging:23", + "from": "claim:beta-organoid-rescue", + "to": "method:live-cell-imaging", + "type": "uses_method", + "evidence": {} + }, + { + "id": "claim:beta-organoid-rescue:uses_dataset:dataset:beta-organoid-v2:24", + "from": "claim:beta-organoid-rescue", + "to": "dataset:beta-organoid-v2", + "type": "uses_dataset", + "evidence": {} + } + ], + "replicationSignals": [ + { + "id": "signal:rep:beta-null-dose", + "type": "replication_signal", + "title": "Low-dose beta rescue not observed in blinded run", + "outcome": "negative_result", + "lab": "Organoid Core West", + "reportedAt": "2026-04-21", + "quality": 0.776, + "strength": -0.427, + "tags": [ + "replication", + "negative_result" + ] + }, + { + "id": "signal:rep:beta-failed-media", + "type": "replication_signal", + "title": "Beta compound failed replication under matched media", + "outcome": "failed_replication", + "lab": "Consortium Lab 4", + "reportedAt": "2026-05-02", + "quality": 0.8, + "strength": -0.8, + "tags": [ + "replication", + "failed_replication" + ] + } + ], + "jsonLd": { + "@context": "https://schema.org", + "@type": "ScholarlyArticle", + "identifier": "claim:beta-organoid-rescue", + "headline": "Beta compound rescues organoid viability at low dose", + "about": [ + "concept:organoid-dose-response" + ], + "isBasedOn": [ + "dataset:beta-organoid-v2" + ], + "measurementTechnique": [ + "method:live-cell-imaging" + ], + "additionalProperty": [ + { + "@type": "PropertyValue", + "name": "SCIBASE replication treatment", + "value": "suppress_recommendation" + }, + { + "@type": "PropertyValue", + "name": "SCIBASE replication score", + "value": -0.502 + } + ] + } + }, + "recommendationDigest": [ + { + "claimId": "claim:alpha-inflammatory-drop", + "title": "Alpha pathway editing lowers IL-6 release in microglia", + "treatment": "promote_as_replicated", + "netScore": 1, + "rationale": "0.832 positive replication support; no negative signal" + }, + { + "claimId": "claim:graphene-ultra-sensitive", + "title": "Graphene biosensor detects femtomolar protein concentrations", + "treatment": "show_with_replication_caution", + "netScore": 0.615, + "rationale": "0.066 negative replication pressure; 0 positive support" + }, + { + "claimId": "claim:beta-organoid-rescue", + "title": "Beta compound rescues organoid viability at low dose", + "treatment": "suppress_recommendation", + "netScore": -0.502, + "rationale": "1.227 negative replication pressure; 0 positive support" + } + ], + "publicationBiasAlerts": [] +} diff --git a/negative-evidence-replication-graph/demo.js b/negative-evidence-replication-graph/demo.js new file mode 100644 index 00000000..13826755 --- /dev/null +++ b/negative-evidence-replication-graph/demo.js @@ -0,0 +1,31 @@ +"use strict"; + +const fs = require("node:fs"); +const path = require("node:path"); +const { + buildReplicationSignalGraph, + createEntityPage, + queryGraph, + renderGraphSvg +} = require("./index"); +const sampleData = require("./sample-data"); + +const outDir = __dirname; +const graph = buildReplicationSignalGraph(sampleData); +const suppressed = queryGraph(graph, { treatment: "suppress_recommendation" }); +const cautious = queryGraph(graph, { treatment: "show_with_replication_caution" }); +const betaEntityPage = createEntityPage(graph, "claim:beta-organoid-rescue"); + +const output = { + summary: graph.stats, + suppressedRecommendations: suppressed.results, + cautionRecommendations: cautious.results, + exampleEntityPage: betaEntityPage, + recommendationDigest: graph.recommendationDigest, + publicationBiasAlerts: graph.publicationBiasAlerts +}; + +fs.writeFileSync(path.join(outDir, "demo-output.json"), `${JSON.stringify(output, null, 2)}\n`); +fs.writeFileSync(path.join(outDir, "demo.svg"), renderGraphSvg(graph)); + +console.log(JSON.stringify(output, null, 2)); diff --git a/negative-evidence-replication-graph/demo.mp4 b/negative-evidence-replication-graph/demo.mp4 new file mode 100644 index 0000000000000000000000000000000000000000..62d8a9914ee14f166905782355ad1a638a82c762 GIT binary patch literal 36366 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 K%*OdNvHc&^?^(Y9 literal 0 HcmV?d00001 diff --git a/negative-evidence-replication-graph/demo.svg b/negative-evidence-replication-graph/demo.svg new file mode 100644 index 00000000..ccd61a2b --- /dev/null +++ b/negative-evidence-replication-graph/demo.svg @@ -0,0 +1 @@ +Negative Evidence Replication GraphFailed replications and null results become graph signals before recommendations are shown.Alpha pathway editing lowers IL-6 release in microgliapromote_as_replicated | positive 0.832 | negative 0score 1Beta compound rescues organoid viability at low dosesuppress_recommendation | positive 0 | negative 1.227score -0.502Graphene biosensor detects femtomolar protein concentrationsshow_with_replication_caution | positive 0 | negative 0.066score 0.615 \ No newline at end of file diff --git a/negative-evidence-replication-graph/index.js b/negative-evidence-replication-graph/index.js new file mode 100644 index 00000000..0c151551 --- /dev/null +++ b/negative-evidence-replication-graph/index.js @@ -0,0 +1,410 @@ +"use strict"; + +const OUTCOME_WEIGHTS = Object.freeze({ + replicated: 1, + partial: 0.35, + inconclusive: -0.15, + negative_result: -0.55, + failed_replication: -1 +}); + +const NODE_TYPES = Object.freeze({ + CLAIM: "claim", + PAPER: "paper", + DATASET: "dataset", + PROTOCOL: "protocol", + METHOD: "method", + CONCEPT: "concept", + REPLICATION_SIGNAL: "replication_signal" +}); + +function assertArray(value, name) { + if (!Array.isArray(value)) { + throw new TypeError(`${name} must be an array`); + } +} + +function clamp(value, min = 0, max = 1) { + return Math.max(min, Math.min(max, value)); +} + +function round(value, digits = 3) { + const factor = 10 ** digits; + return Math.round(value * factor) / factor; +} + +function unique(values) { + return [...new Set(values.filter(Boolean))]; +} + +function addNode(nodes, node) { + if (!node || !node.id) { + throw new Error("Graph nodes require an id"); + } + if (!nodes.has(node.id)) { + nodes.set(node.id, { ...node }); + return; + } + + const existing = nodes.get(node.id); + nodes.set(node.id, { + ...existing, + ...node, + aliases: unique([...(existing.aliases || []), ...(node.aliases || [])]), + tags: unique([...(existing.tags || []), ...(node.tags || [])]) + }); +} + +function addEdge(edges, from, to, type, evidence = {}) { + if (!from || !to) { + throw new Error(`Cannot add ${type} edge without both endpoints`); + } + edges.push({ + id: `${from}:${type}:${to}:${edges.length + 1}`, + from, + to, + type, + evidence + }); +} + +function normalizeOutcome(outcome) { + const normalized = String(outcome || "").trim().toLowerCase().replace(/[\s-]+/g, "_"); + if (!(normalized in OUTCOME_WEIGHTS)) { + throw new Error(`Unsupported replication outcome: ${outcome}`); + } + return normalized; +} + +function evidenceQuality(report) { + const methodMatch = clamp(Number(report.methodMatch ?? 0.5)); + const sampleOverlap = clamp(Number(report.sampleOverlap ?? 0.5)); + const protocolAvailability = report.protocolAvailable === false ? 0.2 : 1; + const independentLab = report.independentLab === false ? 0.7 : 1; + const preregistered = report.preregistered ? 1.1 : 1; + const confidence = clamp(Number(report.confidence ?? 0.5)); + + return round( + clamp( + confidence * + (0.35 + methodMatch * 0.25 + sampleOverlap * 0.25 + protocolAvailability * 0.1 + independentLab * 0.05) * + preregistered, + 0, + 1 + ) + ); +} + +function signalStrength(report) { + const outcome = normalizeOutcome(report.outcome); + return round(OUTCOME_WEIGHTS[outcome] * evidenceQuality(report)); +} + +function claimReferenceScore(claim) { + const citationScore = clamp(Math.log10(Number(claim.citations || 0) + 1) / 4); + const reproducibilityScore = clamp(Number(claim.reproducibilityScore ?? 0.5)); + const publicationConfidence = clamp(Number(claim.publicationConfidence ?? 0.5)); + return round(citationScore * 0.2 + reproducibilityScore * 0.45 + publicationConfidence * 0.35); +} + +function treatmentForScore(score, negativePressure) { + if (negativePressure >= 0.55 || score < -0.25) { + return "suppress_recommendation"; + } + if (negativePressure >= 0.25 || score < 0.15) { + return "show_with_replication_caution"; + } + if (score >= 0.7) { + return "promote_as_replicated"; + } + return "show_with_evidence_context"; +} + +function summarizeClaim(claim, reports) { + const referenceScore = claimReferenceScore(claim); + const signals = reports.map((report) => ({ + id: report.id, + outcome: normalizeOutcome(report.outcome), + strength: signalStrength(report), + quality: evidenceQuality(report), + lab: report.lab, + reportedAt: report.reportedAt + })); + + const positiveSupport = round(signals.filter((s) => s.strength > 0).reduce((sum, s) => sum + s.strength, 0)); + const negativePressure = round(Math.abs(signals.filter((s) => s.strength < 0).reduce((sum, s) => sum + s.strength, 0))); + const netScore = round(clamp(referenceScore + positiveSupport * 0.45 - negativePressure * 0.85, -1, 1), 3); + const hasInconclusiveEvidence = signals.some((signal) => signal.outcome === "inconclusive"); + let treatment = treatmentForScore(netScore, negativePressure); + if (hasInconclusiveEvidence && treatment === "show_with_evidence_context") { + treatment = "show_with_replication_caution"; + } + + const requiredActions = []; + if (negativePressure >= 0.55) { + requiredActions.push("open_curator_review"); + requiredActions.push("attach_failed_replication_to_entity_page"); + } + if (hasInconclusiveEvidence) { + requiredActions.push("request_method_detail_before_digesting"); + } + if (signals.length === 0) { + requiredActions.push("label_unreplicated_claim"); + } + if (treatment === "suppress_recommendation") { + requiredActions.push("remove_from_ai_recommendation_digest"); + } + + return { + claimId: claim.id, + title: claim.title, + domain: claim.domain, + referenceScore, + positiveSupport, + negativePressure, + netScore, + treatment, + signals, + requiredActions: unique(requiredActions) + }; +} + +function buildReplicationSignalGraph(input) { + const data = input || {}; + assertArray(data.claims, "claims"); + assertArray(data.replicationReports, "replicationReports"); + + const nodes = new Map(); + const edges = []; + + for (const concept of data.concepts || []) { + addNode(nodes, { ...concept, type: NODE_TYPES.CONCEPT }); + } + for (const method of data.methods || []) { + addNode(nodes, { ...method, type: NODE_TYPES.METHOD }); + } + for (const paper of data.papers || []) { + addNode(nodes, { ...paper, type: NODE_TYPES.PAPER }); + } + for (const dataset of data.datasets || []) { + addNode(nodes, { ...dataset, type: NODE_TYPES.DATASET }); + } + for (const protocol of data.protocols || []) { + addNode(nodes, { ...protocol, type: NODE_TYPES.PROTOCOL }); + } + + const reportsByClaim = new Map(); + for (const report of data.replicationReports) { + const outcome = normalizeOutcome(report.outcome); + const signalId = `signal:${report.id}`; + addNode(nodes, { + id: signalId, + type: NODE_TYPES.REPLICATION_SIGNAL, + title: report.title || `${outcome} for ${report.claimId}`, + outcome, + lab: report.lab, + reportedAt: report.reportedAt, + quality: evidenceQuality(report), + strength: signalStrength(report), + tags: ["replication", outcome] + }); + addEdge(edges, signalId, report.claimId, "evaluates_claim", { + outcome, + quality: evidenceQuality(report), + strength: signalStrength(report) + }); + if (report.datasetId) addEdge(edges, signalId, report.datasetId, "uses_dataset"); + if (report.protocolId) addEdge(edges, signalId, report.protocolId, "uses_protocol"); + if (report.methodId) addEdge(edges, signalId, report.methodId, "uses_method"); + + if (!reportsByClaim.has(report.claimId)) reportsByClaim.set(report.claimId, []); + reportsByClaim.get(report.claimId).push(report); + } + + const claimSummaries = []; + for (const claim of data.claims) { + addNode(nodes, { + ...claim, + type: NODE_TYPES.CLAIM, + tags: unique(["claim", claim.domain, ...(claim.tags || [])]) + }); + if (claim.paperId) addEdge(edges, claim.paperId, claim.id, "asserts_claim"); + for (const conceptId of claim.conceptIds || []) addEdge(edges, claim.id, conceptId, "mentions_concept"); + for (const methodId of claim.methodIds || []) addEdge(edges, claim.id, methodId, "uses_method"); + for (const datasetId of claim.datasetIds || []) addEdge(edges, claim.id, datasetId, "uses_dataset"); + claimSummaries.push(summarizeClaim(claim, reportsByClaim.get(claim.id) || [])); + } + + const publicationBiasAlerts = buildPublicationBiasAlerts(data.claims, claimSummaries); + const recommendationDigest = claimSummaries + .slice() + .sort((a, b) => b.netScore - a.netScore) + .map((summary) => ({ + claimId: summary.claimId, + title: summary.title, + treatment: summary.treatment, + netScore: summary.netScore, + rationale: + summary.negativePressure > 0 + ? `${summary.negativePressure} negative replication pressure; ${summary.positiveSupport} positive support` + : `${summary.positiveSupport} positive replication support; no negative signal` + })); + + return { + generatedAt: new Date().toISOString(), + nodes: [...nodes.values()], + edges, + claimSummaries, + publicationBiasAlerts, + recommendationDigest, + stats: { + nodeCount: nodes.size, + edgeCount: edges.length, + claimCount: data.claims.length, + replicationSignalCount: data.replicationReports.length, + suppressedRecommendations: claimSummaries.filter((s) => s.treatment === "suppress_recommendation").length, + cautionRecommendations: claimSummaries.filter((s) => s.treatment === "show_with_replication_caution").length + } + }; +} + +function buildPublicationBiasAlerts(claims, summaries) { + const byDomain = new Map(); + for (const claim of claims) { + if (!byDomain.has(claim.domain)) byDomain.set(claim.domain, []); + byDomain.get(claim.domain).push(claim.id); + } + const summaryByClaim = new Map(summaries.map((summary) => [summary.claimId, summary])); + const alerts = []; + for (const [domain, claimIds] of byDomain.entries()) { + const domainSummaries = claimIds.map((id) => summaryByClaim.get(id)); + const unreplicatedHighConfidence = domainSummaries.filter( + (summary) => summary.referenceScore >= 0.6 && summary.signals.length === 0 + ); + const negativeSignals = domainSummaries.filter((summary) => summary.negativePressure > 0); + if (unreplicatedHighConfidence.length > 0 && negativeSignals.length === 0) { + alerts.push({ + domain, + severity: "medium", + reason: "High-confidence claims have no registered negative or failed replication records.", + claimIds: unreplicatedHighConfidence.map((summary) => summary.claimId), + action: "prompt_reviewers_to_register_null_results" + }); + } + } + return alerts; +} + +function queryGraph(graph, options = {}) { + const domain = options.domain; + const treatment = options.treatment; + const minimumScore = options.minimumScore ?? -1; + const results = graph.claimSummaries.filter((summary) => { + if (domain && summary.domain !== domain) return false; + if (treatment && summary.treatment !== treatment) return false; + return summary.netScore >= minimumScore; + }); + return { + count: results.length, + results + }; +} + +function createEntityPage(graph, claimId) { + const summary = graph.claimSummaries.find((item) => item.claimId === claimId); + if (!summary) throw new Error(`Unknown claim: ${claimId}`); + const claim = graph.nodes.find((node) => node.id === claimId); + const relatedEdges = graph.edges.filter((edge) => edge.from === claimId || edge.to === claimId); + const signalNodes = summary.signals.map((signal) => graph.nodes.find((node) => node.id === `signal:${signal.id}`)); + return { + id: claimId, + title: summary.title, + type: "ScientificClaim", + domain: summary.domain, + treatment: summary.treatment, + replicationScore: summary.netScore, + requiredActions: summary.requiredActions, + relationships: relatedEdges, + replicationSignals: signalNodes, + jsonLd: { + "@context": "https://schema.org", + "@type": "ScholarlyArticle", + identifier: claimId, + headline: claim.title, + about: claim.conceptIds || [], + isBasedOn: claim.datasetIds || [], + measurementTechnique: claim.methodIds || [], + additionalProperty: [ + { + "@type": "PropertyValue", + name: "SCIBASE replication treatment", + value: summary.treatment + }, + { + "@type": "PropertyValue", + name: "SCIBASE replication score", + value: summary.netScore + } + ] + } + }; +} + +function renderGraphSvg(graph) { + const width = 900; + const rowHeight = 92; + const height = 120 + graph.claimSummaries.length * rowHeight; + const rows = graph.claimSummaries + .map((summary, index) => { + const y = 90 + index * rowHeight; + const color = + summary.treatment === "suppress_recommendation" + ? "#b42318" + : summary.treatment === "show_with_replication_caution" + ? "#b54708" + : summary.treatment === "promote_as_replicated" + ? "#027a48" + : "#175cd3"; + const barWidth = Math.round((summary.netScore + 1) * 180); + return [ + ``, + ``, + `${escapeXml(summary.title)}`, + `${escapeXml(summary.treatment)} | positive ${summary.positiveSupport} | negative ${summary.negativePressure}`, + ``, + ``, + `score ${summary.netScore}`, + `` + ].join(""); + }) + .join(""); + + return [ + ``, + ``, + `Negative Evidence Replication Graph`, + `Failed replications and null results become graph signals before recommendations are shown.`, + rows, + `` + ].join(""); +} + +function escapeXml(value) { + return String(value) + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """); +} + +module.exports = { + NODE_TYPES, + OUTCOME_WEIGHTS, + buildReplicationSignalGraph, + createEntityPage, + evidenceQuality, + queryGraph, + renderGraphSvg, + signalStrength, + summarizeClaim +}; diff --git a/negative-evidence-replication-graph/requirements-map.md b/negative-evidence-replication-graph/requirements-map.md new file mode 100644 index 00000000..80a48848 --- /dev/null +++ b/negative-evidence-replication-graph/requirements-map.md @@ -0,0 +1,21 @@ +# Requirements Map + +## Issue #17: Scientific Knowledge Graph Integration + +| Requirement | Coverage | +| --- | --- | +| Entity extraction and typed graph nodes | `buildReplicationSignalGraph` creates typed claim, concept, method, dataset, protocol, paper, and replication-signal nodes from structured scientific objects. | +| Linked data and schema.org-compatible metadata | `createEntityPage` emits a schema.org JSON-LD packet with replication treatment and score metadata. | +| Knowledge navigation | `queryGraph` filters claim summaries by domain, score, and recommendation treatment, enabling graph journeys such as suppressed claims or caution-only findings. | +| AI research recommendations | `recommendationDigest` ranks claims while attaching treatment and rationale so AI recommendations do not promote contradicted claims. | +| Reproducibility and evidence context | Replication reports score method match, sample overlap, preregistration, protocol availability, and lab independence before changing recommendation treatment. | +| Knowledge gaps and underexplored intersections | `publicationBiasAlerts` identifies domains with high-confidence claims but no registered null/failed replication evidence. | +| Entity pages with aggregated data | `createEntityPage` includes relationships, replication signals, required curator actions, JSON-LD, and a replication score. | +| Tests and demo | `test.js` covers positive support, failed replication suppression, inconclusive caution, query behavior, schema output, and edge evidence quality. `demo.js` emits JSON and SVG artifacts. | + +## Acceptance Notes + +- Synthetic data only; no external service calls or private data. +- No dependencies beyond Node.js standard library. +- Failed replications and negative results are explicit graph nodes, not comments or untyped metadata. +- Strong failed replication evidence suppresses AI recommendation output while preserving an auditable entity page for reviewers. diff --git a/negative-evidence-replication-graph/sample-data.js b/negative-evidence-replication-graph/sample-data.js new file mode 100644 index 00000000..4ac99fef --- /dev/null +++ b/negative-evidence-replication-graph/sample-data.js @@ -0,0 +1,194 @@ +"use strict"; + +module.exports = { + concepts: [ + { + id: "concept:crispr-neuroinflammation", + title: "CRISPR neuroinflammation screen", + ontology: "MeSH:D000077592" + }, + { + id: "concept:organoid-dose-response", + title: "Organoid dose response", + ontology: "schema:MedicalStudy" + }, + { + id: "concept:graphene-biosensor", + title: "Graphene biosensor sensitivity", + ontology: "PubChem:graphene" + } + ], + methods: [ + { + id: "method:single-cell-rna", + title: "Single-cell RNA sequencing" + }, + { + id: "method:live-cell-imaging", + title: "Live-cell imaging" + }, + { + id: "method:impedance-sweep", + title: "Electrochemical impedance sweep" + } + ], + papers: [ + { + id: "paper:alpha-2025", + title: "Alpha pathway suppresses inflammatory marker release", + doi: "10.0000/scibase.alpha.2025" + }, + { + id: "paper:beta-2025", + title: "Beta compound improves organoid viability at low dose", + doi: "10.0000/scibase.beta.2025" + } + ], + datasets: [ + { + id: "dataset:alpha-counts-v1", + title: "Alpha screen raw counts v1", + license: "CC-BY-4.0", + checksum: "sha256:4fe1-alpha" + }, + { + id: "dataset:beta-organoid-v2", + title: "Beta organoid dose response v2", + license: "CC-BY-NC-4.0", + checksum: "sha256:89af-beta" + }, + { + id: "dataset:graphene-sensor-v1", + title: "Graphene impedance sensor sweep", + license: "CC0-1.0", + checksum: "sha256:77be-graphene" + } + ], + protocols: [ + { + id: "protocol:alpha-crispr-v3", + title: "Alpha CRISPR screen protocol v3", + version: "3.0.1" + }, + { + id: "protocol:beta-organoid-v4", + title: "Beta organoid protocol v4", + version: "4.2.0" + }, + { + id: "protocol:graphene-sensor-v2", + title: "Graphene sensor protocol v2", + version: "2.0.0" + } + ], + claims: [ + { + id: "claim:alpha-inflammatory-drop", + title: "Alpha pathway editing lowers IL-6 release in microglia", + domain: "neuroscience", + paperId: "paper:alpha-2025", + conceptIds: ["concept:crispr-neuroinflammation"], + methodIds: ["method:single-cell-rna"], + datasetIds: ["dataset:alpha-counts-v1"], + citations: 162, + reproducibilityScore: 0.76, + publicationConfidence: 0.81, + tags: ["crispr", "microglia", "inflammation"] + }, + { + id: "claim:beta-organoid-rescue", + title: "Beta compound rescues organoid viability at low dose", + domain: "organoid-pharmacology", + paperId: "paper:beta-2025", + conceptIds: ["concept:organoid-dose-response"], + methodIds: ["method:live-cell-imaging"], + datasetIds: ["dataset:beta-organoid-v2"], + citations: 47, + reproducibilityScore: 0.54, + publicationConfidence: 0.61, + tags: ["organoid", "dose-response"] + }, + { + id: "claim:graphene-ultra-sensitive", + title: "Graphene biosensor detects femtomolar protein concentrations", + domain: "materials-biosensing", + conceptIds: ["concept:graphene-biosensor"], + methodIds: ["method:impedance-sweep"], + datasetIds: ["dataset:graphene-sensor-v1"], + citations: 91, + reproducibilityScore: 0.69, + publicationConfidence: 0.75, + tags: ["graphene", "biosensor"] + } + ], + replicationReports: [ + { + id: "rep:alpha-lab-b-positive", + title: "Independent alpha screen reproduces IL-6 effect", + claimId: "claim:alpha-inflammatory-drop", + outcome: "replicated", + lab: "Lab B", + reportedAt: "2026-04-18", + confidence: 0.82, + methodMatch: 0.92, + sampleOverlap: 0.77, + protocolAvailable: true, + independentLab: true, + preregistered: true, + datasetId: "dataset:alpha-counts-v1", + protocolId: "protocol:alpha-crispr-v3", + methodId: "method:single-cell-rna" + }, + { + id: "rep:beta-null-dose", + title: "Low-dose beta rescue not observed in blinded run", + claimId: "claim:beta-organoid-rescue", + outcome: "negative_result", + lab: "Organoid Core West", + reportedAt: "2026-04-21", + confidence: 0.79, + methodMatch: 0.84, + sampleOverlap: 0.73, + protocolAvailable: true, + independentLab: true, + preregistered: true, + datasetId: "dataset:beta-organoid-v2", + protocolId: "protocol:beta-organoid-v4", + methodId: "method:live-cell-imaging" + }, + { + id: "rep:beta-failed-media", + title: "Beta compound failed replication under matched media", + claimId: "claim:beta-organoid-rescue", + outcome: "failed_replication", + lab: "Consortium Lab 4", + reportedAt: "2026-05-02", + confidence: 0.86, + methodMatch: 0.91, + sampleOverlap: 0.81, + protocolAvailable: true, + independentLab: true, + preregistered: false, + datasetId: "dataset:beta-organoid-v2", + protocolId: "protocol:beta-organoid-v4", + methodId: "method:live-cell-imaging" + }, + { + id: "rep:graphene-inconclusive", + title: "Graphene sensor replication inconclusive after humidity drift", + claimId: "claim:graphene-ultra-sensitive", + outcome: "inconclusive", + lab: "Materials Lab North", + reportedAt: "2026-05-05", + confidence: 0.58, + methodMatch: 0.66, + sampleOverlap: 0.69, + protocolAvailable: false, + independentLab: true, + preregistered: false, + datasetId: "dataset:graphene-sensor-v1", + protocolId: "protocol:graphene-sensor-v2", + methodId: "method:impedance-sweep" + } + ] +}; diff --git a/negative-evidence-replication-graph/test.js b/negative-evidence-replication-graph/test.js new file mode 100644 index 00000000..8d9635f9 --- /dev/null +++ b/negative-evidence-replication-graph/test.js @@ -0,0 +1,56 @@ +"use strict"; + +const assert = require("node:assert/strict"); +const { + buildReplicationSignalGraph, + createEntityPage, + evidenceQuality, + queryGraph, + signalStrength +} = require("./index"); +const sampleData = require("./sample-data"); + +const graph = buildReplicationSignalGraph(sampleData); + +assert.equal(graph.stats.claimCount, 3); +assert.equal(graph.stats.replicationSignalCount, 4); +assert.ok(graph.stats.edgeCount >= 15); + +const beta = graph.claimSummaries.find((summary) => summary.claimId === "claim:beta-organoid-rescue"); +assert.equal(beta.treatment, "suppress_recommendation"); +assert.ok(beta.negativePressure > 1); +assert.ok(beta.requiredActions.includes("open_curator_review")); +assert.ok(beta.requiredActions.includes("remove_from_ai_recommendation_digest")); + +const alpha = graph.claimSummaries.find((summary) => summary.claimId === "claim:alpha-inflammatory-drop"); +assert.equal(alpha.treatment, "promote_as_replicated"); +assert.ok(alpha.positiveSupport > 0.7); + +const graphene = graph.claimSummaries.find((summary) => summary.claimId === "claim:graphene-ultra-sensitive"); +assert.equal(graphene.treatment, "show_with_replication_caution"); +assert.ok(graphene.requiredActions.includes("request_method_detail_before_digesting")); + +const betaSignal = sampleData.replicationReports.find((report) => report.id === "rep:beta-failed-media"); +assert.ok(evidenceQuality(betaSignal) > 0.7); +assert.ok(signalStrength(betaSignal) < -0.7); + +const cautious = queryGraph(graph, { treatment: "show_with_replication_caution" }); +assert.equal(cautious.count, 1); +assert.equal(cautious.results[0].claimId, "claim:graphene-ultra-sensitive"); + +const neuroscience = queryGraph(graph, { domain: "neuroscience", minimumScore: 0.5 }); +assert.equal(neuroscience.count, 1); +assert.equal(neuroscience.results[0].claimId, "claim:alpha-inflammatory-drop"); + +const entityPage = createEntityPage(graph, "claim:beta-organoid-rescue"); +assert.equal(entityPage.type, "ScientificClaim"); +assert.equal(entityPage.treatment, "suppress_recommendation"); +assert.equal(entityPage.replicationSignals.length, 2); +assert.equal(entityPage.jsonLd["@context"], "https://schema.org"); +assert.equal(entityPage.jsonLd.additionalProperty[0].value, "suppress_recommendation"); + +const signalEdges = graph.edges.filter((edge) => edge.type === "evaluates_claim"); +assert.equal(signalEdges.length, 4); +assert.ok(signalEdges.every((edge) => typeof edge.evidence.quality === "number")); + +console.log("negative-evidence-replication-graph tests passed");