diff --git a/README.md b/README.md index d338cf68..450f344b 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,6 @@ # deepevents.ai deepevents.ai main codebase + +## Research Assistant Bounty Slices + +- [Literature freshness review assistant](literature-freshness-review-assistant/README.md) diff --git a/literature-freshness-review-assistant/README.md b/literature-freshness-review-assistant/README.md new file mode 100644 index 00000000..33fcbbe2 --- /dev/null +++ b/literature-freshness-review-assistant/README.md @@ -0,0 +1,33 @@ +# Literature Freshness Review Assistant + +This is a focused AI-Powered Research Assistant Suite slice for SCIBASE issue #16. It checks whether reviewer-facing manuscript claims rely on current evidence before the assistant marks a packet ready. + +## Scope + +- Flags stale support citations outside a configurable freshness window. +- Detects newer systematic reviews, reporting guidelines, dataset releases, and benchmark updates that are missing from a claim. +- Escalates newer contradictory evidence as a review hold. +- Detects dataset and benchmark version drift for claims using "current" or "state-of-the-art" wording. +- Emits reviewer actions, research-gap prompts, stable audit digests, and deterministic JSON/Markdown/SVG/MP4 artifacts. + +This is intentionally separate from broad assistant suites, citation-context reconciliation, retraction notices, statistics review, benchmark-leakage checks, reporting-guideline compliance, figure/table checks, prompt-safety guards, and assay control/calibration completeness. + +## Files + +- `index.js` - freshness evaluator and report renderers +- `sample-data.js` - synthetic evidence ledger and manuscript packets +- `test.js` - dependency-free assertion tests +- `demo.js` - deterministic JSON/Markdown/SVG report generator +- `scripts/render-demo-video.js` - optional ffmpeg MP4 renderer +- `reports/` - generated reviewer artifacts + +## Validate + +```bash +npm run check +npm test +npm run demo +npm run demo:video +``` + +Synthetic data only. The module does not call external APIs, scan private manuscripts, use credentials, or contact payment services. diff --git a/literature-freshness-review-assistant/demo.js b/literature-freshness-review-assistant/demo.js new file mode 100644 index 00000000..4c437104 --- /dev/null +++ b/literature-freshness-review-assistant/demo.js @@ -0,0 +1,28 @@ +const fs = require("fs"); +const path = require("path"); + +const { + evaluateFreshnessReview, + renderMarkdown, + renderSvg +} = require("./index"); +const { + freshnessPolicy, + evidenceLedger, + manuscriptPackets +} = require("./sample-data"); + +const reportsDir = path.join(__dirname, "reports"); +fs.mkdirSync(reportsDir, { recursive: true }); + +const report = evaluateFreshnessReview(manuscriptPackets, evidenceLedger, freshnessPolicy); +fs.writeFileSync(path.join(reportsDir, "demo.json"), `${JSON.stringify(report, null, 2)}\n`); +fs.writeFileSync(path.join(reportsDir, "demo.md"), renderMarkdown(report)); +fs.writeFileSync(path.join(reportsDir, "demo.svg"), renderSvg(report)); + +console.log(`status=${report.manuscripts[0].summary.status}`); +console.log(`reviewed=${report.aggregate.reviewedManuscripts}`); +console.log(`held=${report.aggregate.heldManuscripts}`); +console.log(`critical=${report.aggregate.criticalFindings}`); +console.log(`major=${report.aggregate.majorFindings}`); +console.log(`digest=${report.auditDigest}`); diff --git a/literature-freshness-review-assistant/index.js b/literature-freshness-review-assistant/index.js new file mode 100644 index 00000000..c0f3d6b4 --- /dev/null +++ b/literature-freshness-review-assistant/index.js @@ -0,0 +1,322 @@ +const crypto = require("crypto"); + +const SEVERITY_RANK = { + info: 0, + warning: 1, + major: 2, + critical: 3 +}; + +function yearsBetween(olderDate, newerDate) { + const older = new Date(`${olderDate}T00:00:00Z`); + const newer = new Date(`${newerDate}T00:00:00Z`); + return (newer.getTime() - older.getTime()) / (365.25 * 24 * 60 * 60 * 1000); +} + +function latestDate(dates) { + const validDates = dates.filter(Boolean).sort(); + return validDates[validDates.length - 1] || null; +} + +function evidenceForTopic(ledger, topic) { + return ledger.signals.filter((signal) => signal.topic === topic); +} + +function addFinding(findings, finding) { + findings.push({ + severity: finding.severity, + code: finding.code, + claimId: finding.claimId, + topic: finding.topic, + detail: finding.detail, + requiredAction: finding.requiredAction, + evidenceSignalId: finding.evidenceSignalId || null + }); +} + +function evaluateClaimFreshness(claim, ledger, policy) { + const findings = []; + const signals = evidenceForTopic(ledger, claim.topic); + const latestCitation = latestDate(claim.citationDates || []); + const reviewDate = policy.reviewDate; + + if (!latestCitation) { + addFinding(findings, { + severity: "critical", + code: "NO_SUPPORT_DATES", + claimId: claim.id, + topic: claim.topic, + detail: "Claim has no dated supporting citations for freshness review.", + requiredAction: "Attach dated evidence before the assistant marks this claim reviewer-ready." + }); + } else if (yearsBetween(latestCitation, reviewDate) > policy.staleAfterYears) { + addFinding(findings, { + severity: "major", + code: "STALE_SUPPORT_WINDOW", + claimId: claim.id, + topic: claim.topic, + detail: `Latest support is ${latestCitation}, older than the ${policy.staleAfterYears}-year freshness window.`, + requiredAction: "Add recent evidence or downgrade the claim wording." + }); + } + + for (const signal of signals) { + const cited = (claim.citedCurrentSignalIds || []).includes(signal.id); + const signalIsNewer = !latestCitation || signal.published > latestCitation; + + if (!cited && signalIsNewer) { + const severity = signal.relation === "contradicts" ? policy.contradictionSeverity : "major"; + addFinding(findings, { + severity, + code: signal.relation === "contradicts" ? "NEWER_CONTRADICTORY_EVIDENCE" : "MISSING_NEWER_EVIDENCE", + claimId: claim.id, + topic: claim.topic, + detail: `${signal.title} (${signal.published}) ${signal.relation} older support but is not cited.`, + requiredAction: signal.requirement, + evidenceSignalId: signal.id + }); + } + + if (signal.currentVersion && claim.datasetVersion && claim.datasetVersion !== signal.currentVersion) { + addFinding(findings, { + severity: "major", + code: "DATASET_VERSION_DRIFT", + claimId: claim.id, + topic: claim.topic, + detail: `Claim uses dataset ${claim.datasetVersion}; current ledger version is ${signal.currentVersion}.`, + requiredAction: signal.requirement, + evidenceSignalId: signal.id + }); + } + + if (signal.currentVersion && claim.benchmarkVersion && claim.benchmarkVersion !== signal.currentVersion) { + addFinding(findings, { + severity: "major", + code: "BENCHMARK_VERSION_DRIFT", + claimId: claim.id, + topic: claim.topic, + detail: `Claim uses benchmark ${claim.benchmarkVersion}; current ledger version is ${signal.currentVersion}.`, + requiredAction: signal.requirement, + evidenceSignalId: signal.id + }); + } + } + + if (findings.length === 0) { + addFinding(findings, { + severity: "info", + code: "FRESHNESS_READY", + claimId: claim.id, + topic: claim.topic, + detail: "Claim cites the current evidence signal for its topic.", + requiredAction: "No freshness hold required." + }); + } + + return findings; +} + +function summarizeFindings(findings, policy) { + const actionable = findings.filter((finding) => finding.severity !== "info"); + const critical = findings.filter((finding) => finding.severity === "critical"); + const major = findings.filter((finding) => finding.severity === "major"); + const warnings = findings.filter((finding) => finding.severity === "warning"); + const maxSeverity = findings.reduce((max, finding) => { + return SEVERITY_RANK[finding.severity] > SEVERITY_RANK[max] ? finding.severity : max; + }, "info"); + const status = SEVERITY_RANK[maxSeverity] >= SEVERITY_RANK[policy.holdSeverityCutoff] + ? "hold_freshness_review" + : actionable.length > 0 + ? "revise_freshness_context" + : "ready_for_review"; + + return { + status, + totalFindings: findings.length, + actionableFindings: actionable.length, + criticalFindings: critical.length, + majorFindings: major.length, + warningFindings: warnings.length, + readyFindings: findings.length - actionable.length + }; +} + +function buildReviewerActions(findings) { + return findings + .filter((finding) => finding.severity !== "info") + .map((finding) => ({ + claimId: finding.claimId, + severity: finding.severity, + action: finding.requiredAction, + reason: finding.detail + })); +} + +function buildResearchGapPrompts(findings) { + const topics = new Map(); + for (const finding of findings) { + if (finding.severity === "info") { + continue; + } + if (!topics.has(finding.topic)) { + topics.set( + finding.topic, + `Freshness gap: update the evidence base for ${finding.topic} before treating the claim as current.` + ); + } + } + return [...topics.values()]; +} + +function stableDigest(value) { + return crypto.createHash("sha256").update(JSON.stringify(value)).digest("hex"); +} + +function evaluateManuscript(packet, ledger, policy) { + const findings = packet.claims.flatMap((claim) => evaluateClaimFreshness(claim, ledger, policy)); + const summary = summarizeFindings(findings, policy); + const reviewerActions = buildReviewerActions(findings); + const researchGapPrompts = buildResearchGapPrompts(findings); + const result = { + packetId: packet.id, + title: packet.title, + domain: packet.domain, + reviewDate: policy.reviewDate, + summary, + findings, + reviewerActions, + researchGapPrompts + }; + return { + ...result, + auditDigest: stableDigest(result) + }; +} + +function evaluateFreshnessReview(packets, ledger, policy) { + const manuscripts = packets.map((packet) => evaluateManuscript(packet, ledger, policy)); + const aggregate = { + reviewedManuscripts: manuscripts.length, + heldManuscripts: manuscripts.filter((entry) => entry.summary.status === "hold_freshness_review").length, + revisionManuscripts: manuscripts.filter((entry) => entry.summary.status === "revise_freshness_context").length, + readyManuscripts: manuscripts.filter((entry) => entry.summary.status === "ready_for_review").length, + criticalFindings: manuscripts.reduce((sum, entry) => sum + entry.summary.criticalFindings, 0), + majorFindings: manuscripts.reduce((sum, entry) => sum + entry.summary.majorFindings, 0), + actionableFindings: manuscripts.reduce((sum, entry) => sum + entry.summary.actionableFindings, 0) + }; + const result = { + assistant: "literature-freshness-review-assistant", + policy, + aggregate, + manuscripts + }; + return { + ...result, + auditDigest: stableDigest(result) + }; +} + +function renderMarkdown(report) { + const lines = [ + "# Literature Freshness Review Assistant", + "", + `Review date: ${report.policy.reviewDate}`, + `Overall digest: ${report.auditDigest}`, + "", + "## Aggregate", + "", + `- Reviewed manuscripts: ${report.aggregate.reviewedManuscripts}`, + `- Held manuscripts: ${report.aggregate.heldManuscripts}`, + `- Revision manuscripts: ${report.aggregate.revisionManuscripts}`, + `- Ready manuscripts: ${report.aggregate.readyManuscripts}`, + `- Critical findings: ${report.aggregate.criticalFindings}`, + `- Major findings: ${report.aggregate.majorFindings}`, + `- Actionable findings: ${report.aggregate.actionableFindings}`, + "" + ]; + + for (const manuscript of report.manuscripts) { + lines.push(`## ${manuscript.title}`); + lines.push(""); + lines.push(`Status: ${manuscript.summary.status}`); + lines.push(`Digest: ${manuscript.auditDigest}`); + lines.push(""); + lines.push("| Severity | Code | Claim | Detail | Required action |"); + lines.push("| --- | --- | --- | --- | --- |"); + for (const finding of manuscript.findings) { + lines.push( + `| ${finding.severity} | ${finding.code} | ${finding.claimId} | ${finding.detail} | ${finding.requiredAction} |` + ); + } + lines.push(""); + if (manuscript.researchGapPrompts.length > 0) { + lines.push("Research gap prompts:"); + for (const prompt of manuscript.researchGapPrompts) { + lines.push(`- ${prompt}`); + } + lines.push(""); + } + } + + return `${lines.join("\n").trimEnd()}\n`; +} + +function escapeXml(value) { + return String(value) + .replaceAll("&", "&") + .replaceAll("<", "<") + .replaceAll(">", ">") + .replaceAll('"', """); +} + +function renderSvg(report) { + const held = report.aggregate.heldManuscripts; + const ready = report.aggregate.readyManuscripts; + const critical = report.aggregate.criticalFindings; + const major = report.aggregate.majorFindings; + const bars = [ + { label: "Held", value: held, color: "#b91c1c" }, + { label: "Ready", value: ready, color: "#047857" }, + { label: "Critical", value: critical, color: "#dc2626" }, + { label: "Major", value: major, color: "#d97706" } + ]; + const maxValue = Math.max(1, ...bars.map((bar) => bar.value)); + const rows = bars.map((bar, index) => { + const y = 170 + index * 70; + const width = Math.round((bar.value / maxValue) * 560); + return ` ${escapeXml(bar.label)} + + + ${bar.value}`; + }).join("\n"); + + return ` + Literature freshness review assistant summary + Summary of held manuscripts, ready manuscripts, and freshness findings. + + + + Literature freshness review assistant + Stale citations, missing newer evidence, and temporal drift before AI review release +${rows} + Audit digest: ${escapeXml(report.auditDigest.slice(0, 24))} + +`; +} + +module.exports = { + evaluateClaimFreshness, + evaluateManuscript, + evaluateFreshnessReview, + renderMarkdown, + renderSvg, + stableDigest +}; diff --git a/literature-freshness-review-assistant/package.json b/literature-freshness-review-assistant/package.json new file mode 100644 index 00000000..c867f4ce --- /dev/null +++ b/literature-freshness-review-assistant/package.json @@ -0,0 +1,13 @@ +{ + "name": "literature-freshness-review-assistant", + "version": "1.0.0", + "description": "Freshness-focused AI research assistant review slice for SCIBASE issue #16.", + "main": "index.js", + "scripts": { + "check": "node --check index.js && node --check sample-data.js && node --check test.js && node --check demo.js && node --check scripts/render-demo-video.js", + "test": "node test.js", + "demo": "node demo.js", + "demo:video": "node scripts/render-demo-video.js" + }, + "license": "MIT" +} diff --git a/literature-freshness-review-assistant/reports/demo.json b/literature-freshness-review-assistant/reports/demo.json new file mode 100644 index 00000000..917be5be --- /dev/null +++ b/literature-freshness-review-assistant/reports/demo.json @@ -0,0 +1,217 @@ +{ + "assistant": "literature-freshness-review-assistant", + "policy": { + "reviewDate": "2026-05-23", + "staleAfterYears": 4, + "contradictionSeverity": "critical", + "requiredEvidenceTypes": [ + "systematic_review", + "guideline", + "dataset_release", + "benchmark_update" + ], + "holdSeverityCutoff": "critical" + }, + "aggregate": { + "reviewedManuscripts": 2, + "heldManuscripts": 1, + "revisionManuscripts": 0, + "readyManuscripts": 1, + "criticalFindings": 1, + "majorFindings": 8, + "actionableFindings": 9 + }, + "manuscripts": [ + { + "packetId": "ms-hold-senolytic-review", + "title": "Senolytic cardiac safety manuscript", + "domain": "translational cardiometabolic biology", + "reviewDate": "2026-05-23", + "summary": { + "status": "hold_freshness_review", + "totalFindings": 9, + "actionableFindings": 9, + "criticalFindings": 1, + "majorFindings": 8, + "warningFindings": 0, + "readyFindings": 0 + }, + "findings": [ + { + "severity": "major", + "code": "STALE_SUPPORT_WINDOW", + "claimId": "claim-tolerability", + "topic": "senolytic cardiometabolic safety", + "detail": "Latest support is 2021-06-01, older than the 4-year freshness window.", + "requiredAction": "Add recent evidence or downgrade the claim wording.", + "evidenceSignalId": null + }, + { + "severity": "critical", + "code": "NEWER_CONTRADICTORY_EVIDENCE", + "claimId": "claim-tolerability", + "topic": "senolytic cardiometabolic safety", + "detail": "2025 systematic review of senolytic cardiometabolic safety (2025-11-18) contradicts older support but is not cited.", + "requiredAction": "Discuss the larger safety signal before claiming broad tolerability.", + "evidenceSignalId": "ev-sr-2025-senolytic-cardiometabolic" + }, + { + "severity": "major", + "code": "STALE_SUPPORT_WINDOW", + "claimId": "claim-organoid-methods", + "topic": "organoid assay reporting", + "detail": "Latest support is 2022-02-10, older than the 4-year freshness window.", + "requiredAction": "Add recent evidence or downgrade the claim wording.", + "evidenceSignalId": null + }, + { + "severity": "major", + "code": "MISSING_NEWER_EVIDENCE", + "claimId": "claim-organoid-methods", + "topic": "organoid assay reporting", + "detail": "SCIBASE organoid reporting guidance 2026.1 (2026-02-04) updates older support but is not cited.", + "requiredAction": "Use the 2026.1 organoid reporting checklist for reviewer packets.", + "evidenceSignalId": "ev-guide-2026-organoid-reporting" + }, + { + "severity": "major", + "code": "STALE_SUPPORT_WINDOW", + "claimId": "claim-atlas-normalization", + "topic": "single-cell atlas normalization", + "detail": "Latest support is 2021-12-12, older than the 4-year freshness window.", + "requiredAction": "Add recent evidence or downgrade the claim wording.", + "evidenceSignalId": null + }, + { + "severity": "major", + "code": "MISSING_NEWER_EVIDENCE", + "claimId": "claim-atlas-normalization", + "topic": "single-cell atlas normalization", + "detail": "Single Cell Atlas v4.0 release notes (2026-03-14) supersedes older support but is not cited.", + "requiredAction": "Reconcile claims trained on atlas v2.x or v3.x against v4.0 annotations.", + "evidenceSignalId": "ev-data-2026-single-cell-atlas-v4" + }, + { + "severity": "major", + "code": "DATASET_VERSION_DRIFT", + "claimId": "claim-atlas-normalization", + "topic": "single-cell atlas normalization", + "detail": "Claim uses dataset v2.1; current ledger version is v4.0.", + "requiredAction": "Reconcile claims trained on atlas v2.x or v3.x against v4.0 annotations.", + "evidenceSignalId": "ev-data-2026-single-cell-atlas-v4" + }, + { + "severity": "major", + "code": "MISSING_NEWER_EVIDENCE", + "claimId": "claim-protein-benchmark", + "topic": "protein structure benchmark", + "detail": "Protein structure benchmark leaderboard recalibration (2026-01-22) updates older support but is not cited.", + "requiredAction": "Refresh benchmark claims against the recalibrated 2026.01 leaderboard.", + "evidenceSignalId": "ev-bench-2026-protein-leaderboard" + }, + { + "severity": "major", + "code": "BENCHMARK_VERSION_DRIFT", + "claimId": "claim-protein-benchmark", + "topic": "protein structure benchmark", + "detail": "Claim uses benchmark 2024.10; current ledger version is 2026.01.", + "requiredAction": "Refresh benchmark claims against the recalibrated 2026.01 leaderboard.", + "evidenceSignalId": "ev-bench-2026-protein-leaderboard" + } + ], + "reviewerActions": [ + { + "claimId": "claim-tolerability", + "severity": "major", + "action": "Add recent evidence or downgrade the claim wording.", + "reason": "Latest support is 2021-06-01, older than the 4-year freshness window." + }, + { + "claimId": "claim-tolerability", + "severity": "critical", + "action": "Discuss the larger safety signal before claiming broad tolerability.", + "reason": "2025 systematic review of senolytic cardiometabolic safety (2025-11-18) contradicts older support but is not cited." + }, + { + "claimId": "claim-organoid-methods", + "severity": "major", + "action": "Add recent evidence or downgrade the claim wording.", + "reason": "Latest support is 2022-02-10, older than the 4-year freshness window." + }, + { + "claimId": "claim-organoid-methods", + "severity": "major", + "action": "Use the 2026.1 organoid reporting checklist for reviewer packets.", + "reason": "SCIBASE organoid reporting guidance 2026.1 (2026-02-04) updates older support but is not cited." + }, + { + "claimId": "claim-atlas-normalization", + "severity": "major", + "action": "Add recent evidence or downgrade the claim wording.", + "reason": "Latest support is 2021-12-12, older than the 4-year freshness window." + }, + { + "claimId": "claim-atlas-normalization", + "severity": "major", + "action": "Reconcile claims trained on atlas v2.x or v3.x against v4.0 annotations.", + "reason": "Single Cell Atlas v4.0 release notes (2026-03-14) supersedes older support but is not cited." + }, + { + "claimId": "claim-atlas-normalization", + "severity": "major", + "action": "Reconcile claims trained on atlas v2.x or v3.x against v4.0 annotations.", + "reason": "Claim uses dataset v2.1; current ledger version is v4.0." + }, + { + "claimId": "claim-protein-benchmark", + "severity": "major", + "action": "Refresh benchmark claims against the recalibrated 2026.01 leaderboard.", + "reason": "Protein structure benchmark leaderboard recalibration (2026-01-22) updates older support but is not cited." + }, + { + "claimId": "claim-protein-benchmark", + "severity": "major", + "action": "Refresh benchmark claims against the recalibrated 2026.01 leaderboard.", + "reason": "Claim uses benchmark 2024.10; current ledger version is 2026.01." + } + ], + "researchGapPrompts": [ + "Freshness gap: update the evidence base for senolytic cardiometabolic safety before treating the claim as current.", + "Freshness gap: update the evidence base for organoid assay reporting before treating the claim as current.", + "Freshness gap: update the evidence base for single-cell atlas normalization before treating the claim as current.", + "Freshness gap: update the evidence base for protein structure benchmark before treating the claim as current." + ], + "auditDigest": "2b28dfa1b6fc17166e45aa06df339015e0dec9da568f3a185015dab564cad637" + }, + { + "packetId": "ms-ready-telemetry-review", + "title": "Telemetry implant arrhythmia replication note", + "domain": "clinical devices", + "reviewDate": "2026-05-23", + "summary": { + "status": "ready_for_review", + "totalFindings": 1, + "actionableFindings": 0, + "criticalFindings": 0, + "majorFindings": 0, + "warningFindings": 0, + "readyFindings": 1 + }, + "findings": [ + { + "severity": "info", + "code": "FRESHNESS_READY", + "claimId": "claim-telemetry-risk", + "topic": "telemetry implant arrhythmia", + "detail": "Claim cites the current evidence signal for its topic.", + "requiredAction": "No freshness hold required.", + "evidenceSignalId": null + } + ], + "reviewerActions": [], + "researchGapPrompts": [], + "auditDigest": "30b9621c84e89262d698c368e40fcd6fd32bde83bb05cf9be0e70ad5823c4287" + } + ], + "auditDigest": "45a5a7683b859d96101b72cc8442e889cd630ad2736fcebe4dd6b85302cd0cd8" +} diff --git a/literature-freshness-review-assistant/reports/demo.md b/literature-freshness-review-assistant/reports/demo.md new file mode 100644 index 00000000..3e2ac752 --- /dev/null +++ b/literature-freshness-review-assistant/reports/demo.md @@ -0,0 +1,46 @@ +# Literature Freshness Review Assistant + +Review date: 2026-05-23 +Overall digest: 45a5a7683b859d96101b72cc8442e889cd630ad2736fcebe4dd6b85302cd0cd8 + +## Aggregate + +- Reviewed manuscripts: 2 +- Held manuscripts: 1 +- Revision manuscripts: 0 +- Ready manuscripts: 1 +- Critical findings: 1 +- Major findings: 8 +- Actionable findings: 9 + +## Senolytic cardiac safety manuscript + +Status: hold_freshness_review +Digest: 2b28dfa1b6fc17166e45aa06df339015e0dec9da568f3a185015dab564cad637 + +| Severity | Code | Claim | Detail | Required action | +| --- | --- | --- | --- | --- | +| major | STALE_SUPPORT_WINDOW | claim-tolerability | Latest support is 2021-06-01, older than the 4-year freshness window. | Add recent evidence or downgrade the claim wording. | +| critical | NEWER_CONTRADICTORY_EVIDENCE | claim-tolerability | 2025 systematic review of senolytic cardiometabolic safety (2025-11-18) contradicts older support but is not cited. | Discuss the larger safety signal before claiming broad tolerability. | +| major | STALE_SUPPORT_WINDOW | claim-organoid-methods | Latest support is 2022-02-10, older than the 4-year freshness window. | Add recent evidence or downgrade the claim wording. | +| major | MISSING_NEWER_EVIDENCE | claim-organoid-methods | SCIBASE organoid reporting guidance 2026.1 (2026-02-04) updates older support but is not cited. | Use the 2026.1 organoid reporting checklist for reviewer packets. | +| major | STALE_SUPPORT_WINDOW | claim-atlas-normalization | Latest support is 2021-12-12, older than the 4-year freshness window. | Add recent evidence or downgrade the claim wording. | +| major | MISSING_NEWER_EVIDENCE | claim-atlas-normalization | Single Cell Atlas v4.0 release notes (2026-03-14) supersedes older support but is not cited. | Reconcile claims trained on atlas v2.x or v3.x against v4.0 annotations. | +| major | DATASET_VERSION_DRIFT | claim-atlas-normalization | Claim uses dataset v2.1; current ledger version is v4.0. | Reconcile claims trained on atlas v2.x or v3.x against v4.0 annotations. | +| major | MISSING_NEWER_EVIDENCE | claim-protein-benchmark | Protein structure benchmark leaderboard recalibration (2026-01-22) updates older support but is not cited. | Refresh benchmark claims against the recalibrated 2026.01 leaderboard. | +| major | BENCHMARK_VERSION_DRIFT | claim-protein-benchmark | Claim uses benchmark 2024.10; current ledger version is 2026.01. | Refresh benchmark claims against the recalibrated 2026.01 leaderboard. | + +Research gap prompts: +- Freshness gap: update the evidence base for senolytic cardiometabolic safety before treating the claim as current. +- Freshness gap: update the evidence base for organoid assay reporting before treating the claim as current. +- Freshness gap: update the evidence base for single-cell atlas normalization before treating the claim as current. +- Freshness gap: update the evidence base for protein structure benchmark before treating the claim as current. + +## Telemetry implant arrhythmia replication note + +Status: ready_for_review +Digest: 30b9621c84e89262d698c368e40fcd6fd32bde83bb05cf9be0e70ad5823c4287 + +| Severity | Code | Claim | Detail | Required action | +| --- | --- | --- | --- | --- | +| info | FRESHNESS_READY | claim-telemetry-risk | Claim cites the current evidence signal for its topic. | No freshness hold required. | diff --git a/literature-freshness-review-assistant/reports/demo.mp4 b/literature-freshness-review-assistant/reports/demo.mp4 new file mode 100644 index 00000000..31227d38 Binary files /dev/null and b/literature-freshness-review-assistant/reports/demo.mp4 differ diff --git a/literature-freshness-review-assistant/reports/demo.svg b/literature-freshness-review-assistant/reports/demo.svg new file mode 100644 index 00000000..5081e22b --- /dev/null +++ b/literature-freshness-review-assistant/reports/demo.svg @@ -0,0 +1,34 @@ + + Literature freshness review assistant summary + Summary of held manuscripts, ready manuscripts, and freshness findings. + + + + Literature freshness review assistant + Stale citations, missing newer evidence, and temporal drift before AI review release + Held + + + 1 + Ready + + + 1 + Critical + + + 1 + Major + + + 8 + Audit digest: 45a5a7683b859d96101b72cc + diff --git a/literature-freshness-review-assistant/sample-data.js b/literature-freshness-review-assistant/sample-data.js new file mode 100644 index 00000000..1506269c --- /dev/null +++ b/literature-freshness-review-assistant/sample-data.js @@ -0,0 +1,144 @@ +const freshnessPolicy = { + reviewDate: "2026-05-23", + staleAfterYears: 4, + contradictionSeverity: "critical", + requiredEvidenceTypes: [ + "systematic_review", + "guideline", + "dataset_release", + "benchmark_update" + ], + holdSeverityCutoff: "critical" +}; + +const evidenceLedger = { + signals: [ + { + id: "ev-sr-2025-senolytic-cardiometabolic", + topic: "senolytic cardiometabolic safety", + evidenceType: "systematic_review", + title: "2025 systematic review of senolytic cardiometabolic safety", + published: "2025-11-18", + relation: "contradicts", + requirement: "Discuss the larger safety signal before claiming broad tolerability.", + minCitationDate: "2025-11-18" + }, + { + id: "ev-guide-2026-organoid-reporting", + topic: "organoid assay reporting", + evidenceType: "guideline", + title: "SCIBASE organoid reporting guidance 2026.1", + published: "2026-02-04", + relation: "updates", + requirement: "Use the 2026.1 organoid reporting checklist for reviewer packets.", + minCitationDate: "2026-02-04" + }, + { + id: "ev-data-2026-single-cell-atlas-v4", + topic: "single-cell atlas normalization", + evidenceType: "dataset_release", + title: "Single Cell Atlas v4.0 release notes", + published: "2026-03-14", + relation: "supersedes", + currentVersion: "v4.0", + requirement: "Reconcile claims trained on atlas v2.x or v3.x against v4.0 annotations.", + minCitationDate: "2026-03-14" + }, + { + id: "ev-bench-2026-protein-leaderboard", + topic: "protein structure benchmark", + evidenceType: "benchmark_update", + title: "Protein structure benchmark leaderboard recalibration", + published: "2026-01-22", + relation: "updates", + currentVersion: "2026.01", + requirement: "Refresh benchmark claims against the recalibrated 2026.01 leaderboard.", + minCitationDate: "2026-01-22" + }, + { + id: "ev-sr-2024-telemetry-implant", + topic: "telemetry implant arrhythmia", + evidenceType: "systematic_review", + title: "2024 telemetry implant arrhythmia meta-analysis", + published: "2024-09-20", + relation: "supports", + requirement: "Current support is acceptable if cited with the 2024 meta-analysis.", + minCitationDate: "2024-09-20" + } + ] +}; + +const manuscriptPackets = [ + { + id: "ms-hold-senolytic-review", + title: "Senolytic cardiac safety manuscript", + domain: "translational cardiometabolic biology", + claims: [ + { + id: "claim-tolerability", + text: "The senolytic cocktail is broadly well tolerated across cardiometabolic cohorts.", + topic: "senolytic cardiometabolic safety", + supportCitationIds: ["trial-2019-small", "review-2021-safety"], + citationYears: [2019, 2021], + citationDates: ["2019-08-15", "2021-06-01"], + citedCurrentSignalIds: [], + tone: "broad" + }, + { + id: "claim-organoid-methods", + text: "The organoid assay follows current field reporting guidance.", + topic: "organoid assay reporting", + supportCitationIds: ["organoid-checklist-2022"], + citationYears: [2022], + citationDates: ["2022-02-10"], + citedCurrentSignalIds: [], + tone: "current" + }, + { + id: "claim-atlas-normalization", + text: "The single-cell normalization pipeline reflects current atlas annotations.", + topic: "single-cell atlas normalization", + supportCitationIds: ["atlas-v2-release"], + citationYears: [2021], + citationDates: ["2021-12-12"], + citedCurrentSignalIds: [], + datasetVersion: "v2.1", + tone: "current" + }, + { + id: "claim-protein-benchmark", + text: "The model remains state-of-the-art on the protein structure benchmark.", + topic: "protein structure benchmark", + supportCitationIds: ["leaderboard-2024-10"], + citationYears: [2024], + citationDates: ["2024-10-05"], + citedCurrentSignalIds: [], + benchmarkVersion: "2024.10", + tone: "state-of-the-art" + } + ] + }, + { + id: "ms-ready-telemetry-review", + title: "Telemetry implant arrhythmia replication note", + domain: "clinical devices", + claims: [ + { + id: "claim-telemetry-risk", + text: "Telemetry implant arrhythmia risk is consistent with the latest meta-analysis.", + topic: "telemetry implant arrhythmia", + supportCitationIds: ["telemetry-meta-2024"], + citationYears: [2024], + citationDates: ["2024-09-20"], + citedCurrentSignalIds: ["ev-sr-2024-telemetry-implant"], + tone: "calibrated" + } + ] + } +]; + +module.exports = { + freshnessPolicy, + evidenceLedger, + manuscriptPackets +}; diff --git a/literature-freshness-review-assistant/scripts/render-demo-video.js b/literature-freshness-review-assistant/scripts/render-demo-video.js new file mode 100644 index 00000000..c53297c4 --- /dev/null +++ b/literature-freshness-review-assistant/scripts/render-demo-video.js @@ -0,0 +1,89 @@ +const fs = require("fs"); +const os = require("os"); +const path = require("path"); +const { spawnSync } = require("child_process"); + +const { + evaluateFreshnessReview +} = require("../index"); +const { + freshnessPolicy, + evidenceLedger, + manuscriptPackets +} = require("../sample-data"); + +const moduleDir = path.join(__dirname, ".."); +const reportsDir = path.join(moduleDir, "reports"); +fs.mkdirSync(reportsDir, { recursive: true }); + +function candidateFonts() { + return [ + "C:\\Windows\\Fonts\\arial.ttf", + "C:\\Windows\\Fonts\\segoeui.ttf", + "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", + "/usr/share/fonts/truetype/liberation2/LiberationSans-Regular.ttf", + "/System/Library/Fonts/Supplemental/Arial.ttf", + "/Library/Fonts/Arial.ttf" + ]; +} + +function resolveFontFile() { + const font = candidateFonts().find((candidate) => fs.existsSync(candidate)); + if (!font) { + throw new Error("No supported TrueType font file found for ffmpeg drawtext rendering."); + } + return font; +} + +function escapeDrawtext(value) { + return String(value) + .replaceAll("\\", "\\\\") + .replaceAll(":", "\\:") + .replaceAll("'", "\\'") + .replaceAll(",", "\\,"); +} + +function drawText(fontFile, text, x, y, size, color) { + return `drawtext=fontfile='${escapeDrawtext(fontFile)}':text='${escapeDrawtext(text)}':x=${x}:y=${y}:fontsize=${size}:fontcolor=${color}`; +} + +const ffmpegPath = process.env.FFMPEG_PATH || "ffmpeg"; +const fontFile = resolveFontFile(); +const report = evaluateFreshnessReview(manuscriptPackets, evidenceLedger, freshnessPolicy); +const output = path.join(reportsDir, "demo.mp4"); +const filter = [ + "color=c=0xf8fafc:s=1280x720:d=5", + drawText(fontFile, "Literature Freshness Review Assistant", 70, 70, 42, "0x0f172a"), + drawText(fontFile, `Status: ${report.manuscripts[0].summary.status}`, 70, 145, 32, "0xb91c1c"), + drawText(fontFile, `Reviewed manuscripts: ${report.aggregate.reviewedManuscripts}`, 70, 220, 28, "0x0f172a"), + drawText(fontFile, `Held: ${report.aggregate.heldManuscripts} Ready: ${report.aggregate.readyManuscripts}`, 70, 270, 28, "0x0f172a"), + drawText(fontFile, `Critical freshness findings: ${report.aggregate.criticalFindings}`, 70, 340, 30, "0xdc2626"), + drawText(fontFile, `Major freshness findings: ${report.aggregate.majorFindings}`, 70, 390, 30, "0xd97706"), + drawText(fontFile, "Checks stale support, missing newer evidence, dataset drift, and benchmark drift.", 70, 475, 25, "0x334155"), + drawText(fontFile, `Audit ${report.auditDigest.slice(0, 24)}`, 70, 635, 22, "0x475569") +].join(","); + +const args = [ + "-y", + "-f", "lavfi", + "-i", filter, + "-t", "5", + "-pix_fmt", "yuv420p", + "-c:v", "libx264", + "-movflags", "+faststart", + output +]; + +const result = spawnSync(ffmpegPath, args, { + encoding: "utf8", + stdio: "pipe", + windowsHide: true +}); + +if (result.status !== 0) { + process.stderr.write(result.stderr || result.stdout); + throw new Error(`ffmpeg failed with status ${result.status}`); +} + +const stats = fs.statSync(output); +console.log(`wrote ${path.relative(process.cwd(), output)} (${stats.size} bytes) using ${path.basename(ffmpegPath)} on ${os.platform()}`); diff --git a/literature-freshness-review-assistant/test.js b/literature-freshness-review-assistant/test.js new file mode 100644 index 00000000..05d879bc --- /dev/null +++ b/literature-freshness-review-assistant/test.js @@ -0,0 +1,47 @@ +const assert = require("assert"); + +const { + evaluateFreshnessReview, + renderMarkdown, + renderSvg +} = require("./index"); +const { + freshnessPolicy, + evidenceLedger, + manuscriptPackets +} = require("./sample-data"); + +const report = evaluateFreshnessReview(manuscriptPackets, evidenceLedger, freshnessPolicy); + +assert.strictEqual(report.aggregate.reviewedManuscripts, 2); +assert.strictEqual(report.aggregate.heldManuscripts, 1); +assert.strictEqual(report.aggregate.readyManuscripts, 1); +assert.strictEqual(report.aggregate.criticalFindings, 1); +assert.ok(report.aggregate.majorFindings >= 4); + +const held = report.manuscripts.find((entry) => entry.packetId === "ms-hold-senolytic-review"); +assert.ok(held); +assert.strictEqual(held.summary.status, "hold_freshness_review"); +assert.ok(held.findings.some((finding) => finding.code === "NEWER_CONTRADICTORY_EVIDENCE")); +assert.ok(held.findings.some((finding) => finding.code === "MISSING_NEWER_EVIDENCE")); +assert.ok(held.findings.some((finding) => finding.code === "DATASET_VERSION_DRIFT")); +assert.ok(held.findings.some((finding) => finding.code === "BENCHMARK_VERSION_DRIFT")); +assert.ok(held.reviewerActions.length >= 4); + +const ready = report.manuscripts.find((entry) => entry.packetId === "ms-ready-telemetry-review"); +assert.ok(ready); +assert.strictEqual(ready.summary.status, "ready_for_review"); +assert.deepStrictEqual(ready.findings.map((finding) => finding.code), ["FRESHNESS_READY"]); + +const secondPass = evaluateFreshnessReview(manuscriptPackets, evidenceLedger, freshnessPolicy); +assert.strictEqual(secondPass.auditDigest, report.auditDigest); + +const markdown = renderMarkdown(report); +assert.ok(markdown.includes("Literature Freshness Review Assistant")); +assert.ok(markdown.includes("NEWER_CONTRADICTORY_EVIDENCE")); + +const svg = renderSvg(report); +assert.ok(svg.includes("