From 7d618fc386183a95f687b43b0229df38c288dc88 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BB=84=E9=93=AD=E9=94=8B?= Date: Fri, 15 May 2026 19:51:44 +0800 Subject: [PATCH] Add AI research redline packet --- ai-research-redline-packet/README.md | 69 ++++ .../data/sample-manuscript.json | 168 ++++++++++ ai-research-redline-packet/docs/demo.mp4 | Bin 0 -> 48686 bytes ai-research-redline-packet/docs/demo.svg | 55 ++++ .../docs/requirement-map.md | 40 +++ ai-research-redline-packet/package.json | 15 + ai-research-redline-packet/scripts/demo.js | 17 + .../src/redline-packet.js | 307 ++++++++++++++++++ .../test/redline-packet.test.js | 85 +++++ 9 files changed, 756 insertions(+) create mode 100644 ai-research-redline-packet/README.md create mode 100644 ai-research-redline-packet/data/sample-manuscript.json create mode 100644 ai-research-redline-packet/docs/demo.mp4 create mode 100644 ai-research-redline-packet/docs/demo.svg create mode 100644 ai-research-redline-packet/docs/requirement-map.md create mode 100644 ai-research-redline-packet/package.json create mode 100644 ai-research-redline-packet/scripts/demo.js create mode 100644 ai-research-redline-packet/src/redline-packet.js create mode 100644 ai-research-redline-packet/test/redline-packet.test.js diff --git a/ai-research-redline-packet/README.md b/ai-research-redline-packet/README.md new file mode 100644 index 0000000..674c968 --- /dev/null +++ b/ai-research-redline-packet/README.md @@ -0,0 +1,69 @@ +# AI Research Redline Packet + +This module adds a focused AI-assisted research-tool slice for SCIBASE. It does +not try to be another broad paper summarizer or generic citation formatter. +Instead, it produces an evidence-backed pre-submission redline packet that tells +researchers what must be fixed before sharing a manuscript. + +The workflow is deterministic and dependency-free, making it suitable for local +review checks, future API handlers, or reviewer-facing demo artifacts. + +## What It Covers + +- Generates abstract, executive, and layperson summary deltas from a structured + manuscript sample. +- Maps manuscript claims to evidence spans and flags unsupported claims. +- Detects statistical redlines such as missing confidence intervals and + inconsistent p-value language. +- Checks compliance items such as ethics statements and data availability. +- Ranks citation gap recommendations with transparent reasons. +- Routes issues to reviewer templates and creates insertion-ready revision + tasks. +- Includes tests, synthetic sample data, a CLI demo, requirement mapping, and a + short demo video. + +## Quick Start + +```bash +npm run check +npm test +npm run demo +``` + +Expected demo summary: + +```text +Packet decision: revise-before-submit +Redlines: 5 +Revision tasks: 5 +Citation recommendations: 3 +``` + +## Repository Layout + +```text +ai-research-redline-packet/ + data/sample-manuscript.json + docs/demo.svg + docs/demo.mp4 + docs/requirement-map.md + scripts/demo.js + src/redline-packet.js + test/redline-packet.test.js +``` + +## Design Notes + +The redline packet sits between AI summarization, AI peer review, and AI +citation assistance: + +1. A manuscript is parsed into claims, evidence spans, results, compliance + statements, and citations. +2. Summary modes are generated from the same evidence inventory. +3. Unsupported claims, statistical issues, compliance gaps, and citation gaps + become reviewer-facing redlines. +4. Redlines are converted into concrete revision tasks with insertion targets. +5. The final packet has an audit digest so reviewers can reproduce the decision. + +This gives researchers an actionable pre-review artifact rather than a loose AI +chat transcript. diff --git a/ai-research-redline-packet/data/sample-manuscript.json b/ai-research-redline-packet/data/sample-manuscript.json new file mode 100644 index 0000000..0d72b27 --- /dev/null +++ b/ai-research-redline-packet/data/sample-manuscript.json @@ -0,0 +1,168 @@ +{ + "schemaVersion": "ai-redline-packet.v1", + "manuscript": { + "id": "ms-neuro-organoid-response", + "title": "Transcriptomic Response of Neural Organoids to Microfluidic Oxygen Gradients", + "domain": "biology", + "targetJournal": "Nature Biomedical Engineering", + "stage": "pre-submission", + "sections": [ + { + "id": "abstract", + "heading": "Abstract", + "text": "We report a microfluidic oxygen-gradient platform for neural organoids. Treated organoids showed increased maturation markers and improved viability after seven days." + }, + { + "id": "results", + "heading": "Results", + "text": "Marker GFAP increased by 22 percent after treatment. Organoids had significantly better viability with p = 0.08. The platform eliminates batch effects across all donor lines." + }, + { + "id": "methods", + "heading": "Methods", + "text": "Organoids from three donor lines were cultured in a controlled oxygen gradient. Analysis code and raw count tables were deposited with the project repository." + }, + { + "id": "ethics", + "heading": "Ethics", + "text": "Human induced pluripotent stem cell lines were obtained from a certified biobank." + } + ], + "claims": [ + { + "id": "claim-marker-maturation", + "text": "GFAP expression increased by 22 percent after seven days of treatment.", + "sectionId": "results", + "evidenceSpanIds": ["span-gfap-table"], + "citationIds": ["smith-2024-organoid-maturation"], + "importance": "key-finding" + }, + { + "id": "claim-viability-significance", + "text": "Treated organoids had significantly better viability.", + "sectionId": "results", + "evidenceSpanIds": ["span-viability-pvalue"], + "citationIds": [], + "importance": "key-finding" + }, + { + "id": "claim-eliminates-batch-effects", + "text": "The platform eliminates batch effects across all donor lines.", + "sectionId": "results", + "evidenceSpanIds": [], + "citationIds": [], + "importance": "broad-claim" + }, + { + "id": "claim-data-deposited", + "text": "Analysis code and raw count tables were deposited with the project repository.", + "sectionId": "methods", + "evidenceSpanIds": ["span-repository-link"], + "citationIds": ["project-repository-v2"], + "importance": "reproducibility" + } + ], + "evidenceSpans": [ + { + "id": "span-gfap-table", + "sectionId": "results", + "quote": "Marker GFAP increased by 22 percent after treatment.", + "artifact": "results/gfap-differential-expression.csv", + "confidence": 0.91 + }, + { + "id": "span-viability-pvalue", + "sectionId": "results", + "quote": "Organoids had significantly better viability with p = 0.08.", + "artifact": "results/viability-analysis.json", + "confidence": 0.72 + }, + { + "id": "span-repository-link", + "sectionId": "methods", + "quote": "Analysis code and raw count tables were deposited with the project repository.", + "artifact": "metadata/repository-export.json", + "confidence": 0.86 + } + ], + "statisticalChecks": [ + { + "id": "stat-viability-pvalue", + "claimId": "claim-viability-significance", + "metric": "viability", + "pValue": 0.08, + "claimsSignificant": true, + "confidenceInterval": null, + "sampleSize": 3 + }, + { + "id": "stat-gfap-effect", + "claimId": "claim-marker-maturation", + "metric": "GFAP expression", + "pValue": 0.012, + "claimsSignificant": true, + "confidenceInterval": [0.14, 0.31], + "sampleSize": 3 + } + ], + "compliance": { + "ethicsStatementPresent": true, + "irbProtocolId": null, + "dataAvailabilityStatementPresent": true, + "codeAvailabilityStatementPresent": true, + "conflictOfInterestPresent": false, + "fundingStatementPresent": true + }, + "citations": [ + { + "id": "smith-2024-organoid-maturation", + "title": "Organoid maturation markers under controlled oxygen gradients", + "year": 2024, + "style": "Nature", + "supportsClaimIds": ["claim-marker-maturation"] + }, + { + "id": "project-repository-v2", + "title": "SCIBASE project repository export preprint-v2", + "year": 2026, + "style": "DataCite", + "supportsClaimIds": ["claim-data-deposited"] + } + ], + "candidateCitations": [ + { + "id": "lee-2025-donor-batch-effects", + "title": "Donor-line batch effects in neural organoid transcriptomics", + "year": 2025, + "reason": "Needed before making a broad batch-effect claim.", + "claimIds": ["claim-eliminates-batch-effects"] + }, + { + "id": "nguyen-2024-viability-statistics", + "title": "Statistical reporting standards for organoid viability assays", + "year": 2024, + "reason": "Supports corrected viability reporting and confidence intervals.", + "claimIds": ["claim-viability-significance"] + }, + { + "id": "icmje-2025-ethics-reporting", + "title": "Research ethics and disclosure reporting guidance", + "year": 2025, + "reason": "Helps complete ethics protocol and disclosure statements.", + "claimIds": [] + } + ] + }, + "reviewTemplates": [ + { + "id": "biology-pre-submission", + "name": "Biology pre-submission review", + "requiredTopics": ["evidence", "statistics", "ethics", "data-availability", "citations"] + }, + { + "id": "statistical-methods", + "name": "Statistical methods review", + "requiredTopics": ["statistics", "sample-size", "effect-size"] + } + ] +} diff --git a/ai-research-redline-packet/docs/demo.mp4 b/ai-research-redline-packet/docs/demo.mp4 new file mode 100644 index 0000000000000000000000000000000000000000..63a943b6226857ae48df5ffed5a9c6da8a3f1f17 GIT binary patch literal 48686 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

n?Sp$ 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 + AI research redline packet demo + Dashboard showing AI-assisted manuscript redlines, citation recommendations, and revision tasks. + + + AI Research Redline Packet + Pre-submission action packet for organoid manuscript review + + REVISE BEFORE SUBMIT + + + Redlines + 5 + issues found + + + Blockers + 2 + must fix first + + + Revision tasks + 5 + ready to insert + + + Citation recs + 3 + ranked gaps + + + Reviewer redlines + + + + Unsupported broad claim + Batch-effect elimination claim has no linked evidence span. + + blocker + + + p-value language + Viability described as significant with p = 0.08. + + blocker + + + Citation gap + Recommend donor-line batch effects and viability reporting sources. + + ranked + + + Revision packet: 5 insertion-ready tasks routed to biology pre-submission and statistical-methods templates. + diff --git a/ai-research-redline-packet/docs/requirement-map.md b/ai-research-redline-packet/docs/requirement-map.md new file mode 100644 index 0000000..c44b36f --- /dev/null +++ b/ai-research-redline-packet/docs/requirement-map.md @@ -0,0 +1,40 @@ +# Requirement Map + +Issue: SCIBASE-AI/SCIBASE.AI#13, AI-Assisted Research Tools (MVP Level). + +## AI Paper Summarizer + +The packet emits abstract, executive, and layperson summary modes from the same +structured manuscript evidence. It also reports summary deltas such as how many +key findings, reproducibility claims, and broad claims require review. + +## AI Peer Review Aid + +The redline engine flags unsupported claims, statistical reporting issues, +missing confidence intervals, ethics protocol gaps, and missing disclosure +statements. Each redline carries severity, insertion target, and evidence digest +so reviewers can audit why it was raised. + +## AI Citation Tool + +Citation recommendations are ranked against the redlines they would help fix. +The output includes transparent reasons and claim ids, making citations +insertion-ready rather than opaque suggestions. + +## Review Templates + +The packet routes redlines to domain-specific reviewer templates such as biology +pre-submission review and statistical methods review. This maps AI findings into +review workflows that institutions can standardize. + +## Actionable Output + +Every redline becomes a revision task with an insertion target and action text. +This supports one-click manuscript editing or PR-like review queues in a future +SCIBASE interface. + +## Scope Boundary + +This is not another generic summarizer or chatbot transcript. It is a +deterministic pre-submission redline packet that ties summaries, peer-review +diagnostics, citation recommendations, and revision tasks together. diff --git a/ai-research-redline-packet/package.json b/ai-research-redline-packet/package.json new file mode 100644 index 0000000..0d36f34 --- /dev/null +++ b/ai-research-redline-packet/package.json @@ -0,0 +1,15 @@ +{ + "name": "ai-research-redline-packet", + "version": "0.1.0", + "description": "Dependency-free evidence redline packet for AI-assisted research review workflows.", + "private": true, + "scripts": { + "check": "node scripts/demo.js --json > /dev/null", + "demo": "node scripts/demo.js", + "test": "node --test test/*.test.js" + }, + "engines": { + "node": ">=18" + }, + "license": "MIT" +} diff --git a/ai-research-redline-packet/scripts/demo.js b/ai-research-redline-packet/scripts/demo.js new file mode 100644 index 0000000..3acfe21 --- /dev/null +++ b/ai-research-redline-packet/scripts/demo.js @@ -0,0 +1,17 @@ +const fs = require("node:fs"); +const path = require("node:path"); +const { createRedlinePacket, formatPacketReport } = require("../src/redline-packet"); + +const manifestPath = path.join(__dirname, "..", "data", "sample-manuscript.json"); +const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8")); +const packet = createRedlinePacket(manifest); + +if (process.argv.includes("--json")) { + process.stdout.write(`${JSON.stringify(packet, null, 2)}\n`); +} else { + process.stdout.write(`${formatPacketReport(packet)}\n`); +} + +if (packet.decision === "invalid-manifest") { + process.exitCode = 1; +} diff --git a/ai-research-redline-packet/src/redline-packet.js b/ai-research-redline-packet/src/redline-packet.js new file mode 100644 index 0000000..a73eaf1 --- /dev/null +++ b/ai-research-redline-packet/src/redline-packet.js @@ -0,0 +1,307 @@ +const crypto = require("node:crypto"); + +function stableStringify(value) { + if (Array.isArray(value)) { + return `[${value.map((entry) => stableStringify(entry)).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 digest(value) { + return `sha256:${crypto.createHash("sha256").update(stableStringify(value)).digest("hex")}`; +} + +function validateManifest(manifest) { + const errors = []; + if (manifest.schemaVersion !== "ai-redline-packet.v1") { + errors.push("schemaVersion must be ai-redline-packet.v1"); + } + if (!manifest.manuscript?.id) { + errors.push("manuscript.id is required"); + } + if (!Array.isArray(manifest.manuscript?.claims)) { + errors.push("manuscript.claims must be an array"); + } + if (!Array.isArray(manifest.manuscript?.evidenceSpans)) { + errors.push("manuscript.evidenceSpans must be an array"); + } + for (const claim of manifest.manuscript?.claims || []) { + if (!claim.id || !claim.text || !claim.sectionId) { + errors.push("every claim requires id, text, and sectionId"); + } + } + for (const span of manifest.manuscript?.evidenceSpans || []) { + if (!span.id || !span.quote || !span.artifact) { + errors.push("every evidence span requires id, quote, and artifact"); + } + } + return errors; +} + +function indexById(items) { + return new Map(items.map((item) => [item.id, item])); +} + +function summarize(manuscript) { + const keyClaims = manuscript.claims.filter((claim) => claim.importance === "key-finding"); + const reproducibilityClaims = manuscript.claims.filter((claim) => claim.importance === "reproducibility"); + const broadClaims = manuscript.claims.filter((claim) => claim.importance === "broad-claim"); + + return { + abstract: `${manuscript.title}: ${keyClaims.map((claim) => claim.text).join(" ")}`, + executive: [ + `${keyClaims.length} key finding(s) are ready for evidence review.`, + `${reproducibilityClaims.length} reproducibility claim(s) mention project repository evidence.`, + `${broadClaims.length} broad claim(s) need tighter support before submission.` + ].join(" "), + layperson: "The study tests whether a controlled oxygen environment changes neural organoid health, but some claims need clearer evidence and reporting before submission." + }; +} + +function detectEvidenceRedlines(manuscript) { + const evidenceIndex = indexById(manuscript.evidenceSpans); + return manuscript.claims + .filter((claim) => claim.evidenceSpanIds.length === 0) + .map((claim) => ({ + id: `evidence-${claim.id}`, + type: "evidence", + severity: claim.importance === "broad-claim" ? "blocker" : "major", + claimId: claim.id, + sectionId: claim.sectionId, + message: "Claim has no linked evidence span.", + insertionTarget: claim.sectionId, + evidenceDigest: digest({ claim: claim.text, evidence: [] }) + })) + .concat( + manuscript.claims + .filter((claim) => claim.evidenceSpanIds.some((id) => !evidenceIndex.has(id))) + .map((claim) => ({ + id: `missing-evidence-${claim.id}`, + type: "evidence", + severity: "major", + claimId: claim.id, + sectionId: claim.sectionId, + message: "Claim references an evidence span that is not present in the manuscript packet.", + insertionTarget: claim.sectionId, + evidenceDigest: digest({ claim: claim.text, evidenceSpanIds: claim.evidenceSpanIds }) + })) + ); +} + +function detectStatisticalRedlines(manuscript) { + const redlines = []; + for (const check of manuscript.statisticalChecks || []) { + if (check.claimsSignificant && check.pValue >= 0.05) { + redlines.push({ + id: `stat-significance-${check.id}`, + type: "statistics", + severity: "blocker", + claimId: check.claimId, + sectionId: manuscript.claims.find((claim) => claim.id === check.claimId)?.sectionId || "results", + message: `${check.metric} is described as significant but p=${check.pValue}.`, + insertionTarget: "results", + evidenceDigest: digest(check) + }); + } + if (!check.confidenceInterval) { + redlines.push({ + id: `stat-ci-${check.id}`, + type: "statistics", + severity: "major", + claimId: check.claimId, + sectionId: manuscript.claims.find((claim) => claim.id === check.claimId)?.sectionId || "results", + message: `${check.metric} is missing a confidence interval.`, + insertionTarget: "results", + evidenceDigest: digest(check) + }); + } + } + return redlines; +} + +function detectComplianceRedlines(manuscript) { + const items = []; + const compliance = manuscript.compliance || {}; + if (compliance.ethicsStatementPresent && !compliance.irbProtocolId) { + items.push({ + id: "compliance-irb-protocol", + type: "ethics", + severity: "major", + claimId: null, + sectionId: "ethics", + message: "Ethics statement is present but lacks an IRB/protocol identifier.", + insertionTarget: "ethics", + evidenceDigest: digest({ ethicsStatementPresent: true, irbProtocolId: null }) + }); + } + if (!compliance.conflictOfInterestPresent) { + items.push({ + id: "compliance-conflict-disclosure", + type: "compliance", + severity: "major", + claimId: null, + sectionId: "disclosures", + message: "Conflict-of-interest disclosure is missing.", + insertionTarget: "disclosures", + evidenceDigest: digest({ conflictOfInterestPresent: false }) + }); + } + return items; +} + +function rankCitationRecommendations(manuscript, redlines) { + const claimRedlineIds = new Set(redlines.map((redline) => redline.claimId).filter(Boolean)); + return manuscript.candidateCitations + .map((citation) => { + const matchingRedlineCount = citation.claimIds.filter((claimId) => claimRedlineIds.has(claimId)).length; + const score = matchingRedlineCount * 2 + (citation.year >= 2024 ? 1 : 0); + return { + id: citation.id, + title: citation.title, + year: citation.year, + score, + reason: citation.reason, + claimIds: citation.claimIds + }; + }) + .sort((a, b) => b.score - a.score || b.year - a.year); +} + +function routeReviewTemplates(manifest, redlines) { + const topicSet = new Set(redlines.map((redline) => redline.type)); + if (redlines.some((redline) => redline.type === "statistics")) { + topicSet.add("sample-size"); + topicSet.add("effect-size"); + } + if (redlines.some((redline) => redline.type === "compliance")) { + topicSet.add("data-availability"); + } + + return manifest.reviewTemplates.map((template) => { + const matchedTopics = template.requiredTopics.filter((topic) => topicSet.has(topic)); + return { + id: template.id, + name: template.name, + matchedTopics, + recommended: matchedTopics.length > 0 + }; + }); +} + +function buildRevisionTasks(redlines, citations) { + return redlines.map((redline, index) => { + const citation = citations.find((candidate) => candidate.claimIds.includes(redline.claimId)); + return { + id: `task-${String(index + 1).padStart(2, "0")}`, + severity: redline.severity, + insertionTarget: redline.insertionTarget, + redlineId: redline.id, + action: citation + ? `${redline.message} Add or discuss citation: ${citation.title}.` + : redline.message, + readyForInsert: true + }; + }); +} + +function createRedlinePacket(manifest) { + const validationErrors = validateManifest(manifest); + if (validationErrors.length > 0) { + return { + valid: false, + validationErrors, + decision: "invalid-manifest", + redlines: [], + revisionTasks: [], + citationRecommendations: [], + summary: { + redlines: 0, + revisionTasks: 0, + citationRecommendations: 0 + } + }; + } + + const manuscript = manifest.manuscript; + const redlines = [ + ...detectEvidenceRedlines(manuscript), + ...detectStatisticalRedlines(manuscript), + ...detectComplianceRedlines(manuscript) + ]; + const citationRecommendations = rankCitationRecommendations(manuscript, redlines); + const revisionTasks = buildRevisionTasks(redlines, citationRecommendations); + const templateRouting = routeReviewTemplates(manifest, redlines); + const decision = redlines.some((redline) => redline.severity === "blocker") + ? "revise-before-submit" + : redlines.length > 0 + ? "minor-revision" + : "ready"; + + const summary = { + manuscriptId: manuscript.id, + decision, + redlines: redlines.length, + blockers: redlines.filter((redline) => redline.severity === "blocker").length, + revisionTasks: revisionTasks.length, + citationRecommendations: citationRecommendations.length + }; + + return { + valid: true, + validationErrors: [], + decision, + summaries: summarize(manuscript), + redlines, + revisionTasks, + citationRecommendations, + templateRouting, + summary, + auditDigest: digest({ + summary, + redlines: redlines.map((redline) => ({ + id: redline.id, + severity: redline.severity, + evidenceDigest: redline.evidenceDigest + })) + }) + }; +} + +function formatPacketReport(packet) { + if (!packet.valid) { + return [`Manifest invalid:`, ...packet.validationErrors.map((error) => `- ${error}`)].join("\n"); + } + + const lines = [ + `Packet decision: ${packet.decision}`, + `Redlines: ${packet.summary.redlines}`, + `Blockers: ${packet.summary.blockers}`, + `Revision tasks: ${packet.summary.revisionTasks}`, + `Citation recommendations: ${packet.summary.citationRecommendations}`, + `Audit digest: ${packet.auditDigest}`, + "", + `Executive summary: ${packet.summaries.executive}`, + "" + ]; + + for (const redline of packet.redlines) { + lines.push(`${redline.severity.toUpperCase()} ${redline.id}`); + lines.push(` ${redline.message}`); + } + + return lines.join("\n"); +} + +module.exports = { + createRedlinePacket, + digest, + formatPacketReport, + stableStringify, + validateManifest +}; diff --git a/ai-research-redline-packet/test/redline-packet.test.js b/ai-research-redline-packet/test/redline-packet.test.js new file mode 100644 index 0000000..3f14fc5 --- /dev/null +++ b/ai-research-redline-packet/test/redline-packet.test.js @@ -0,0 +1,85 @@ +const assert = require("node:assert/strict"); +const fs = require("node:fs"); +const path = require("node:path"); +const test = require("node:test"); +const { + createRedlinePacket, + digest, + stableStringify, + validateManifest +} = require("../src/redline-packet"); + +const samplePath = path.join(__dirname, "..", "data", "sample-manuscript.json"); + +function loadSample() { + return JSON.parse(fs.readFileSync(samplePath, "utf8")); +} + +test("creates revise-before-submit packet with evidence, statistics, and compliance redlines", () => { + const packet = createRedlinePacket(loadSample()); + + assert.equal(packet.decision, "revise-before-submit"); + assert.equal(packet.summary.redlines, 5); + assert.equal(packet.summary.blockers, 2); + assert.equal(packet.summary.revisionTasks, 5); + assert.match(packet.auditDigest, /^sha256:[a-f0-9]{64}$/); +}); + +test("flags unsupported broad claims as blockers", () => { + const packet = createRedlinePacket(loadSample()); + const redline = packet.redlines.find((item) => item.id === "evidence-claim-eliminates-batch-effects"); + + assert.equal(redline.severity, "blocker"); + assert.equal(redline.type, "evidence"); + assert.match(redline.message, /no linked evidence/); +}); + +test("detects inconsistent p-value significance language and missing confidence interval", () => { + const packet = createRedlinePacket(loadSample()); + const ids = packet.redlines.map((redline) => redline.id); + + assert.equal(ids.includes("stat-significance-stat-viability-pvalue"), true); + assert.equal(ids.includes("stat-ci-stat-viability-pvalue"), true); +}); + +test("ranks citation recommendations based on claim redlines", () => { + const packet = createRedlinePacket(loadSample()); + + assert.equal(packet.citationRecommendations[0].id, "lee-2025-donor-batch-effects"); + assert.equal(packet.citationRecommendations[1].id, "nguyen-2024-viability-statistics"); +}); + +test("builds insertion-ready revision tasks", () => { + const packet = createRedlinePacket(loadSample()); + + assert.equal(packet.revisionTasks.every((task) => task.readyForInsert), true); + assert.equal( + packet.revisionTasks.some((task) => task.action.includes("Donor-line batch effects")), + true + ); +}); + +test("routes redlines to relevant reviewer templates", () => { + const packet = createRedlinePacket(loadSample()); + const stats = packet.templateRouting.find((template) => template.id === "statistical-methods"); + + assert.equal(stats.recommended, true); + assert.deepEqual(stats.matchedTopics.sort(), ["effect-size", "sample-size", "statistics"]); +}); + +test("validates manifest shape", () => { + const manifest = loadSample(); + delete manifest.manuscript.id; + + const errors = validateManifest(manifest); + + assert.equal(errors.some((error) => error.includes("manuscript.id")), true); +}); + +test("uses deterministic stable digests", () => { + const left = { b: 2, a: { d: 4, c: 3 } }; + const right = { a: { c: 3, d: 4 }, b: 2 }; + + assert.equal(stableStringify(left), stableStringify(right)); + assert.equal(digest(left), digest(right)); +});