diff --git a/README.md b/README.md index d338cf6..64f31b0 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 0000000..f5de652 --- /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 0000000..1f5a330 --- /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 0000000..1382675 --- /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 0000000..62d8a99 Binary files /dev/null and b/negative-evidence-replication-graph/demo.mp4 differ diff --git a/negative-evidence-replication-graph/demo.svg b/negative-evidence-replication-graph/demo.svg new file mode 100644 index 0000000..ccd61a2 --- /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 0000000..0c15155 --- /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 0000000..80a4884 --- /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 0000000..4ac99fe --- /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 0000000..8d9635f --- /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");