Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions manuscript-unit-consistency-assistant/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# Manuscript Unit Consistency Assistant

Dependency-free AI peer-review aid slice for SCIBASE issue #13.

This module audits draft manuscript packets for unit hygiene risks that slow down scientific peer review:

- numeric values that appear without adjacent units
- the same reagent or outcome reported across incompatible unit families
- table header units that drift from cell-level units
- implausible wet-lab temperature or percentage values
- reviewer-ready actions and deterministic audit digests

The implementation uses synthetic sample data only. It is designed as a small, auditable MVP component that can sit behind an AI-assisted pre-submission review workflow.

## Run

```bash
npm --prefix manuscript-unit-consistency-assistant run check
npm --prefix manuscript-unit-consistency-assistant test
npm --prefix manuscript-unit-consistency-assistant run demo

node manuscript-unit-consistency-assistant/test.js
node manuscript-unit-consistency-assistant/demo.js
node --check manuscript-unit-consistency-assistant/index.js
node --check manuscript-unit-consistency-assistant/sample-data.js
node --check manuscript-unit-consistency-assistant/test.js
node --check manuscript-unit-consistency-assistant/demo.js
```

The demo writes:

- `reports/unit-consistency-report.json`
- `reports/unit-consistency-report.md`
- `reports/unit-consistency-summary.svg`
- `reports/demo-script.txt`
- `reports/demo.mp4`

## API

```js
const { analyzeManuscript } = require("./index");

const result = analyzeManuscript({
manuscriptId: "draft-001",
title: "Draft title",
trackedTerms: [{ name: "IL-6", aliases: ["IL-6", "IL6"] }],
sections: [{ id: "methods", title: "Methods", text: "IL-6 was measured at 180 pg/mL." }],
tables: [],
});
```

`result` contains a readiness summary, findings, reviewer actions, measurement inventory, and a stable audit digest.
31 changes: 31 additions & 0 deletions manuscript-unit-consistency-assistant/acceptance-notes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Acceptance Notes

## What Changed

- Added a self-contained manuscript unit consistency assistant.
- Added synthetic sample data that demonstrates missing units, molar-vs-mass concentration conflicts, table unit drift, and implausible temperature detection.
- Added deterministic tests and demo artifacts for local review.

## Verification Targets

- The module runs with Node.js and no third-party dependencies.
- Findings include stable IDs, evidence snippets, recommendations, and severity.
- Reports are deterministic across repeated runs.
- Clean sample manuscripts return a 100 readiness score with no findings.
- The included demo video is H.264, 1280x720, and 4 seconds long.

## Local Validation

```bash
npm --prefix manuscript-unit-consistency-assistant run check
npm --prefix manuscript-unit-consistency-assistant test
npm --prefix manuscript-unit-consistency-assistant run demo
node manuscript-unit-consistency-assistant/test.js
node manuscript-unit-consistency-assistant/demo.js
node --check manuscript-unit-consistency-assistant/index.js
node --check manuscript-unit-consistency-assistant/sample-data.js
node --check manuscript-unit-consistency-assistant/test.js
node --check manuscript-unit-consistency-assistant/demo.js
git diff --check
mdls -name kMDItemDurationSeconds -name kMDItemCodecs -name kMDItemPixelHeight -name kMDItemPixelWidth manuscript-unit-consistency-assistant/reports/demo.mp4
```
123 changes: 123 additions & 0 deletions manuscript-unit-consistency-assistant/demo.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
"use strict";

const fs = require("node:fs");
const path = require("node:path");
const { analyzeManuscript } = require("./index");
const { sampleManuscript } = require("./sample-data");

const outputDir = path.join(__dirname, "reports");
const report = analyzeManuscript(sampleManuscript);

fs.mkdirSync(outputDir, { recursive: true });
fs.writeFileSync(
path.join(outputDir, "unit-consistency-report.json"),
`${JSON.stringify(report, null, 2)}\n`
);
fs.writeFileSync(
path.join(outputDir, "unit-consistency-report.md"),
renderMarkdown(report)
);
fs.writeFileSync(
path.join(outputDir, "unit-consistency-summary.svg"),
renderSvg(report)
);
fs.writeFileSync(
path.join(outputDir, "demo-script.txt"),
renderDemoScript(report)
);

console.log(`Wrote reports to ${outputDir}`);

function renderMarkdown(result) {
const lines = [
"# Manuscript Unit Consistency Report",
"",
`Manuscript: ${result.summary.title}`,
`Audit digest: ${result.auditDigest}`,
`Readiness: ${result.summary.readiness} (${result.summary.readinessScore}/100)`,
"",
"## Findings",
"",
];

if (result.findings.length === 0) {
lines.push("No unit consistency findings were detected.");
} else {
result.findings.forEach((finding) => {
lines.push(`- ${finding.severity.toUpperCase()} ${finding.type}: ${finding.message}`);
lines.push(` Evidence: ${finding.evidence}`);
lines.push(` Action: ${finding.recommendation}`);
});
}

lines.push("", "## Reviewer Actions", "");
result.reviewerActions.forEach((action) => {
lines.push(`- P${action.priority} ${action.action} (${action.sourceId})`);
});

lines.push("", "## Measurement Inventory", "");
result.measurementInventory.forEach((measurement) => {
lines.push(`- ${measurement.sourceId}: ${measurement.raw} -> ${measurement.unitFamily} (${measurement.subject || "untracked"})`);
});

return `${lines.join("\n")}\n`;
}

function renderDemoScript(result) {
return [
"Demo storyboard for manuscript-unit-consistency-assistant",
"",
"1. Load a draft manuscript packet with sections, tables, and tracked terms.",
`2. Extract ${result.summary.measurementsReviewed} measurements from methods, results, and table cells.`,
`3. Surface ${result.findings.length} reviewer findings, prioritizing high-severity blockers first.`,
"4. Export JSON, Markdown, and SVG artifacts for an AI peer-review aid workflow.",
"",
`Readiness result: ${result.summary.readiness} (${result.summary.readinessScore}/100).`,
`Audit digest: ${result.auditDigest}.`,
"",
].join("\n");
}

function renderSvg(result) {
const high = result.summary.findingsBySeverity.high || 0;
const medium = result.summary.findingsBySeverity.medium || 0;
const low = result.summary.findingsBySeverity.low || 0;
const score = result.summary.readinessScore;
const bars = [
{ label: "High", value: high, color: "#d92d20", y: 150 },
{ label: "Medium", value: medium, color: "#f79009", y: 205 },
{ label: "Low", value: low, color: "#475467", y: 260 },
];
const max = Math.max(1, high, medium, low);
const barSvg = bars.map((bar) => {
const width = 360 * (bar.value / max);
return [
`<text x="42" y="${bar.y - 8}" font-family="Arial" font-size="17" fill="#101828">${bar.label}: ${bar.value}</text>`,
`<rect x="42" y="${bar.y}" width="${Math.max(8, width)}" height="26" rx="4" fill="${bar.color}"/>`,
].join("\n");
}).join("\n");

return [
'<svg xmlns="http://www.w3.org/2000/svg" width="920" height="420" viewBox="0 0 920 420">',
'<rect width="920" height="420" fill="#f8fafc"/>',
'<rect x="24" y="24" width="872" height="372" rx="8" fill="#ffffff" stroke="#d0d5dd"/>',
'<text x="42" y="72" font-family="Arial" font-size="30" font-weight="700" fill="#101828">Manuscript Unit Consistency Assistant</text>',
`<text x="42" y="110" font-family="Arial" font-size="18" fill="#475467">${escapeXml(result.summary.title)}</text>`,
`<text x="42" y="135" font-family="Arial" font-size="15" fill="#667085">Audit digest ${result.auditDigest}</text>`,
barSvg,
`<circle cx="730" cy="205" r="86" fill="#ecfdf3" stroke="#12b76a" stroke-width="10"/>`,
`<text x="730" y="195" font-family="Arial" font-size="42" font-weight="700" text-anchor="middle" fill="#027a48">${score}</text>`,
'<text x="730" y="228" font-family="Arial" font-size="16" text-anchor="middle" fill="#027a48">readiness score</text>',
`<text x="730" y="286" font-family="Arial" font-size="17" text-anchor="middle" fill="#344054">${escapeXml(result.summary.readiness)}</text>`,
'</svg>',
"",
].join("\n");
}

function escapeXml(value) {
return String(value)
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;");
}
Loading