From 9f70184bc972ed05f50ed35ee432b3371c234f06 Mon Sep 17 00:00:00 2001 From: kiheon0709 Date: Fri, 15 May 2026 17:32:53 +0900 Subject: [PATCH] Add research assistant evidence grounding --- .../README.md | 62 +++++ .../docs/demo.mp4 | Bin 0 -> 74934 bytes .../docs/demo.svg | 64 +++++ .../package.json | 12 + .../sample/assistant-fixture.json | 92 +++++++ .../src/cli.js | 25 ++ .../src/index.js | 242 ++++++++++++++++++ .../test/assistant.test.js | 57 +++++ 8 files changed, 554 insertions(+) create mode 100644 research-assistant-evidence-grounding/README.md create mode 100644 research-assistant-evidence-grounding/docs/demo.mp4 create mode 100644 research-assistant-evidence-grounding/docs/demo.svg create mode 100644 research-assistant-evidence-grounding/package.json create mode 100644 research-assistant-evidence-grounding/sample/assistant-fixture.json create mode 100755 research-assistant-evidence-grounding/src/cli.js create mode 100644 research-assistant-evidence-grounding/src/index.js create mode 100644 research-assistant-evidence-grounding/test/assistant.test.js 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 0000000000000000000000000000000000000000..2342fccf0944c4ea238059ccd0e1601363ab3c6b GIT binary patch literal 74934 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 + 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')); +});