diff --git a/research-assistant-evidence-grounding/README.md b/research-assistant-evidence-grounding/README.md new file mode 100644 index 0000000..c8f61a0 --- /dev/null +++ b/research-assistant-evidence-grounding/README.md @@ -0,0 +1,62 @@ +# Research Assistant Evidence Grounding + +Self-contained MVP module for issue #16, **AI-Powered Research Assistant Suite**. It focuses on evidence-grounded assistant behavior: claims-vs-evidence mapping, pre-submission peer review, reproducibility readiness, and research gap prioritization. + +## What It Covers + +- Auto peer-review reports grounded in manuscript sections, domain templates, and evidence alignment. +- Claims-vs-evidence matrix for citations, datasets, protocols, statistical analyses, and invalid/missing references. +- Reproducibility checker for environment lockfiles, raw data checksums, pipeline steps, reported output consistency, and prior attempt links. +- Research gap finder that ranks unresolved questions, low-replication clusters, negative signals, and user/lab fit. +- Aggregated assistant brief with release status, blockers, top gap, audit hashes, and reviewer-ready signals. + +## Run + +```bash +npm run check +``` + +That runs: + +```bash +npm test +npm run demo +``` + +Expected demo shape: + +```json +{ + "projectId": "project-ai-biomarker-002", + "status": "needs_researcher_attention", + "evidenceCoverage": 0.667, + "peerReviewRecommendation": "revise_before_release", + "reproducibilityConfidence": 100, + "topResearchGap": "gap-spatial-microglia" +} +``` + +## Requirement Map + +| Issue #16 requirement | Implementation evidence | +| --- | --- | +| Auto peer review reports with clarity, methodology, missing citations, and claims-vs-evidence alignment | `generatePeerReviewReport()` applies domain templates and emits severity-tagged findings for clarity, methodology, and weak claims. | +| Adaptive templates per domain | `DOMAIN_TEMPLATES` maps molecular biology, clinical trials, quantum physics, and generic reviewer lenses/risks. | +| Researcher feedback before release/internal review | `buildAssistantBrief()` combines evidence coverage, peer-review recommendation, blockers, and readiness status. | +| Reproducibility checker for code/notebooks, dependencies, raw data, and reported results | `runReproducibilityCheck()` scores environment lockfiles, raw data checksums, pipeline steps, reported outputs, and prior attempts. | +| Reproducibility confidence score and prior attempt links | The reproducibility result includes `confidenceScore`, `status`, `blockers`, and `attemptLinks`. | +| Research gap finder for under-studied intersections, unresolved questions, low replication, negative results, and user fit | `findResearchGaps()` ranks corpus opportunities by unresolved questions, replication count, negative signals, interests, and lab capabilities. | +| Project-aware AI research assistant output | `buildEvidenceMap()` and `buildAssistantBrief()` generate deterministic audit hashes and reviewer-ready assistant summaries. | + +## Files + +- `src/index.js` - assistant rules and exported functions. +- `src/cli.js` - reviewer demo command. +- `sample/assistant-fixture.json` - manuscript, evidence library, reproducibility, and corpus fixture. +- `test/assistant.test.js` - regression tests for normalization, evidence mapping, peer review, reproducibility, gap ranking, and brief aggregation. +- `docs/demo.svg` - visual walkthrough for PR review. +- `docs/demo.mp4` - short generated video walkthrough for maintainers who prefer an inline demo artifact. + +## Notes + +This module is dependency-free and credential-free. It is designed as a deterministic foundation for a future LLM provider adapter, sandbox execution runner, citation index, and UI workflow. diff --git a/research-assistant-evidence-grounding/docs/demo.mp4 b/research-assistant-evidence-grounding/docs/demo.mp4 new file mode 100644 index 0000000..2342fcc Binary files /dev/null and b/research-assistant-evidence-grounding/docs/demo.mp4 differ diff --git a/research-assistant-evidence-grounding/docs/demo.svg b/research-assistant-evidence-grounding/docs/demo.svg new file mode 100644 index 0000000..3f6c765 --- /dev/null +++ b/research-assistant-evidence-grounding/docs/demo.svg @@ -0,0 +1,64 @@ + + Research assistant evidence grounding demo + Visual walkthrough of claims-to-evidence mapping, peer review, reproducibility scoring, and research gap ranking. + + + + + + + + + + + + + SCIBASE AI Research Assistant Suite + Evidence-grounded review + reproducibility + gap discovery + + + 1. Evidence Map + claims ↔ datasets + citations ↔ protocols + coverage 0.667 + + + + + 2. Peer Review + clarity + methods + claims-vs-evidence + revise before release + + + + + 3. Repro Check + lockfile + raw data + pipeline + outputs + confidence 100 + + + 4. Gap Finder + low replication + negative signals + matches lab capabilities + gap-spatial-microglia + + + + + Assistant Brief + status: needs_researcher_attention + blocker: weak clinical-readiness claim + audit-ready JSON + diff --git a/research-assistant-evidence-grounding/package.json b/research-assistant-evidence-grounding/package.json new file mode 100644 index 0000000..f3aad92 --- /dev/null +++ b/research-assistant-evidence-grounding/package.json @@ -0,0 +1,12 @@ +{ + "name": "research-assistant-evidence-grounding", + "version": "0.1.0", + "private": true, + "type": "module", + "description": "Dependency-free evidence-grounding assistant module for SCIBASE issue #16.", + "scripts": { + "test": "node --test test/*.test.js", + "demo": "node src/cli.js", + "check": "npm test && npm run demo" + } +} diff --git a/research-assistant-evidence-grounding/sample/assistant-fixture.json b/research-assistant-evidence-grounding/sample/assistant-fixture.json new file mode 100644 index 0000000..42536d9 --- /dev/null +++ b/research-assistant-evidence-grounding/sample/assistant-fixture.json @@ -0,0 +1,92 @@ +{ + "projectId": "project-ai-biomarker-002", + "domain": "molecular_biology", + "researcherInterests": ["single-cell", "alzheimers", "biomarkers"], + "labCapabilities": ["single-cell-rna-seq", "mouse-model", "spatial-transcriptomics"], + "manuscript": { + "title": "Single-cell inflammatory biomarker panel for early Alzheimer's progression", + "abstract": "We present a single-cell RNA sequencing analysis of inflammatory signatures associated with early Alzheimer's progression. The study combines a curated cohort, reproducible processing workflow, and pathway enrichment review to prioritize candidate biomarkers for follow-up validation.", + "sections": [ + { "name": "Methods", "text": "Batch-effect controls, differential expression thresholds, and pathway enrichment strategy are described with protocol references." }, + { "name": "Results", "text": "A three-gene inflammatory panel stratifies early-stage samples and aligns with microglial activation literature." } + ], + "claims": [ + { + "id": "claim-panel-accuracy", + "text": "The three-gene inflammatory panel separates early-stage Alzheimer's samples from controls.", + "importance": "high", + "expectedEvidence": ["dataset", "statistical-analysis", "protocol"], + "artifacts": ["dataset-cohort", "analysis-differential-expression", "protocol-processing"], + "citations": ["citation-microglia-review"] + }, + { + "id": "claim-pathway-novelty", + "text": "The pathway intersection is under-studied in spatial transcriptomics replication cohorts.", + "importance": "medium", + "expectedEvidence": ["literature-scan"], + "artifacts": ["gap-scan-spatial"], + "citations": ["citation-negative-results"] + }, + { + "id": "claim-clinical-readiness", + "text": "The panel is ready for clinical deployment.", + "importance": "high", + "expectedEvidence": ["clinical-validation", "ethics-approval"], + "artifacts": ["analysis-differential-expression"], + "citations": [] + } + ] + }, + "evidenceLibrary": [ + { "id": "dataset-cohort", "type": "dataset", "title": "Curated single-cell cohort", "year": 2026, "checksum": "sha256:cohort", "reproducible": true }, + { "id": "analysis-differential-expression", "type": "statistical-analysis", "title": "Differential expression notebook", "year": 2026, "checksum": "sha256:analysis", "reproducible": true }, + { "id": "protocol-processing", "type": "protocol", "title": "Cell filtering and integration protocol", "year": 2025, "peerReviewed": true }, + { "id": "citation-microglia-review", "type": "literature-scan", "title": "Microglial activation review", "year": 2023, "peerReviewed": true }, + { "id": "gap-scan-spatial", "type": "literature-scan", "title": "Spatial transcriptomics gap scan", "year": 2026, "reproducible": true }, + { "id": "citation-negative-results", "type": "literature-scan", "title": "Negative replication signals in neuroinflammation", "year": 2024, "peerReviewed": true } + ], + "reproducibility": { + "environment": { "type": "docker", "lockfile": "renv.lock" }, + "rawData": { "available": true, "checksum": "sha256:raw" }, + "pipelineSteps": [ + { "id": "ingest", "command": "Rscript scripts/ingest.R", "input": "raw/", "output": "derived/cell-matrix.rds" }, + { "id": "model", "command": "Rscript scripts/model.R", "input": "derived/cell-matrix.rds", "output": "results/panel.json" } + ], + "reportedResults": [ + { "metric": "panel_auc", "expected": 0.88, "observed": 0.88 }, + { "metric": "signature_count", "expected": 3, "observed": 3 } + ], + "previousAttempts": [ + { "status": "passed", "url": "https://scibase.example/repro/project-ai-biomarker-002/attempt-1" } + ] + }, + "corpus": [ + { + "id": "gap-spatial-microglia", + "title": "Spatial replication of microglial inflammatory biomarkers", + "tags": ["single-cell", "alzheimers", "spatial-transcriptomics"], + "requiredCapabilities": ["spatial-transcriptomics"], + "replicationCount": 0, + "unresolvedQuestions": ["Do spatial niches preserve the single-cell inflammatory signature?", "Which cell-cell interactions explain false positives?"], + "negativeSignals": ["bulk RNA-seq replication was inconsistent"] + }, + { + "id": "gap-mouse-model-transfer", + "title": "Mouse model transferability of human inflammatory panel", + "tags": ["alzheimers", "mouse-model"], + "requiredCapabilities": ["mouse-model"], + "replicationCount": 1, + "unresolvedQuestions": ["Which model captures early-stage microglial state?"], + "negativeSignals": [] + }, + { + "id": "well-covered-proteomics", + "title": "Proteomics validation of established amyloid markers", + "tags": ["proteomics"], + "requiredCapabilities": ["mass-spec"], + "replicationCount": 6, + "unresolvedQuestions": [], + "negativeSignals": [] + } + ] +} diff --git a/research-assistant-evidence-grounding/src/cli.js b/research-assistant-evidence-grounding/src/cli.js new file mode 100755 index 0000000..3160c1f --- /dev/null +++ b/research-assistant-evidence-grounding/src/cli.js @@ -0,0 +1,25 @@ +#!/usr/bin/env node +import fs from 'node:fs'; +import path from 'node:path'; +import { buildAssistantBrief, buildEvidenceMap, generatePeerReviewReport, runReproducibilityCheck, findResearchGaps } from './index.js'; + +const fixturePath = process.argv[2] || path.join(import.meta.dirname, '..', 'sample', 'assistant-fixture.json'); +const project = JSON.parse(fs.readFileSync(fixturePath, 'utf8')); +const evidenceMap = buildEvidenceMap(project); +const peerReview = generatePeerReviewReport(project, evidenceMap); +const reproducibility = runReproducibilityCheck(project); +const gaps = findResearchGaps(project); +const brief = buildAssistantBrief(project); + +console.log(JSON.stringify({ + projectId: brief.projectId, + status: brief.status, + evidenceCoverage: brief.evidenceCoverage, + peerReviewRecommendation: brief.peerReviewRecommendation, + reproducibilityConfidence: brief.reproducibilityConfidence, + topResearchGap: brief.topResearchGap?.id, + weakClaims: evidenceMap.weakClaims.map((claim) => claim.claimId), + peerReviewFindings: peerReview.findings.length, + gapCount: gaps.opportunities.length, + auditHash: brief.auditHash +}, null, 2)); diff --git a/research-assistant-evidence-grounding/src/index.js b/research-assistant-evidence-grounding/src/index.js new file mode 100644 index 0000000..47340eb --- /dev/null +++ b/research-assistant-evidence-grounding/src/index.js @@ -0,0 +1,242 @@ +import crypto from 'node:crypto'; + +const DOMAIN_TEMPLATES = { + molecular_biology: { + requiredEvidence: ['dataset', 'protocol', 'statistical-analysis'], + commonRisks: ['missing replication cohort', 'unreported controls', 'batch-effect risk'], + reviewerLens: 'methods and biological validity' + }, + clinical_trials: { + requiredEvidence: ['protocol', 'statistical-analysis', 'ethics-approval'], + commonRisks: ['underpowered subgroup', 'missing adverse-event table', 'endpoint drift'], + reviewerLens: 'trial design and safety reporting' + }, + quantum_physics: { + requiredEvidence: ['simulation-code', 'raw-measurements', 'calibration-log'], + commonRisks: ['insufficient calibration detail', 'unbounded numerical error'], + reviewerLens: 'theoretical assumptions and experimental calibration' + }, + generic: { + requiredEvidence: ['dataset', 'code', 'method-note'], + commonRisks: ['missing citation', 'unclear method', 'weak evidence trace'], + reviewerLens: 'clarity, rigor, and reproducibility' + } +}; + +const REQUIRED_PROJECT_KEYS = ['projectId', 'domain', 'manuscript', 'evidenceLibrary', 'reproducibility', 'corpus']; + +function stableStringify(value) { + if (Array.isArray(value)) return `[${value.map(stableStringify).join(',')}]`; + if (value && typeof value === 'object') { + return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${stableStringify(value[key])}`).join(',')}}`; + } + return JSON.stringify(value); +} + +function stableHash(value) { + return crypto.createHash('sha256').update(stableStringify(value)).digest('hex'); +} + +function assertObject(value, name) { + if (!value || typeof value !== 'object' || Array.isArray(value)) { + throw new Error(`${name} must be an object`); + } +} + +export function normalizeProject(project) { + assertObject(project, 'project'); + const missing = REQUIRED_PROJECT_KEYS.filter((key) => project[key] === undefined || project[key] === null); + if (missing.length) throw new Error(`project missing required fields: ${missing.join(', ')}`); + assertObject(project.manuscript, 'manuscript'); + if (!Array.isArray(project.manuscript.claims) || project.manuscript.claims.length === 0) { + throw new Error('manuscript.claims must include at least one claim'); + } + if (!Array.isArray(project.evidenceLibrary)) throw new Error('evidenceLibrary must be an array'); + if (!Array.isArray(project.corpus)) throw new Error('corpus must be an array'); + const template = DOMAIN_TEMPLATES[project.domain] || DOMAIN_TEMPLATES.generic; + const normalizedClaims = project.manuscript.claims.map((claim, index) => { + if (!claim.id || !claim.text) throw new Error(`claim at index ${index} requires id and text`); + return { + expectedEvidence: [], + citations: [], + artifacts: [], + importance: 'medium', + ...claim + }; + }); + return { + ...project, + domainTemplate: template, + manuscript: { + title: project.manuscript.title || 'Untitled manuscript', + abstract: project.manuscript.abstract || '', + sections: project.manuscript.sections || [], + claims: normalizedClaims + }, + auditHash: stableHash({ projectId: project.projectId, claims: normalizedClaims, evidence: project.evidenceLibrary }) + }; +} + +function scoreEvidenceForClaim(claim, evidenceById) { + const expected = new Set(claim.expectedEvidence || []); + const artifactHits = (claim.artifacts || []).map((id) => evidenceById.get(id)).filter(Boolean); + const citationHits = (claim.citations || []).map((id) => evidenceById.get(id)).filter(Boolean); + const allHits = [...artifactHits, ...citationHits]; + const presentTypes = new Set(allHits.map((item) => item.type)); + const missingTypes = [...expected].filter((type) => !presentTypes.has(type)); + const invalidReferences = [...(claim.artifacts || []), ...(claim.citations || [])].filter((id) => !evidenceById.has(id)); + const staleReferences = allHits.filter((item) => item.year && item.year < 2020).map((item) => item.id); + const reproducibleHits = allHits.filter((item) => item.reproducible || item.peerReviewed).length; + const expectedCoverage = expected.size ? (expected.size - missingTypes.length) / expected.size : allHits.length > 0 ? 1 : 0; + const qualityBoost = Math.min(0.25, reproducibleHits * 0.08); + const penalty = invalidReferences.length * 0.18 + staleReferences.length * 0.05; + const supportScore = Math.max(0, Math.min(1, Number((expectedCoverage * 0.75 + qualityBoost - penalty).toFixed(3)))); + return { + claimId: claim.id, + text: claim.text, + importance: claim.importance, + supportScore, + supported: supportScore >= 0.72 && invalidReferences.length === 0, + evidenceIds: allHits.map((item) => item.id), + presentTypes: [...presentTypes].sort(), + missingTypes, + invalidReferences, + staleReferences + }; +} + +export function buildEvidenceMap(projectInput) { + const project = normalizeProject(projectInput); + const evidenceById = new Map(project.evidenceLibrary.map((item) => [item.id, item])); + const claimMap = project.manuscript.claims.map((claim) => scoreEvidenceForClaim(claim, evidenceById)); + const weakClaims = claimMap.filter((claim) => !claim.supported || claim.missingTypes.length > 0 || claim.invalidReferences.length > 0); + const coverage = Number((claimMap.filter((claim) => claim.supported).length / claimMap.length).toFixed(3)); + return { + projectId: project.projectId, + domain: project.domain, + reviewerLens: project.domainTemplate.reviewerLens, + coverage, + readyForInternalReview: coverage >= 0.75 && weakClaims.length <= 1, + weakClaims, + claims: claimMap, + auditHash: stableHash({ project: project.projectId, claimMap }) + }; +} + +export function generatePeerReviewReport(projectInput, evidenceMap = buildEvidenceMap(projectInput)) { + const project = normalizeProject(projectInput); + const findings = []; + if ((project.manuscript.abstract || '').split(/\s+/).filter(Boolean).length < 40) { + findings.push({ severity: 'medium', category: 'clarity', message: 'Abstract is short; add design, data, and result summary before release.' }); + } + for (const risk of project.domainTemplate.commonRisks) { + if (!project.manuscript.sections.some((section) => JSON.stringify(section).toLowerCase().includes(risk.split(' ')[0]))) { + findings.push({ severity: 'low', category: 'domain-template', message: `Domain template check: ${risk}.` }); + } + } + for (const weak of evidenceMap.weakClaims) { + const severity = weak.importance === 'high' ? 'high' : 'medium'; + findings.push({ + severity, + category: 'claims-vs-evidence', + claimId: weak.claimId, + message: `Claim support score ${weak.supportScore}; missing ${weak.missingTypes.join(', ') || 'no required types'}; invalid references ${weak.invalidReferences.join(', ') || 'none'}.` + }); + } + const methodologicalSignals = project.evidenceLibrary.filter((item) => ['protocol', 'statistical-analysis', 'simulation-code'].includes(item.type)); + if (methodologicalSignals.length === 0) { + findings.push({ severity: 'high', category: 'methodology', message: 'No protocol, statistical-analysis, or simulation-code evidence found.' }); + } + const highSeverity = findings.filter((finding) => finding.severity === 'high').length; + const mediumSeverity = findings.filter((finding) => finding.severity === 'medium').length; + return { + projectId: project.projectId, + template: project.domain, + reviewerLens: project.domainTemplate.reviewerLens, + recommendation: highSeverity ? 'revise_before_release' : mediumSeverity > 2 ? 'minor_revision' : 'ready_for_team_review', + findings, + summary: `${findings.length} findings (${highSeverity} high, ${mediumSeverity} medium) across clarity, methodology, and claims-vs-evidence checks.`, + auditHash: stableHash({ projectId: project.projectId, findings }) + }; +} + +export function runReproducibilityCheck(projectInput) { + const project = normalizeProject(projectInput); + const repro = project.reproducibility || {}; + const checks = [ + { id: 'environment', passed: Boolean(repro.environment?.type && repro.environment?.lockfile), weight: 20, message: 'Environment definition and lockfile present.' }, + { id: 'raw-data', passed: Boolean(repro.rawData?.available && repro.rawData?.checksum), weight: 20, message: 'Raw data availability and checksum present.' }, + { id: 'pipeline', passed: Array.isArray(repro.pipelineSteps) && repro.pipelineSteps.length > 0 && repro.pipelineSteps.every((step) => step.command && step.input && step.output), weight: 20, message: 'Raw-to-results pipeline steps are declared.' }, + { id: 'reported-results', passed: Array.isArray(repro.reportedResults) && repro.reportedResults.every((result) => result.expected === result.observed), weight: 25, message: 'Reported outputs match observed outputs.' }, + { id: 'attempt-history', passed: Array.isArray(repro.previousAttempts) && repro.previousAttempts.some((attempt) => attempt.status === 'passed'), weight: 15, message: 'Links to previous reproducibility attempts exist.' } + ]; + const passedWeight = checks.filter((check) => check.passed).reduce((sum, check) => sum + check.weight, 0); + const blockers = checks.filter((check) => !check.passed).map((check) => ({ id: check.id, message: check.message })); + const confidenceScore = passedWeight; + return { + projectId: project.projectId, + confidenceScore, + status: blockers.length === 0 ? 'reproducible' : confidenceScore >= 70 ? 'needs_minor_evidence' : 'blocked', + checks, + blockers, + attemptLinks: (repro.previousAttempts || []).map((attempt) => attempt.url).filter(Boolean), + auditHash: stableHash({ projectId: project.projectId, checks, blockers }) + }; +} + +export function findResearchGaps(projectInput) { + const project = normalizeProject(projectInput); + const interests = new Set(project.researcherInterests || []); + const capabilities = new Set(project.labCapabilities || []); + const opportunities = project.corpus + .filter((record) => record.unresolvedQuestions?.length || record.replicationCount < 2 || record.negativeSignals?.length) + .map((record) => { + const interestOverlap = (record.tags || []).filter((tag) => interests.has(tag)).length; + const capabilityOverlap = (record.requiredCapabilities || []).filter((capability) => capabilities.has(capability)).length; + const unresolvedScore = Math.min(30, (record.unresolvedQuestions || []).length * 10); + const replicationGap = record.replicationCount < 2 ? 25 : 0; + const negativeSignalScore = Math.min(20, (record.negativeSignals || []).length * 10); + const fitScore = Math.min(25, interestOverlap * 7 + capabilityOverlap * 6); + const priorityScore = unresolvedScore + replicationGap + negativeSignalScore + fitScore; + return { + id: record.id, + title: record.title, + tags: record.tags || [], + unresolvedQuestions: record.unresolvedQuestions || [], + negativeSignals: record.negativeSignals || [], + replicationCount: record.replicationCount || 0, + priorityScore, + rationale: [ + replicationGap ? 'low replication' : null, + unresolvedScore ? 'unresolved questions' : null, + negativeSignalScore ? 'negative results/limitations available' : null, + fitScore ? 'matches researcher interests or lab capabilities' : null + ].filter(Boolean) + }; + }) + .sort((a, b) => b.priorityScore - a.priorityScore || a.id.localeCompare(b.id)); + return { + projectId: project.projectId, + opportunities, + topOpportunity: opportunities[0] || null, + auditHash: stableHash({ projectId: project.projectId, opportunities }) + }; +} + +export function buildAssistantBrief(projectInput) { + const evidenceMap = buildEvidenceMap(projectInput); + const peerReview = generatePeerReviewReport(projectInput, evidenceMap); + const reproducibility = runReproducibilityCheck(projectInput); + const gaps = findResearchGaps(projectInput); + const releaseReady = evidenceMap.readyForInternalReview && peerReview.recommendation !== 'revise_before_release' && reproducibility.confidenceScore >= 80; + return { + projectId: evidenceMap.projectId, + status: releaseReady ? 'assistant_ready' : 'needs_researcher_attention', + evidenceCoverage: evidenceMap.coverage, + peerReviewRecommendation: peerReview.recommendation, + reproducibilityConfidence: reproducibility.confidenceScore, + topResearchGap: gaps.topOpportunity, + blockers: [...evidenceMap.weakClaims.map((claim) => `weak-claim:${claim.claimId}`), ...reproducibility.blockers.map((blocker) => `repro:${blocker.id}`)], + auditHash: stableHash({ evidenceMap, peerReview, reproducibility, topGap: gaps.topOpportunity }) + }; +} diff --git a/research-assistant-evidence-grounding/test/assistant.test.js b/research-assistant-evidence-grounding/test/assistant.test.js new file mode 100644 index 0000000..1f9beb5 --- /dev/null +++ b/research-assistant-evidence-grounding/test/assistant.test.js @@ -0,0 +1,57 @@ +import assert from 'node:assert/strict'; +import fs from 'node:fs'; +import path from 'node:path'; +import test from 'node:test'; +import { buildAssistantBrief, buildEvidenceMap, findResearchGaps, generatePeerReviewReport, normalizeProject, runReproducibilityCheck } from '../src/index.js'; + +const fixture = JSON.parse(fs.readFileSync(path.join(import.meta.dirname, '..', 'sample', 'assistant-fixture.json'), 'utf8')); + +test('normalizes project metadata and domain template requirements', () => { + const project = normalizeProject(fixture); + assert.equal(project.projectId, 'project-ai-biomarker-002'); + assert.equal(project.domainTemplate.reviewerLens, 'methods and biological validity'); + assert.equal(project.manuscript.claims.length, 3); + assert.match(project.auditHash, /^[a-f0-9]{64}$/); +}); + +test('maps manuscript claims to supporting evidence and weak evidence gaps', () => { + const evidenceMap = buildEvidenceMap(fixture); + assert.equal(evidenceMap.coverage, 0.667); + assert.equal(evidenceMap.readyForInternalReview, false); + const clinicalClaim = evidenceMap.weakClaims.find((claim) => claim.claimId === 'claim-clinical-readiness'); + assert.ok(clinicalClaim); + assert.deepEqual(clinicalClaim.missingTypes, ['clinical-validation', 'ethics-approval']); + assert.equal(evidenceMap.claims.find((claim) => claim.claimId === 'claim-panel-accuracy').supported, true); +}); + +test('generates adaptive peer-review findings for claim evidence alignment', () => { + const review = generatePeerReviewReport(fixture); + assert.equal(review.recommendation, 'revise_before_release'); + assert.ok(review.findings.some((finding) => finding.category === 'claims-vs-evidence' && finding.severity === 'high')); + assert.match(review.summary, /findings/); +}); + +test('scores reproducibility readiness from environment, data, pipeline, outputs, and attempts', () => { + const reproducibility = runReproducibilityCheck(fixture); + assert.equal(reproducibility.status, 'reproducible'); + assert.equal(reproducibility.confidenceScore, 100); + assert.equal(reproducibility.blockers.length, 0); + assert.equal(reproducibility.attemptLinks.length, 1); +}); + +test('finds research gaps ranked by unresolved questions, replication, negative signals, and fit', () => { + const gaps = findResearchGaps(fixture); + assert.equal(gaps.topOpportunity.id, 'gap-spatial-microglia'); + assert.ok(gaps.topOpportunity.priorityScore > gaps.opportunities[1].priorityScore); + assert.deepEqual(gaps.topOpportunity.rationale, ['low replication', 'unresolved questions', 'negative results/limitations available', 'matches researcher interests or lab capabilities']); +}); + +test('builds an assistant brief aggregating peer review, reproducibility, and research gaps', () => { + const brief = buildAssistantBrief(fixture); + assert.equal(brief.status, 'needs_researcher_attention'); + assert.equal(brief.evidenceCoverage, 0.667); + assert.equal(brief.peerReviewRecommendation, 'revise_before_release'); + assert.equal(brief.reproducibilityConfidence, 100); + assert.equal(brief.topResearchGap.id, 'gap-spatial-microglia'); + assert.ok(brief.blockers.includes('weak-claim:claim-clinical-readiness')); +});