diff --git a/research-disclosure-assistant/README.md b/research-disclosure-assistant/README.md new file mode 100644 index 00000000..e3efcc40 --- /dev/null +++ b/research-disclosure-assistant/README.md @@ -0,0 +1,17 @@ +# Research Disclosure Assistant + +Self-contained SCIBASE AI-Assisted Research Tools slice for issue #13. The assistant checks funding statements, sponsor role clarity, competing-interest disclosures, author contribution roles, and corresponding-author accountability before AI peer-review aid output is trusted. + +## Why this slice is distinct + +Existing #13 submissions cover broad AI tool suites, evidence-grounded summaries, citation metadata/context/style/diversity/retraction/venue checks, methods reproducibility, protocol deviation, statistical power, multiple-comparison control, terminology/unit consistency, contradictory evidence, novelty overlap, and reporting-guideline checklists. This module focuses only on disclosure completeness and accountability. + +## Run + +```bash +npm test +npm run demo +npm run demo:video +``` + +Demo artifacts are written to `reports/`, including JSON, Markdown, SVG, GIF, and MP4 files. diff --git a/research-disclosure-assistant/demo.js b/research-disclosure-assistant/demo.js new file mode 100644 index 00000000..d0571320 --- /dev/null +++ b/research-disclosure-assistant/demo.js @@ -0,0 +1,59 @@ +const fs = require("fs"); +const path = require("path"); + +const { assessResearchDisclosures } = require("./index"); +const { cleanPacket, riskyPacket } = require("./sample-data"); + +const reportsDir = path.join(__dirname, "reports"); +fs.mkdirSync(reportsDir, { recursive: true }); + +function markdownReport(name, report) { + const findings = report.findings.length + ? report.findings + .map((item) => `- ${item.severity.toUpperCase()} ${item.code}: ${item.message}`) + .join("\n") + : "- No disclosure findings."; + return `# ${report.title} + +Scenario: ${name} + +Decision: ${report.decision.toUpperCase()} + +Reviewed ${report.summary.authorsReviewed} authors, ${report.summary.fundersReviewed} funders, and ${report.summary.disclosuresReviewed} disclosure records. + +## Findings + +${findings} + +## Release Criteria + +${report.releaseCriteria.map((item) => `- ${item}`).join("\n")} +`; +} + +function svgReport(report) { + const color = report.decision === "hold" ? "#dc2626" : report.decision === "revise" ? "#d97706" : "#16a34a"; + return ` + + Research Disclosure Assistant + ${report.manuscriptId} + + ${report.decision.toUpperCase()} + Findings: ${report.summary.findings} + High: ${report.summary.high} + Medium: ${report.summary.medium} + Low: ${report.summary.low} + Synthetic peer-review packet only. No private manuscripts or external APIs. +`; +} + +for (const [name, packet] of [ + ["clean-packet", cleanPacket], + ["risky-packet", riskyPacket], +]) { + const report = assessResearchDisclosures(packet); + fs.writeFileSync(path.join(reportsDir, `${name}.json`), JSON.stringify(report, null, 2)); + fs.writeFileSync(path.join(reportsDir, `${name}.md`), markdownReport(name, report)); + fs.writeFileSync(path.join(reportsDir, `${name}.svg`), svgReport(report)); + console.log(`${name}: ${report.decision} (${report.summary.findings} findings)`); +} diff --git a/research-disclosure-assistant/demo_video.py b/research-disclosure-assistant/demo_video.py new file mode 100644 index 00000000..5ef1196d --- /dev/null +++ b/research-disclosure-assistant/demo_video.py @@ -0,0 +1,46 @@ +from pathlib import Path + +import imageio.v3 as iio +import numpy as np +from PIL import Image, ImageDraw, ImageFont + + +ROOT = Path(__file__).resolve().parent +REPORTS = ROOT / "reports" +REPORTS.mkdir(exist_ok=True) + + +def font(size): + for name in ("arial.ttf", "segoeui.ttf"): + try: + return ImageFont.truetype(name, size) + except OSError: + pass + return ImageFont.load_default() + + +slides = [ + ("Research Disclosure Assistant", "AI-Assisted Research Tools #13"), + ("Checks", "funding statements + sponsor role clarity"), + ("Checks", "COI declarations + industry relationships"), + ("Decision", "hold AI review output until disclosures are complete"), +] + +frames = [] +for index, (title, subtitle) in enumerate(slides, start=1): + image = Image.new("RGB", (960, 544), "#102033") + draw = ImageDraw.Draw(image) + draw.rectangle((46, 54, 914, 490), outline="#facc15", width=3) + draw.text((82, 124), title, fill="#f8fafc", font=font(40)) + draw.text((82, 206), subtitle, fill="#fef9c3", font=font(26)) + draw.rectangle((82, 326, 770, 382), fill="#854d0e") + draw.text((104, 342), "disclosure gaps block trusted peer-review output", fill="#fffbeb", font=font(22)) + draw.text((82, 438), f"Slide {index}/4 - synthetic reviewer artifact", fill="#cbd5e1", font=font(20)) + frames.extend([image] * 14) + +gif_path = REPORTS / "demo.gif" +mp4_path = REPORTS / "demo.mp4" +frames[0].save(gif_path, save_all=True, append_images=frames[1:], duration=120, loop=0) +iio.imwrite(mp4_path, [np.asarray(frame) for frame in frames], fps=8, codec="libx264") +print(f"wrote {gif_path}") +print(f"wrote {mp4_path}") diff --git a/research-disclosure-assistant/index.js b/research-disclosure-assistant/index.js new file mode 100644 index 00000000..934a199b --- /dev/null +++ b/research-disclosure-assistant/index.js @@ -0,0 +1,231 @@ +const HIGH = "high"; +const MEDIUM = "medium"; +const LOW = "low"; + +const REQUIRED_CONTRIBUTIONS = new Set([ + "Conceptualization", + "Methodology", + "Software", + "Validation", + "Formal analysis", + "Data curation", + "Writing - original draft", + "Writing - review and editing", +]); + +function requireString(value, field) { + if (typeof value !== "string" || value.trim() === "") { + throw new TypeError(`${field} must be a non-empty string`); + } + return value.trim(); +} + +function asArray(value, field) { + if (!Array.isArray(value)) { + throw new TypeError(`${field} must be an array`); + } + return value; +} + +function normalizePacket(raw) { + return { + manuscriptId: requireString(raw.manuscriptId, "manuscriptId"), + title: requireString(raw.title, "title"), + fundingStatement: { + funders: asArray(raw.fundingStatement?.funders || [], "fundingStatement.funders").map(String), + grantIds: asArray(raw.fundingStatement?.grantIds || [], "fundingStatement.grantIds").map(String), + sponsorRole: String(raw.fundingStatement?.sponsorRole || "").trim(), + }, + competingInterests: asArray(raw.competingInterests || [], "competingInterests").map((item) => ({ + authorId: requireString(item.authorId, "competingInterests.authorId"), + statement: String(item.statement || "").trim(), + })), + acknowledgements: asArray(raw.acknowledgements || [], "acknowledgements").map(String), + authors: asArray(raw.authors || [], "authors").map((author) => ({ + id: requireString(author.id, "author.id"), + name: requireString(author.name, "author.name"), + corresponding: Boolean(author.corresponding), + contributions: asArray(author.contributions || [], "author.contributions").map(String), + affiliations: asArray(author.affiliations || [], "author.affiliations").map(String), + industryRelationships: asArray(author.industryRelationships || [], "author.industryRelationships").map(String), + })), + }; +} + +function finding(code, severity, message, remediation, subject = "manuscript") { + return { code, severity, subject, message, remediation }; +} + +function hasNoConflictStatement(statement) { + return /no competing|none declared|no conflict/i.test(statement); +} + +function relationshipDisclosed(relationship, statement) { + return statement.toLowerCase().includes(relationship.toLowerCase()); +} + +function assessResearchDisclosures(rawPacket) { + const packet = normalizePacket(rawPacket); + const findings = []; + const disclosures = new Map(packet.competingInterests.map((item) => [item.authorId, item.statement])); + const authorIds = new Set(packet.authors.map((author) => author.id)); + const acknowledgementText = packet.acknowledgements.join(" ").toLowerCase(); + + if (packet.fundingStatement.funders.length === 0) { + findings.push( + finding( + "MISSING_FUNDER_STATEMENT", + HIGH, + "No funder is listed for the manuscript.", + "Add an explicit funding or no-funding statement before AI peer-review output is trusted." + ) + ); + } + + if (packet.fundingStatement.funders.length > 0 && packet.fundingStatement.grantIds.length === 0) { + findings.push( + finding( + "MISSING_GRANT_IDENTIFIERS", + MEDIUM, + "Funding is declared without grant, award, or contract identifiers.", + "Add grant identifiers or document why the funder did not issue one." + ) + ); + } + + if (packet.fundingStatement.funders.length > 0 && !packet.fundingStatement.sponsorRole) { + findings.push( + finding( + "SPONSOR_ROLE_UNCLEAR", + HIGH, + "Sponsor role in study design, analysis, writing, or publication decision is not stated.", + "State the sponsor role explicitly, including whether the funder reviewed or influenced the manuscript." + ) + ); + } + + for (const author of packet.authors) { + const statement = disclosures.get(author.id) || ""; + if (!statement) { + findings.push( + finding( + "MISSING_COMPETING_INTEREST", + HIGH, + `${author.name} has no competing-interest statement.`, + "Add a competing-interest declaration for every listed author.", + author.id + ) + ); + } + + for (const relationship of author.industryRelationships) { + if (hasNoConflictStatement(statement) || !relationshipDisclosed(relationship, statement)) { + findings.push( + finding( + "UNDISCLOSED_INDUSTRY_RELATIONSHIP", + HIGH, + `${author.name} lists an industry relationship with ${relationship}, but the conflict statement does not disclose it.`, + "Revise the competing-interest declaration or remove/justify the relationship record.", + author.id + ) + ); + } + } + + if (author.contributions.length === 0) { + findings.push( + finding( + "MISSING_AUTHOR_CONTRIBUTIONS", + MEDIUM, + `${author.name} has no CRediT-style contributions listed.`, + "Add at least one contributor role for every author before reviewer packets are released.", + author.id + ) + ); + } + + const unrecognized = author.contributions.filter((role) => !REQUIRED_CONTRIBUTIONS.has(role)); + if (unrecognized.length > 0) { + findings.push( + finding( + "UNRECOGNIZED_CONTRIBUTION_ROLE", + LOW, + `${author.name} has non-standard contribution roles: ${unrecognized.join(", ")}.`, + "Map non-standard roles to the configured contribution taxonomy or document the extension.", + author.id + ) + ); + } + } + + for (const disclosure of packet.competingInterests) { + if (!authorIds.has(disclosure.authorId)) { + findings.push( + finding( + "ORPHAN_COMPETING_INTEREST", + MEDIUM, + `Competing-interest statement references unknown author ${disclosure.authorId}.`, + "Retarget or remove orphan disclosure records before export.", + disclosure.authorId + ) + ); + } + } + + for (const funder of packet.fundingStatement.funders) { + const namedInAcknowledgements = acknowledgementText.includes(funder.toLowerCase()); + if (namedInAcknowledgements && !packet.fundingStatement.sponsorRole) { + findings.push( + finding( + "ACKNOWLEDGED_SPONSOR_ROLE_MISMATCH", + MEDIUM, + `${funder} is acknowledged in the manuscript but sponsor role is absent from the funding statement.`, + "Reconcile acknowledgements with funding and sponsor-role disclosures." + ) + ); + } + } + + const hasCorrespondingAuthor = packet.authors.some((author) => author.corresponding); + if (!hasCorrespondingAuthor) { + findings.push( + finding( + "MISSING_CORRESPONDING_AUTHOR", + HIGH, + "No corresponding author is marked for accountability and disclosure follow-up.", + "Mark one or more corresponding authors before submission or AI-generated review packets proceed." + ) + ); + } + + const high = findings.filter((item) => item.severity === HIGH).length; + const medium = findings.filter((item) => item.severity === MEDIUM).length; + const decision = high > 0 ? "hold" : medium > 0 ? "revise" : "release"; + + return { + manuscriptId: packet.manuscriptId, + title: packet.title, + decision, + summary: { + authorsReviewed: packet.authors.length, + fundersReviewed: packet.fundingStatement.funders.length, + disclosuresReviewed: packet.competingInterests.length, + findings: findings.length, + high, + medium, + low: findings.filter((item) => item.severity === LOW).length, + }, + findings, + releaseCriteria: [ + "Funder and grant statements are explicit before AI peer-review aid output is trusted.", + "Sponsor role in design, analysis, writing, review, and publication decision is disclosed.", + "Every author has a competing-interest statement matching known relationships.", + "CRediT-style author contributions and corresponding-author accountability are present.", + ], + }; +} + +module.exports = { + assessResearchDisclosures, + normalizePacket, +}; diff --git a/research-disclosure-assistant/package.json b/research-disclosure-assistant/package.json new file mode 100644 index 00000000..c54c0dd4 --- /dev/null +++ b/research-disclosure-assistant/package.json @@ -0,0 +1,13 @@ +{ + "name": "research-disclosure-assistant", + "version": "1.0.0", + "description": "Funding, conflict-of-interest, and author contribution assistant for SCIBASE issue #13", + "main": "index.js", + "type": "commonjs", + "scripts": { + "test": "node test.js", + "demo": "node demo.js", + "demo:video": "python demo_video.py" + }, + "license": "MIT" +} diff --git a/research-disclosure-assistant/reports/clean-packet.json b/research-disclosure-assistant/reports/clean-packet.json new file mode 100644 index 00000000..dc6bb73b --- /dev/null +++ b/research-disclosure-assistant/reports/clean-packet.json @@ -0,0 +1,21 @@ +{ + "manuscriptId": "peer-review-disclosure-001", + "title": "AI-assisted polymer screening", + "decision": "release", + "summary": { + "authorsReviewed": 3, + "fundersReviewed": 2, + "disclosuresReviewed": 3, + "findings": 0, + "high": 0, + "medium": 0, + "low": 0 + }, + "findings": [], + "releaseCriteria": [ + "Funder and grant statements are explicit before AI peer-review aid output is trusted.", + "Sponsor role in design, analysis, writing, review, and publication decision is disclosed.", + "Every author has a competing-interest statement matching known relationships.", + "CRediT-style author contributions and corresponding-author accountability are present." + ] +} \ No newline at end of file diff --git a/research-disclosure-assistant/reports/clean-packet.md b/research-disclosure-assistant/reports/clean-packet.md new file mode 100644 index 00000000..afeeb079 --- /dev/null +++ b/research-disclosure-assistant/reports/clean-packet.md @@ -0,0 +1,18 @@ +# AI-assisted polymer screening + +Scenario: clean-packet + +Decision: RELEASE + +Reviewed 3 authors, 2 funders, and 3 disclosure records. + +## Findings + +- No disclosure findings. + +## Release Criteria + +- Funder and grant statements are explicit before AI peer-review aid output is trusted. +- Sponsor role in design, analysis, writing, review, and publication decision is disclosed. +- Every author has a competing-interest statement matching known relationships. +- CRediT-style author contributions and corresponding-author accountability are present. diff --git a/research-disclosure-assistant/reports/clean-packet.svg b/research-disclosure-assistant/reports/clean-packet.svg new file mode 100644 index 00000000..7c555c7f --- /dev/null +++ b/research-disclosure-assistant/reports/clean-packet.svg @@ -0,0 +1,12 @@ + + + Research Disclosure Assistant + peer-review-disclosure-001 + + RELEASE + Findings: 0 + High: 0 + Medium: 0 + Low: 0 + Synthetic peer-review packet only. No private manuscripts or external APIs. + \ No newline at end of file diff --git a/research-disclosure-assistant/reports/demo.gif b/research-disclosure-assistant/reports/demo.gif new file mode 100644 index 00000000..4adab082 Binary files /dev/null and b/research-disclosure-assistant/reports/demo.gif differ diff --git a/research-disclosure-assistant/reports/demo.mp4 b/research-disclosure-assistant/reports/demo.mp4 new file mode 100644 index 00000000..0440ffa3 Binary files /dev/null and b/research-disclosure-assistant/reports/demo.mp4 differ diff --git a/research-disclosure-assistant/reports/risky-packet.json b/research-disclosure-assistant/reports/risky-packet.json new file mode 100644 index 00000000..8ae87e91 --- /dev/null +++ b/research-disclosure-assistant/reports/risky-packet.json @@ -0,0 +1,71 @@ +{ + "manuscriptId": "peer-review-disclosure-002", + "title": "Closed-loop catalyst ranking", + "decision": "hold", + "summary": { + "authorsReviewed": 2, + "fundersReviewed": 1, + "disclosuresReviewed": 2, + "findings": 7, + "high": 4, + "medium": 3, + "low": 0 + }, + "findings": [ + { + "code": "MISSING_GRANT_IDENTIFIERS", + "severity": "medium", + "subject": "manuscript", + "message": "Funding is declared without grant, award, or contract identifiers.", + "remediation": "Add grant identifiers or document why the funder did not issue one." + }, + { + "code": "SPONSOR_ROLE_UNCLEAR", + "severity": "high", + "subject": "manuscript", + "message": "Sponsor role in study design, analysis, writing, or publication decision is not stated.", + "remediation": "State the sponsor role explicitly, including whether the funder reviewed or influenced the manuscript." + }, + { + "code": "UNDISCLOSED_INDUSTRY_RELATIONSHIP", + "severity": "high", + "subject": "a1", + "message": "Ari Chen lists an industry relationship with NanoCatalyst Inc., but the conflict statement does not disclose it.", + "remediation": "Revise the competing-interest declaration or remove/justify the relationship record." + }, + { + "code": "MISSING_COMPETING_INTEREST", + "severity": "high", + "subject": "a2", + "message": "Bea Singh has no competing-interest statement.", + "remediation": "Add a competing-interest declaration for every listed author." + }, + { + "code": "MISSING_AUTHOR_CONTRIBUTIONS", + "severity": "medium", + "subject": "a2", + "message": "Bea Singh has no CRediT-style contributions listed.", + "remediation": "Add at least one contributor role for every author before reviewer packets are released." + }, + { + "code": "ACKNOWLEDGED_SPONSOR_ROLE_MISMATCH", + "severity": "medium", + "subject": "manuscript", + "message": "NanoCatalyst Inc. is acknowledged in the manuscript but sponsor role is absent from the funding statement.", + "remediation": "Reconcile acknowledgements with funding and sponsor-role disclosures." + }, + { + "code": "MISSING_CORRESPONDING_AUTHOR", + "severity": "high", + "subject": "manuscript", + "message": "No corresponding author is marked for accountability and disclosure follow-up.", + "remediation": "Mark one or more corresponding authors before submission or AI-generated review packets proceed." + } + ], + "releaseCriteria": [ + "Funder and grant statements are explicit before AI peer-review aid output is trusted.", + "Sponsor role in design, analysis, writing, review, and publication decision is disclosed.", + "Every author has a competing-interest statement matching known relationships.", + "CRediT-style author contributions and corresponding-author accountability are present." + ] +} \ No newline at end of file diff --git a/research-disclosure-assistant/reports/risky-packet.md b/research-disclosure-assistant/reports/risky-packet.md new file mode 100644 index 00000000..97868e79 --- /dev/null +++ b/research-disclosure-assistant/reports/risky-packet.md @@ -0,0 +1,24 @@ +# Closed-loop catalyst ranking + +Scenario: risky-packet + +Decision: HOLD + +Reviewed 2 authors, 1 funders, and 2 disclosure records. + +## Findings + +- MEDIUM MISSING_GRANT_IDENTIFIERS: Funding is declared without grant, award, or contract identifiers. +- HIGH SPONSOR_ROLE_UNCLEAR: Sponsor role in study design, analysis, writing, or publication decision is not stated. +- HIGH UNDISCLOSED_INDUSTRY_RELATIONSHIP: Ari Chen lists an industry relationship with NanoCatalyst Inc., but the conflict statement does not disclose it. +- HIGH MISSING_COMPETING_INTEREST: Bea Singh has no competing-interest statement. +- MEDIUM MISSING_AUTHOR_CONTRIBUTIONS: Bea Singh has no CRediT-style contributions listed. +- MEDIUM ACKNOWLEDGED_SPONSOR_ROLE_MISMATCH: NanoCatalyst Inc. is acknowledged in the manuscript but sponsor role is absent from the funding statement. +- HIGH MISSING_CORRESPONDING_AUTHOR: No corresponding author is marked for accountability and disclosure follow-up. + +## Release Criteria + +- Funder and grant statements are explicit before AI peer-review aid output is trusted. +- Sponsor role in design, analysis, writing, review, and publication decision is disclosed. +- Every author has a competing-interest statement matching known relationships. +- CRediT-style author contributions and corresponding-author accountability are present. diff --git a/research-disclosure-assistant/reports/risky-packet.svg b/research-disclosure-assistant/reports/risky-packet.svg new file mode 100644 index 00000000..05cdd359 --- /dev/null +++ b/research-disclosure-assistant/reports/risky-packet.svg @@ -0,0 +1,12 @@ + + + Research Disclosure Assistant + peer-review-disclosure-002 + + HOLD + Findings: 7 + High: 4 + Medium: 3 + Low: 0 + Synthetic peer-review packet only. No private manuscripts or external APIs. + \ No newline at end of file diff --git a/research-disclosure-assistant/requirements-map.md b/research-disclosure-assistant/requirements-map.md new file mode 100644 index 00000000..ddc7fdfe --- /dev/null +++ b/research-disclosure-assistant/requirements-map.md @@ -0,0 +1,11 @@ +# Requirements Map + +Issue #13 asks for AI-assisted research tools, including a peer-review aid that checks technical and compliance issues before sharing or submission. + +| Issue capability | This implementation | +| --- | --- | +| AI Peer Review Aid | Produces deterministic disclosure diagnostics for manuscript peer-review packets. | +| Compliance checks | Checks funder statements, sponsor-role clarity, competing interests, author contributions, and accountability gaps. | +| Custom reviewer diagnostics | Emits finding codes, severities, affected subjects, and remediation guidance. | +| Research quality workflow | Uses `release`, `revise`, or `hold` decisions before AI review output is trusted. | +| Safe MVP behavior | Uses synthetic packets only, with no credentials, external AI APIs, private manuscripts, or live compliance systems. | diff --git a/research-disclosure-assistant/sample-data.js b/research-disclosure-assistant/sample-data.js new file mode 100644 index 00000000..98052825 --- /dev/null +++ b/research-disclosure-assistant/sample-data.js @@ -0,0 +1,82 @@ +const cleanPacket = { + manuscriptId: "peer-review-disclosure-001", + title: "AI-assisted polymer screening", + fundingStatement: { + funders: ["National Science Fund", "Open Materials Institute"], + grantIds: ["NSF-2244", "OMI-18"], + sponsorRole: "Funders had no role in study design, analysis, manuscript preparation, or publication decision.", + }, + competingInterests: [ + { authorId: "a1", statement: "No competing interests declared." }, + { authorId: "a2", statement: "No competing interests declared." }, + { authorId: "a3", statement: "Consulting fees from NanoCatalyst Inc.; unrelated to this work." }, + ], + acknowledgements: ["NanoCatalyst Inc. provided access to a public benchmark dataset."], + authors: [ + { + id: "a1", + name: "Ari Chen", + corresponding: true, + contributions: ["Conceptualization", "Methodology", "Writing - original draft"], + affiliations: ["Open Materials Institute"], + industryRelationships: [], + }, + { + id: "a2", + name: "Bea Singh", + corresponding: false, + contributions: ["Software", "Validation", "Formal analysis"], + affiliations: ["State University"], + industryRelationships: [], + }, + { + id: "a3", + name: "Cam Reed", + corresponding: false, + contributions: ["Data curation", "Writing - review and editing"], + affiliations: ["State University"], + industryRelationships: ["NanoCatalyst Inc."], + }, + ], +}; + +const riskyPacket = { + manuscriptId: "peer-review-disclosure-002", + title: "Closed-loop catalyst ranking", + fundingStatement: { + funders: ["NanoCatalyst Inc."], + grantIds: [], + sponsorRole: "", + }, + competingInterests: [ + { authorId: "a1", statement: "No competing interests declared." }, + { authorId: "a2", statement: "" }, + ], + acknowledgements: [ + "NanoCatalyst Inc. supplied proprietary screening data and reviewed the manuscript.", + "Dana Fox helped prepare the final figures.", + ], + authors: [ + { + id: "a1", + name: "Ari Chen", + corresponding: false, + contributions: ["Conceptualization"], + affiliations: ["Open Materials Institute"], + industryRelationships: ["NanoCatalyst Inc."], + }, + { + id: "a2", + name: "Bea Singh", + corresponding: false, + contributions: [], + affiliations: ["State University"], + industryRelationships: [], + }, + ], +}; + +module.exports = { + cleanPacket, + riskyPacket, +}; diff --git a/research-disclosure-assistant/test.js b/research-disclosure-assistant/test.js new file mode 100644 index 00000000..afd5b9b5 --- /dev/null +++ b/research-disclosure-assistant/test.js @@ -0,0 +1,46 @@ +const assert = require("assert"); + +const { assessResearchDisclosures, normalizePacket } = require("./index"); +const { cleanPacket, riskyPacket } = require("./sample-data"); + +const clean = assessResearchDisclosures(cleanPacket); +assert.strictEqual(clean.decision, "release"); +assert.strictEqual(clean.summary.findings, 0); + +const risky = assessResearchDisclosures(riskyPacket); +assert.strictEqual(risky.decision, "hold"); +for (const code of [ + "MISSING_GRANT_IDENTIFIERS", + "SPONSOR_ROLE_UNCLEAR", + "UNDISCLOSED_INDUSTRY_RELATIONSHIP", + "MISSING_COMPETING_INTEREST", + "MISSING_AUTHOR_CONTRIBUTIONS", + "ACKNOWLEDGED_SPONSOR_ROLE_MISMATCH", + "MISSING_CORRESPONDING_AUTHOR", +]) { + assert(risky.findings.some((finding) => finding.code === code), `missing ${code}`); +} + +const noFunding = assessResearchDisclosures({ + ...cleanPacket, + fundingStatement: { funders: [], grantIds: [], sponsorRole: "" }, +}); +assert.strictEqual(noFunding.decision, "hold"); +assert(noFunding.findings.some((finding) => finding.code === "MISSING_FUNDER_STATEMENT")); + +const orphanDisclosure = assessResearchDisclosures({ + ...cleanPacket, + competingInterests: [ + ...cleanPacket.competingInterests, + { authorId: "ghost", statement: "No competing interests declared." }, + ], +}); +assert.strictEqual(orphanDisclosure.decision, "revise"); +assert(orphanDisclosure.findings.some((finding) => finding.code === "ORPHAN_COMPETING_INTEREST")); + +assert.throws( + () => normalizePacket({ ...cleanPacket, manuscriptId: "" }), + /manuscriptId must be a non-empty string/ +); + +console.log("research disclosure assistant tests passed");