SPB Git

Repository Public Research preview

manuscript.mjs 6.1 KB 115 lines javascript

1#!/usr/bin/env node2/**3 * Deterministic audits of a LaTeX or Markdown manuscript. No dependencies, no network, no model.4 *5 *   node manuscript.mjs audit <file>          every check, findings sorted by severity6 *   node manuscript.mjs outline <file>        sections, word counts, abstract length, totals7 *   node manuscript.mjs citations <file>      cited keys against the bibliography8 *   node manuscript.mjs refs <file>           labels against references9 *   node manuscript.mjs numbers <file>        abstract numbers, sample sizes, percentages10 *   node manuscript.mjs stats <file>          how statistics are reported11 *   node manuscript.mjs tables <file>         tables and figures12 *   node manuscript.mjs claims <file>         priority, certainty and robustness claims13 *   node manuscript.mjs style <file>          leftovers, acronyms, long sentences, hedging14 *   node manuscript.mjs replication <file>    data, code, software, seed statements15 *   node manuscript.mjs scaffold <file>       referee-report skeleton in Markdown16 *   node manuscript.mjs compare <old> <new>   what changed between two versions17 *   node manuscript.mjs codes                 every finding code and how to verify it18 *19 * Options:20 *   --root DIR           only directory files are read from (default: the directory of <file>).21 *                        Set it to the project directory when the manuscript includes ../tables/x.22 *   --bib FILE           bibliography (.bib or CSL JSON) inside the root; default: the one declared23 *   --format latex|markdown24 *   --text               short text listing instead of JSON25 *   --min-severity info|warning|error      lowest severity listed (counts cover everything)26 *   --fail-on error|warning|never          exit 2 when such findings exist (default: error)27 *   --include-info       scaffold: list info-level notes too28 *   --json               scaffold: JSON instead of Markdown29 *30 * Output is JSON unless --text (scaffold: Markdown unless --json).31 * Exit code 0 ok, 1 usage or read error, 2 findings at the --fail-on level.32 * Findings marked heuristic are pattern matches: read the line before reporting them.33 * An empty list never means the manuscript is correct.34 */35import { readFileSync } from "node:fs";36import { dirname, relative, resolve } from "node:path";37import { fileURLToPath } from "node:url";38import {39  CODES, auditCitations, auditClaims, auditCrossReferences, auditNumbers, auditReplication, auditStatistics, auditStyle, auditTables,40  compareVersions, fullAudit, loadDocument, outline, reviewScaffold,41} from "./lib/index.mjs";4243const AUDITS = {44  audit: fullAudit, outline, citations: auditCitations, refs: auditCrossReferences, numbers: auditNumbers, stats: auditStatistics,45  tables: auditTables, claims: auditClaims, style: auditStyle, replication: auditReplication,46};47const BOOL = new Set(["text", "json", "include-info", "help"]);4849function usage() {50  const src = readFileSync(fileURLToPath(import.meta.url), "utf8");51  return src.slice(src.indexOf("/**") + 3, src.indexOf("*/")).split("\n").map((l) => l.replace(/^ \* ?/, "")).join("\n").trim();52}53function die(code, message) {54  process.stderr.write(`${JSON.stringify({ error: { code, message } })}\n`);55  process.exit(1);56}5758const [cmd, ...rest] = process.argv.slice(2);59const flags = {}, args = [];60for (let i = 0; i < rest.length; i++) {61  const a = rest[i];62  if (!a.startsWith("--")) { args.push(a); continue; }63  const k = a.slice(2);64  if (BOOL.has(k)) flags[k] = true;65  else { if (rest[i + 1] === undefined) die("usage", `--${k} needs a value`); flags[k] = rest[++i]; }66}67if (!cmd || cmd === "help" || cmd === "--help" || flags.help) { console.log(usage()); process.exit(cmd ? 0 : 1); }6869const rank = { info: 0, warning: 1, error: 2 };70if (flags["min-severity"] && !(flags["min-severity"] in rank)) die("usage", "--min-severity must be info, warning or error");71const failOn = flags["fail-on"] ?? "error";72if (!["error", "warning", "never"].includes(failOn)) die("usage", "--fail-on must be error, warning or never");7374function load(file) {75  const abs = resolve(file);76  const root = flags.root ? resolve(flags.root) : dirname(abs);77  return loadDocument({ root, path: relative(root, abs) || file, format: flags.format, bib_path: flags.bib });78}79const print = (data) => console.log(JSON.stringify(data, null, 2));80function listing(r) {81  const s = r.summary;82  const lines = [`${r.file} (${r.format}): ${s.errors} errors, ${s.warnings} warnings, ${s.infos} notes${s.hygiene_score !== undefined ? `, hygiene score ${s.hygiene_score}/100 (not a quality score)` : ""}`];83  for (const f of r.findings) lines.push(`${f.severity.toUpperCase().padEnd(7)} ${f.location.file}:${f.location.line}  ${f.code}${f.heuristic ? " (heuristic)" : ""}  ${f.message}`);84  lines.push(r.disclaimer);85  return lines.join("\n");86}8788try {89  if (cmd === "codes") {90    print(Object.entries(CODES).map(([code, c]) => ({ code, ...c })));91  } else if (cmd === "compare") {92    if (args.length !== 2) die("usage", "compare needs two files: <old> <new>");93    const [a, b] = args.map((x) => resolve(x));94    const root = flags.root ? resolve(flags.root) : null;95    const one = (abs) => { const r = root ?? dirname(abs); return loadDocument({ root: r, path: relative(r, abs), format: flags.format }); };96    print(compareVersions(one(a), one(b)));97  } else if (cmd === "scaffold") {98    if (args.length !== 1) die("usage", "scaffold needs one file");99    const r = reviewScaffold(load(args[0]), { include_info: !!flags["include-info"] });100    if (flags.json) print(r); else console.log(r.markdown);101  } else if (AUDITS[cmd]) {102    if (args.length !== 1) die("usage", `${cmd} needs one file`);103    const full = AUDITS[cmd](load(args[0]), {});104    const min = rank[flags["min-severity"] ?? "info"];105    const r = { ...full, findings: full.findings.filter((f) => rank[f.severity] >= min) };106    if (flags.text) console.log(listing(r)); else print(r);107    const bad = failOn === "never" ? 0 : failOn === "warning" ? full.summary.errors + full.summary.warnings : full.summary.errors;108    if (bad > 0) process.exitCode = 2;109  } else {110    die("usage", `Unknown command "${cmd}". Run with --help.`);111  }112} catch (e) {113  die(e.code ?? "error", String(e.message ?? e));114}115
Files main
Menu

Theme