diff --git a/negative-results-opportunity-radar/README.md b/negative-results-opportunity-radar/README.md new file mode 100644 index 0000000..8cc384f --- /dev/null +++ b/negative-results-opportunity-radar/README.md @@ -0,0 +1,24 @@ +# Negative Results Opportunity Radar + +This submission targets [SCIBASE issue #16](https://github.com/SCIBASE-AI/SCIBASE.AI/issues/16) with a focused AI research assistant module. + +It turns negative results, limitations, and failed replications into ranked research opportunities. The goal is to help a researcher find tractable gaps that are often buried in paper discussions instead of highlighted by normal keyword search. + +## What It Adds + +- Signal extraction for limitations, negative results, and failed replications. +- Opportunity ranking that weighs evidence strength, lab capabilities, and researcher interests. +- Reproducibility flags for gaps backed by failed replication evidence. +- Assistant packets with peer-review prompts, citation queries, and next-step recommendations. +- Dependency-free tests and demo data that can be wired into a larger assistant/search service. + +## Demo + +```powershell +node negative-results-opportunity-radar/test.js +node negative-results-opportunity-radar/demo.js +``` + +`demo.mp4` is the reviewer-facing video artifact for the bounty submission. It walks through the problem, implementation, acceptance path, and command validation in 8.4 seconds. `demo.svg` shows the assistant flow from paper evidence to ranked opportunity and reviewer prompts. + +See `acceptance-notes.md` for the payout-gate evidence checklist. diff --git a/negative-results-opportunity-radar/acceptance-notes.md b/negative-results-opportunity-radar/acceptance-notes.md new file mode 100644 index 0000000..23bdd58 --- /dev/null +++ b/negative-results-opportunity-radar/acceptance-notes.md @@ -0,0 +1,30 @@ +# Acceptance Notes + +This is a focused implementation for SCIBASE issue #16, not a generic AI-generated content drop. The module is small because the bounty asks for a specific AI research-assistant capability that can be reviewed independently before integration. + +## What Changed + +- Added signal extraction for negative results, limitations, and failed replications. +- Added evidence clustering and opportunity ranking by topic, method, lab fit, and user interest. +- Added reproducibility flags and assistant packets that include peer-review prompts, citation queries, and next-step recommendations. +- Added focused dependency-free tests and demo data. + +## Video Demo + +- `demo.mp4` shows the problem, implementation, acceptance behavior, and validation command. +- `demo.svg` provides a static architecture and workflow diagram. + +## Validation + +Run from the repository root: + +```powershell +node negative-results-opportunity-radar/test.js +node negative-results-opportunity-radar/demo.js +``` + +Expected result: the test prints `negative-results-opportunity-radar tests passed`, and the demo prints a ranked opportunity packet with evidence, reproducibility flags, citation queries, and peer-review prompts. + +## Integration Notes + +The module is intentionally dependency-free so maintainers can lift it into a larger search or assistant service. The next integration step is replacing the sample paper objects with SCIBASE paper ingestion output. diff --git a/negative-results-opportunity-radar/demo.js b/negative-results-opportunity-radar/demo.js new file mode 100644 index 0000000..826ddce --- /dev/null +++ b/negative-results-opportunity-radar/demo.js @@ -0,0 +1,52 @@ +"use strict"; + +const { + buildAssistantPacket, + rankOpportunities +} = require("./index"); + +const papers = [ + { + id: "trial-101", + title: "Neuroinflammation classifier pilot", + topics: ["neuroinflammation", "biomarkers"], + methods: ["proteomics", "cohort-validation"], + limitations: ["study excluded early-stage patients"], + negativeResults: ["blood panel failed to separate mild disease"], + failedReplications: [] + }, + { + id: "trial-118", + title: "External proteomics replication study", + topics: ["neuroinflammation", "biomarkers"], + methods: ["proteomics"], + limitations: ["assay drift was not modeled across sites"], + negativeResults: [], + failedReplications: ["published classifier failed on two external cohorts"] + }, + { + id: "screen-22", + title: "Mouse model intervention screen", + topics: ["neuroinflammation"], + methods: ["animal-model"], + limitations: ["translation to human cohorts is uncertain"], + negativeResults: ["late intervention showed no measurable rescue"], + failedReplications: [] + } +]; + +const opportunities = rankOpportunities({ + papers, + labCapabilities: ["proteomics", "cohort-validation"], + userInterests: ["neuroinflammation"], + minimumScore: 20 +}); + +console.log(JSON.stringify({ + opportunities, + assistantPacket: buildAssistantPacket({ + projectId: "neuro-gap-review", + opportunities + }) +}, null, 2)); + diff --git a/negative-results-opportunity-radar/demo.mp4 b/negative-results-opportunity-radar/demo.mp4 new file mode 100644 index 0000000..957bcb9 Binary files /dev/null and b/negative-results-opportunity-radar/demo.mp4 differ diff --git a/negative-results-opportunity-radar/demo.svg b/negative-results-opportunity-radar/demo.svg new file mode 100644 index 0000000..e790fa1 --- /dev/null +++ b/negative-results-opportunity-radar/demo.svg @@ -0,0 +1,42 @@ + + Negative results opportunity radar demo + An AI assistant converts negative results and limitations into ranked research opportunities. + + + Negative results opportunity radar + Buried limitations become assistant-ready research gap recommendations. + + + + Paper evidence + limitations + negative results + failed replications + + + + Signal clustering + topic + method + lab capability + interest match + + + + Ranked gap + score + confidence + evidence digest + + + + Assistant + review + citations + + + + + + + diff --git a/negative-results-opportunity-radar/index.js b/negative-results-opportunity-radar/index.js new file mode 100644 index 0000000..d81e052 --- /dev/null +++ b/negative-results-opportunity-radar/index.js @@ -0,0 +1,224 @@ +"use strict"; + +const crypto = require("crypto"); + +function stable(value) { + if (Array.isArray(value)) { + return value.map(stable); + } + if (value && typeof value === "object") { + return Object.keys(value).sort().reduce((result, key) => { + result[key] = stable(value[key]); + return result; + }, {}); + } + return value; +} + +function digest(value) { + return crypto.createHash("sha256").update(JSON.stringify(stable(value))).digest("hex"); +} + +function normalizeList(values) { + return [...new Set((values || []) + .map((value) => String(value).trim().toLowerCase()) + .filter(Boolean))].sort(); +} + +function normalizePaper(paper) { + if (!paper || !paper.id || !paper.title) { + throw new Error("Paper requires id and title"); + } + + return { + id: String(paper.id), + title: String(paper.title), + year: paper.year || null, + topics: normalizeList(paper.topics), + methods: normalizeList(paper.methods), + limitations: normalizeList(paper.limitations), + negativeResults: normalizeList(paper.negativeResults), + failedReplications: normalizeList(paper.failedReplications), + citations: paper.citations || [] + }; +} + +function signalRecord(paper, kind, text) { + return { + paperId: paper.id, + title: paper.title, + year: paper.year, + kind, + text, + topics: paper.topics, + methods: paper.methods, + evidenceDigest: digest({ + paperId: paper.id, + kind, + text + }) + }; +} + +function extractSignals(paperInput) { + const paper = normalizePaper(paperInput); + return [ + ...paper.limitations.map((text) => signalRecord(paper, "limitation", text)), + ...paper.negativeResults.map((text) => signalRecord(paper, "negative_result", text)), + ...paper.failedReplications.map((text) => signalRecord(paper, "failed_replication", text)) + ]; +} + +function clusterKey(topic, method) { + return `${topic || "general"}::${method || "method-open"}`; +} + +function addToCluster(clusters, topic, method, signal) { + const key = clusterKey(topic, method); + if (!clusters.has(key)) { + clusters.set(key, { + key, + topic: topic || "general", + method: method || "method-open", + signals: [] + }); + } + clusters.get(key).signals.push(signal); +} + +function buildClusters(signals) { + const clusters = new Map(); + + for (const signal of signals) { + const topics = signal.topics.length > 0 ? signal.topics : ["general"]; + const methods = signal.methods.length > 0 ? signal.methods : ["method-open"]; + for (const topic of topics) { + for (const method of methods) { + addToCluster(clusters, topic, method, signal); + } + } + } + + return [...clusters.values()]; +} + +function countKinds(signals) { + return signals.reduce((counts, signal) => { + counts[signal.kind] = (counts[signal.kind] || 0) + 1; + return counts; + }, {}); +} + +function opportunityScore(cluster, labCapabilities, userInterests) { + const counts = countKinds(cluster.signals); + const uniquePaperCount = new Set(cluster.signals.map((signal) => signal.paperId)).size; + const capabilityMatches = [cluster.topic, cluster.method].filter((value) => labCapabilities.has(value)).length; + const interestMatches = [cluster.topic, cluster.method].filter((value) => userInterests.has(value)).length; + const unsupportedPenalty = capabilityMatches === 0 ? 12 : 0; + + return ( + uniquePaperCount * 10 + + (counts.limitation || 0) * 8 + + (counts.negative_result || 0) * 16 + + (counts.failed_replication || 0) * 22 + + capabilityMatches * 12 + + interestMatches * 9 - + unsupportedPenalty + ); +} + +function recommendedActions(cluster) { + const kinds = new Set(cluster.signals.map((signal) => signal.kind)); + const actions = []; + + if (kinds.has("failed_replication")) { + actions.push("Design a focused replication or adversarial validation run."); + } + if (kinds.has("negative_result")) { + actions.push("Convert the negative result into a bounded hypothesis search."); + } + if (kinds.has("limitation")) { + actions.push("Target the stated limitation with a smaller controlled follow-up."); + } + actions.push(`Search related citations for ${cluster.topic} using ${cluster.method}.`); + + return actions; +} + +function reproducibilityFlags(cluster) { + const flags = []; + if (cluster.signals.some((signal) => signal.kind === "failed_replication")) { + flags.push("failed_replication_evidence"); + } + if (new Set(cluster.signals.map((signal) => signal.paperId)).size > 1) { + flags.push("multi_paper_signal"); + } + if (cluster.signals.some((signal) => signal.kind === "negative_result")) { + flags.push("negative_result_context_required"); + } + return flags; +} + +function rankOpportunities(input) { + const labCapabilities = new Set(normalizeList(input.labCapabilities)); + const userInterests = new Set(normalizeList(input.userInterests)); + const signals = (input.papers || []).flatMap(extractSignals); + + return buildClusters(signals) + .map((cluster) => { + const score = opportunityScore(cluster, labCapabilities, userInterests); + return { + id: `opportunity-${digest(cluster.key).slice(0, 10)}`, + focus: `${cluster.topic} + ${cluster.method}`, + topic: cluster.topic, + method: cluster.method, + score, + confidence: score >= 70 ? "high" : score >= 42 ? "medium" : "low", + evidenceCount: cluster.signals.length, + evidence: cluster.signals + .sort((a, b) => a.paperId.localeCompare(b.paperId) || a.kind.localeCompare(b.kind)) + .slice(0, input.evidenceLimit || 6), + recommendedActions: recommendedActions(cluster), + reproducibilityFlags: reproducibilityFlags(cluster) + }; + }) + .filter((opportunity) => opportunity.score >= (input.minimumScore || 20)) + .sort((a, b) => b.score - a.score || a.focus.localeCompare(b.focus)); +} + +function buildAssistantPacket(input) { + const opportunities = input.opportunities || []; + const top = opportunities[0] || null; + + return { + projectId: input.projectId || "research-project", + generatedFrom: "negative-results-opportunity-radar", + rankedOpportunityCount: opportunities.length, + topRecommendation: top ? { + id: top.id, + focus: top.focus, + score: top.score, + confidence: top.confidence + } : null, + peerReviewPrompts: top ? [ + `Ask whether the manuscript discusses negative or failed evidence around ${top.focus}.`, + `Check whether the proposed study distinguishes novelty from known limitations in ${top.topic}.` + ] : [], + citationQueries: opportunities.slice(0, 3).map((opportunity) => { + return `${opportunity.topic} ${opportunity.method} negative result failed replication limitation`; + }), + reproducibilityChecklist: top ? [ + `Trace evidence papers: ${top.evidence.map((item) => item.paperId).join(", ")}`, + `Confirm capability match for ${top.method}.`, + `Plan a preregistered validation for ${top.topic}.` + ] : [] + }; +} + +module.exports = { + buildAssistantPacket, + digest, + extractSignals, + rankOpportunities +}; + diff --git a/negative-results-opportunity-radar/requirements-map.md b/negative-results-opportunity-radar/requirements-map.md new file mode 100644 index 0000000..0c09636 --- /dev/null +++ b/negative-results-opportunity-radar/requirements-map.md @@ -0,0 +1,14 @@ +# Requirements Map + +Issue #16 asks for an AI research assistant suite with literature review, research gap finding, hypothesis generation, peer-review help, summarization, citation management, and reproducibility support. + +| Requirement | Implementation | +| --- | --- | +| Research gap finder | `rankOpportunities` ranks gaps from negative results, limitations, and failed replications. | +| Literature review assistant | `extractSignals` turns paper metadata into normalized evidence records. | +| Hypothesis generation | Recommended actions convert negative evidence into bounded follow-up studies. | +| Peer review assistant | `buildAssistantPacket` emits prompts for checking missing negative-result context. | +| Citation support | Citation queries include topic, method, negative-result, limitation, and replication terms. | +| Reproducibility support | Opportunities flag failed replication and multi-paper evidence. | +| Scientific trust | Evidence records preserve paper IDs, titles, years, methods, topics, and digests. | + diff --git a/negative-results-opportunity-radar/test.js b/negative-results-opportunity-radar/test.js new file mode 100644 index 0000000..51773d1 --- /dev/null +++ b/negative-results-opportunity-radar/test.js @@ -0,0 +1,79 @@ +"use strict"; + +const assert = require("assert"); +const { + buildAssistantPacket, + extractSignals, + rankOpportunities +} = require("./index"); + +const papers = [ + { + id: "P1", + title: "Single-cell biomarker panel for immune relapse", + year: 2025, + topics: ["cancer-immunology", "biomarkers"], + methods: ["single-cell-rna", "cohort-validation"], + limitations: ["small donor diversity limited generalization"], + negativeResults: ["marker panel failed in low-input samples"], + failedReplications: [] + }, + { + id: "P2", + title: "External validation of relapse classifiers", + year: 2026, + topics: ["cancer-immunology", "biomarkers"], + methods: ["single-cell-rna"], + limitations: ["public cohort lacked paired tissue samples"], + negativeResults: [], + failedReplications: ["classifier did not reproduce on public atlas data"] + }, + { + id: "P3", + title: "Perovskite stress test benchmark", + year: 2024, + topics: ["materials"], + methods: ["xray-diffraction"], + limitations: ["humidity chamber unavailable"], + negativeResults: ["annealing protocol decreased stability"], + failedReplications: [] + } +]; + +const signals = extractSignals(papers[0]); +assert.strictEqual(signals.length, 2); +assert.ok(signals.every((signal) => signal.evidenceDigest.length > 20)); + +const opportunities = rankOpportunities({ + papers, + labCapabilities: ["single-cell-rna", "cohort-validation"], + userInterests: ["cancer-immunology", "biomarkers"], + minimumScore: 20 +}); + +assert.ok(opportunities.length >= 2); +assert.strictEqual(opportunities[0].topic, "biomarkers"); +assert.strictEqual(opportunities[0].method, "single-cell-rna"); +assert.strictEqual(opportunities[0].confidence, "high"); +assert.ok(opportunities[0].evidence.some((item) => item.paperId === "P1")); +assert.ok(opportunities[0].evidence.some((item) => item.paperId === "P2")); +assert.ok(opportunities[0].reproducibilityFlags.includes("failed_replication_evidence")); +assert.ok(opportunities[0].recommendedActions.some((action) => action.includes("replication"))); + +const materials = opportunities.find((item) => item.topic === "materials"); +assert.ok(materials); +assert.ok(materials.score < opportunities[0].score); + +const packet = buildAssistantPacket({ + projectId: "immune-relapse-roadmap", + opportunities +}); + +assert.strictEqual(packet.rankedOpportunityCount, opportunities.length); +assert.ok(packet.topRecommendation.focus.includes("single-cell-rna")); +assert.ok(packet.peerReviewPrompts[0].includes("negative")); +assert.ok(packet.citationQueries[0].includes("failed replication")); +assert.ok(packet.reproducibilityChecklist[0].includes("P1")); + +console.log("negative-results-opportunity-radar tests passed"); +