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 @@
+
\ 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 [
+ ``
+ ].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");