Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions negative-results-opportunity-radar/README.md
Original file line number Diff line number Diff line change
@@ -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.
30 changes: 30 additions & 0 deletions negative-results-opportunity-radar/acceptance-notes.md
Original file line number Diff line number Diff line change
@@ -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.
52 changes: 52 additions & 0 deletions negative-results-opportunity-radar/demo.js
Original file line number Diff line number Diff line change
@@ -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));

Binary file added negative-results-opportunity-radar/demo.mp4
Binary file not shown.
42 changes: 42 additions & 0 deletions negative-results-opportunity-radar/demo.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
224 changes: 224 additions & 0 deletions negative-results-opportunity-radar/index.js
Original file line number Diff line number Diff line change
@@ -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
};

14 changes: 14 additions & 0 deletions negative-results-opportunity-radar/requirements-map.md
Original file line number Diff line number Diff line change
@@ -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. |

Loading