paperlint 7.2.0 → 8.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (156) hide show
  1. package/dist/build.d.ts.map +1 -1
  2. package/dist/build.js +1 -6
  3. package/dist/build.js.map +1 -1
  4. package/dist/cli.d.ts +3 -4
  5. package/dist/cli.d.ts.map +1 -1
  6. package/dist/cli.js +13 -42
  7. package/dist/cli.js.map +1 -1
  8. package/dist/eslint-rules/latex-language.d.ts.map +1 -1
  9. package/dist/eslint-rules/latex-language.js +10 -13
  10. package/dist/eslint-rules/latex-language.js.map +1 -1
  11. package/dist/eslint-rules/paper-context.js +2 -2
  12. package/dist/eslint-rules/paper-typography.d.ts.map +1 -1
  13. package/dist/eslint-rules/paper-typography.js +22 -91
  14. package/dist/eslint-rules/paper-typography.js.map +1 -1
  15. package/dist/eslint-rules/papers.d.ts +1 -3
  16. package/dist/eslint-rules/papers.d.ts.map +1 -1
  17. package/dist/eslint-rules/papers.js +2 -4
  18. package/dist/eslint-rules/papers.js.map +1 -1
  19. package/dist/eslint-rules/tex-build.d.ts.map +1 -1
  20. package/dist/eslint-rules/tex-build.js +8 -25
  21. package/dist/eslint-rules/tex-build.js.map +1 -1
  22. package/dist/init.d.ts +1 -4
  23. package/dist/init.d.ts.map +1 -1
  24. package/dist/init.js +3 -9
  25. package/dist/init.js.map +1 -1
  26. package/dist/lib/markdown.d.ts +5 -81
  27. package/dist/lib/markdown.d.ts.map +1 -1
  28. package/dist/lib/markdown.js +5 -116
  29. package/dist/lib/markdown.js.map +1 -1
  30. package/dist/lib/paper-config.d.ts.map +1 -1
  31. package/dist/lib/paper-config.js +0 -2
  32. package/dist/lib/paper-config.js.map +1 -1
  33. package/dist/lib/skill-eval-fixture.d.ts +6 -4
  34. package/dist/lib/skill-eval-fixture.d.ts.map +1 -1
  35. package/dist/lib/skill-eval-fixture.js +25 -18
  36. package/dist/lib/skill-eval-fixture.js.map +1 -1
  37. package/dist/new-paper.d.ts +5 -11
  38. package/dist/new-paper.d.ts.map +1 -1
  39. package/dist/new-paper.js +12 -23
  40. package/dist/new-paper.js.map +1 -1
  41. package/dist/paper-files.d.ts +4 -4
  42. package/dist/paper-prose.d.ts +17 -0
  43. package/dist/paper-prose.d.ts.map +1 -0
  44. package/dist/paper-prose.js +46 -0
  45. package/dist/paper-prose.js.map +1 -0
  46. package/dist/structure.d.ts.map +1 -1
  47. package/dist/structure.js +4 -3
  48. package/dist/structure.js.map +1 -1
  49. package/dist/types.d.ts +0 -2
  50. package/dist/types.d.ts.map +1 -1
  51. package/docs/configuration.md +7 -9
  52. package/docs/e2e.md +4 -6
  53. package/docs/prior-art/paper-folder-scaffolding.md +24 -29
  54. package/docs/rules/tex/claim-provenance.md +0 -1
  55. package/docs/rules.md +2 -6
  56. package/eslint-rules/latex-language.harness.mjs +3 -3
  57. package/eslint-rules/latex-language.ts +10 -13
  58. package/eslint-rules/paper-context.ts +2 -2
  59. package/eslint-rules/paper-typography.test.mjs +17 -85
  60. package/eslint-rules/paper-typography.ts +22 -98
  61. package/eslint-rules/papers.harness.mjs +1 -1
  62. package/eslint-rules/papers.ts +2 -4
  63. package/eslint-rules/tex-build.harness.mjs +2 -2
  64. package/eslint-rules/tex-build.ts +8 -25
  65. package/eslint.config.mjs +0 -1
  66. package/hooks/hooks.harness.ts +15 -15
  67. package/hooks/paper-edit-guard.hook.mjs +5 -8
  68. package/hooks/paper-edit-guard.test.ts +12 -3
  69. package/hooks/paper-skills-nudge.hook.mjs +2 -3
  70. package/hooks/paper-skills-nudge.test.ts +13 -3
  71. package/hooks/paper-status-gates.hook.mjs +2 -2
  72. package/hooks/paper-status-gates.test.ts +14 -2
  73. package/lib/markdown.test.mjs +5 -60
  74. package/lib/markdown.ts +9 -136
  75. package/lib/paper-config.ts +0 -2
  76. package/lib/skill-checks.test.mjs +4 -1
  77. package/lib/skill-eval-fixture.ts +25 -18
  78. package/package.json +1 -3
  79. package/presets/realm.jsonc +1 -1
  80. package/scripts/harness-api.frozen.json +0 -5
  81. package/skills/cold-read-diff/SKILL.md +3 -3
  82. package/skills/cold-read-diff/SKILL.md.spec.ts +2 -2
  83. package/skills/grade-paper-writing/SKILL.md +14 -22
  84. package/skills/grade-paper-writing/SKILL.md.spec.ts +13 -21
  85. package/skills/grade-paper-writing/__snapshots__/prose-lint.test.ts.snap +40 -26
  86. package/skills/grade-paper-writing/prose-lint.mjs +108 -278
  87. package/skills/grade-paper-writing/prose-lint.test.ts +184 -81
  88. package/skills/paper-adversarial-review/SKILL.md +2 -2
  89. package/skills/paper-adversarial-review/SKILL.md.spec.ts +1 -1
  90. package/skills/paper-pipeline/SKILL.md +9 -12
  91. package/skills/paper-pipeline/SKILL.md.spec.ts +8 -11
  92. package/skills/paper-pipeline/description-language.eval.mjs +25 -15
  93. package/skills/paper-pipeline/framing-vs-vocabulary.eval.mjs +25 -15
  94. package/skills/paper-pipeline/grade-paper-writing-ablation.eval.mjs +25 -15
  95. package/skills/paper-pipeline/pipeline-firing.eval.mjs +27 -16
  96. package/skills/paper-pipeline/pipeline-language.eval.mjs +25 -15
  97. package/skills/paper-pipeline/references/acceptance-gate.md +16 -20
  98. package/skills/paper-pipeline/references/artifact-checklist.md +4 -5
  99. package/skills/paper-pipeline/references/pipeline-status-template.md +1 -1
  100. package/skills/paper-pipeline/scripts/README.md +0 -4
  101. package/skills/paper-pipeline/scripts/extract-ref-facts.harness.mjs +8 -72
  102. package/skills/paper-pipeline/scripts/extract-ref-facts.mjs +48 -156
  103. package/skills/paper-pipeline/scripts/extract-ref-facts.test.mjs +81 -75
  104. package/skills/paper-pipeline/scripts/generated-code.harness.mjs +3 -3
  105. package/skills/paper-status/SKILL.md +2 -2
  106. package/skills/paper-status/SKILL.md.spec.ts +1 -1
  107. package/skills/render-paper/SKILL.md +2 -2
  108. package/skills/render-paper/SKILL.md.spec.ts +1 -1
  109. package/skills/render-paper/extract-pdf-facts.mjs +2 -2
  110. package/skills/tighten-paper/SKILL.md +29 -42
  111. package/skills/tighten-paper/SKILL.md.spec.ts +28 -41
  112. package/skills/verify-citations/scripts/bib-authors.check.test.mjs +0 -13
  113. package/skills/verify-citations/scripts/bib-authors.cli.test.ts +23 -11
  114. package/skills/verify-citations/scripts/bib-authors.mjs +8 -93
  115. package/skills/verify-citations/scripts/bib-authors.test.mjs +1 -44
  116. package/src/build.harness.mjs +2 -2
  117. package/src/build.test.ts +1 -1
  118. package/src/build.ts +1 -6
  119. package/src/cli.harness.mjs +26 -46
  120. package/src/cli.test.ts +32 -40
  121. package/src/cli.ts +14 -54
  122. package/src/folder-venue-rule.test.ts +1 -1
  123. package/src/init.test.ts +6 -7
  124. package/src/init.ts +3 -22
  125. package/src/lint-fix.test.ts +10 -4
  126. package/src/new-paper.harness.mjs +9 -28
  127. package/src/new-paper.test.ts +6 -6
  128. package/src/new-paper.ts +11 -30
  129. package/src/paper-files.test.ts +5 -4
  130. package/src/paper-files.ts +4 -4
  131. package/src/paper-prose.test.ts +49 -0
  132. package/src/paper-prose.ts +67 -0
  133. package/src/parse-args.test.ts +0 -1
  134. package/src/structure.harness.ts +5 -11
  135. package/src/structure.ts +4 -3
  136. package/src/types.ts +0 -2
  137. package/fixtures/real-markdown-paper/baseline.json +0 -22
  138. package/fixtures/real-markdown-paper/baseline.ts +0 -70
  139. package/skills/paper-pipeline/PIPELINE-MAP.md +0 -381
  140. package/skills/paper-pipeline/pipeline-firing.baseline.json +0 -270
  141. package/skills/paper-pipeline/references/sweep-2026-08-09-ideal-pipeline.md +0 -585
  142. package/skills/paper-pipeline/scripts/artifact-coverage.harness.mjs +0 -492
  143. package/skills/paper-pipeline/scripts/artifact-coverage.mjs +0 -397
  144. package/skills/paper-pipeline/scripts/artifact-coverage.test.ts +0 -86
  145. package/skills/paper-pipeline/scripts/check-provenance.mjs +0 -184
  146. package/skills/paper-pipeline/scripts/population-map.harness.mjs +0 -135
  147. package/skills/paper-pipeline/scripts/population-map.mjs +0 -188
  148. package/skills/paper-pipeline/scripts/population-map.test.mjs +0 -154
  149. package/skills/paper-pipeline/scripts/provenance.harness.mjs +0 -266
  150. package/skills/paper-pipeline/scripts/round-diff.harness.ts +0 -907
  151. package/skills/paper-pipeline/scripts/round-diff.mjs +0 -583
  152. package/skills/paper-pipeline/scripts/round-diff.test.mjs +0 -159
  153. package/skills/tighten-paper/__snapshots__/structure.test.ts.snap +0 -133
  154. package/skills/tighten-paper/structure.mjs +0 -364
  155. package/skills/tighten-paper/structure.test.ts +0 -117
  156. package/templates/paper/paper.md +0 -4
@@ -42,6 +42,10 @@ const { PROGRAMS } = await import(join(HERE, "doctor.ts"));
42
42
  // loads vigiles without recording a check is reported as having verified nothing.
43
43
  const check = createChecker();
44
44
 
45
+ /** A paper.tex whose whole body is `body`. */
46
+ const tex = (body) =>
47
+ `\\documentclass{article}\n\\begin{document}\n${body}\n\\end{document}\n`;
48
+
45
49
  /** Runs the utility with output captured — quieter and faster than spawning a process. */
46
50
  async function cli(args, cwd) {
47
51
  // 🔴 THE STREAMS ARE SPLIT, AND THAT IS LOAD-BEARING. As long as the harness dumped log and
@@ -129,11 +133,11 @@ check(
129
133
  const ruleBlocks = (c) => c.filter((b) => Array.isArray(b.files));
130
134
  check(
131
135
  "without a LaTeX language the config still gets built — a corpus with no .tex is not a reason to refuse",
132
- Array.isArray(cfg) && ruleBlocks(cfg).length === 4,
136
+ Array.isArray(cfg) && ruleBlocks(cfg).length === 3,
133
137
  );
134
138
  check(
135
139
  "with a LaTeX language two blocks are added: paper.tex, and the files it includes",
136
- ruleBlocks(buildConfig({}, {})).length === 6,
140
+ ruleBlocks(buildConfig({}, {})).length === 5,
137
141
  );
138
142
  check(
139
143
  "🔴 the project's paper TEMPLATE directory is ignored — flat config does not skip dot-directories",
@@ -178,7 +182,7 @@ check(
178
182
  join(paper, "versions", "2026-07-22-submitted.pdf"),
179
183
  "x".repeat(100),
180
184
  );
181
- writeFileSync(join(paper, "paper.md"), "# Intro\n\nRQ1: does it hold?\n");
185
+ writeFileSync(join(paper, "paper.tex"), tex("RQ1: does it hold?"));
182
186
  writeFileSync(
183
187
  join(paper, "PIPELINE-STATUS.md"),
184
188
  `---\nstages:\n - stage: submitted\n date: 2026-07-22\n pdf: versions/2026-07-22-submitted.pdf\n bytes: 100\n source: versions/s.tex\n sourceBytes: 4\n---\n# S\n\n| id | note |\n|---|---|\n| cites | bib-authors run |\n`,
@@ -703,7 +707,7 @@ check(
703
707
  const status = (bytes) =>
704
708
  `---\nstages:\n - stage: submitted\n date: 2026-07-22\n pdf: versions/2026-07-22-submitted.pdf\n bytes: ${bytes}\n source: versions/s.tex\n sourceBytes: 4\n---\n# S\n\n| id | note |\n|---|---|\n| cites | bib-authors run |\n`;
705
709
  writeFileSync(join(paper, "versions", "s.tex"), "abcd");
706
- writeFileSync(join(paper, "paper.md"), "# Intro\n\nRQ1: does it hold?\n");
710
+ writeFileSync(join(paper, "paper.tex"), tex("RQ1: does it hold?"));
707
711
 
708
712
  writeFileSync(join(paper, "PIPELINE-STATUS.md"), status(100));
709
713
  const clean = await cli(["lint", "papers"], root);
@@ -788,7 +792,7 @@ check(
788
792
  join(paper, "versions", "2026-07-22-submitted.pdf"),
789
793
  "x".repeat(100),
790
794
  );
791
- writeFileSync(join(paper, "paper.md"), "# Intro\n\nRQ1: does it hold?\n");
795
+ writeFileSync(join(paper, "paper.tex"), tex("RQ1: does it hold?"));
792
796
  const status = (bytes) =>
793
797
  `---\nstages:\n - stage: submitted\n date: 2026-07-22\n pdf: versions/2026-07-22-submitted.pdf\n bytes: ${bytes}\n source: versions/s.tex\n sourceBytes: 4\n---\n# S\n\n| id | note |\n|---|---|\n| cites | bib-authors run |\n`;
794
798
  writeFileSync(join(paper, "PIPELINE-STATUS.md"), status(100));
@@ -897,8 +901,8 @@ check(
897
901
  );
898
902
  // Three § signs — `paper/section-word`, warn level and only warn.
899
903
  writeFileSync(
900
- join(paper, "paper.md"),
901
- "# Intro\n\nRQ1: does it hold?\n\nSee \u00a7 5 and \u00a7 6 and \u00a7 7.\n",
904
+ join(paper, "paper.tex"),
905
+ tex("RQ1: does it hold?\n\nSee \u00a7 5 and \u00a7 6 and \u00a7 7."),
902
906
  );
903
907
  writeFileSync(
904
908
  join(paper, "PIPELINE-STATUS.md"),
@@ -1040,7 +1044,7 @@ check(
1040
1044
  const paper = join(tree, "papers", "p");
1041
1045
  mkdirSync(paper, { recursive: true });
1042
1046
  // Two findings of different kinds: a structural one (no scorecard) and a rule one (`§`).
1043
- writeFileSync(join(paper, "paper.md"), "# T\n\nSee § 3 and §4.\n");
1047
+ writeFileSync(join(paper, "paper.tex"), tex("See § 3 and §4."));
1044
1048
 
1045
1049
  const inside = await cli(["lint", "--json", "papers"], tree);
1046
1050
  const outside = await cli(
@@ -1273,22 +1277,10 @@ console.log(
1273
1277
 
1274
1278
  // ── the new flags parse, and none of them becomes the directory argument ─────────────────
1275
1279
  {
1276
- const a = parseArgs([
1277
- "init",
1278
- "--yes",
1279
- "--no-hooks",
1280
- "--paper",
1281
- "demo",
1282
- "--format",
1283
- "md",
1284
- ]);
1280
+ const a = parseArgs(["init", "--yes", "--no-hooks", "--paper", "demo"]);
1285
1281
  check(
1286
1282
  "init flags parse into fields, not into paths",
1287
- a.yes &&
1288
- a.noHooks &&
1289
- a.paper === "demo" &&
1290
- a.format === "md" &&
1291
- a.paths.length === 0,
1283
+ a.yes && a.noHooks && a.paper === "demo" && a.paths.length === 0,
1292
1284
  );
1293
1285
  check("-y is --yes", parseArgs(["init", "-y"]).yes === true);
1294
1286
  check(
@@ -1300,11 +1292,6 @@ console.log(
1300
1292
  "🔴 --hooks=local is NOT implemented, and it is refused by name rather than read as a path",
1301
1293
  r.code === 2 && /--hooks=local is not implemented/.test(r.out),
1302
1294
  );
1303
- const f = await cli(["init", "--format", "pdf"]);
1304
- check(
1305
- "an unknown --format is refused before anything is written",
1306
- f.code === 2 && /--format must be one of tex, md/.test(f.out),
1307
- );
1308
1295
  }
1309
1296
 
1310
1297
  // ── init wires the hooks, and a human can say no ─────────────────────────────────────────
@@ -1315,7 +1302,7 @@ console.log(
1315
1302
  const mk = (name) => {
1316
1303
  const dir = join(root, name);
1317
1304
  mkdirSync(join(dir, "papers", "p1"), { recursive: true });
1318
- writeFileSync(join(dir, "papers", "p1", "paper.md"), "# P\n");
1305
+ writeFileSync(join(dir, "papers", "p1", "paper.tex"), "% P\n");
1319
1306
  writeFileSync(
1320
1307
  join(dir, "papers", "p1", "PIPELINE-STATUS.md"),
1321
1308
  "---\n---\n",
@@ -1426,19 +1413,17 @@ console.log(
1426
1413
  ask: async (q) => {
1427
1414
  asked.push(q);
1428
1415
  if (/create a first paper/.test(q)) return "first";
1429
- if (/format:/.test(q)) return "md";
1430
1416
  return "";
1431
1417
  },
1432
- createPaper: async (papersRoot, name, format) => {
1433
- made.push({ papersRoot, name, format });
1418
+ createPaper: async (papersRoot, name) => {
1419
+ made.push({ papersRoot, name });
1434
1420
  return 0;
1435
1421
  },
1436
1422
  });
1437
1423
  check(
1438
- "a human with no paper is offered one, and the answer and format reach `paperlint new`'s routine",
1424
+ "a human with no paper is offered one, and the answer reaches `paperlint new`'s routine",
1439
1425
  made.length === 1 &&
1440
1426
  made[0].name === "first" &&
1441
- made[0].format === "md" &&
1442
1427
  made[0].papersRoot === join(dir, "papers"),
1443
1428
  );
1444
1429
  }
@@ -1466,22 +1451,17 @@ console.log(
1466
1451
  log: () => {},
1467
1452
  interactive: false,
1468
1453
  paper: "given",
1469
- createPaper: async (papersRoot, name, format) => (
1470
- made.push({ name, format }),
1471
- 0
1472
- ),
1454
+ createPaper: async (papersRoot, name) => (made.push({ name }), 0),
1473
1455
  });
1474
1456
  check(
1475
- "--paper creates it without a human, in the default format",
1476
- made.length === 1 &&
1477
- made[0].name === "given" &&
1478
- made[0].format === "tex",
1457
+ "--paper creates it without a human",
1458
+ made.length === 1 && made[0].name === "given",
1479
1459
  );
1480
1460
  }
1481
1461
  {
1482
1462
  const dir = bare("has-one");
1483
1463
  mkdirSync(join(dir, "papers", "p1"), { recursive: true });
1484
- writeFileSync(join(dir, "papers", "p1", "paper.md"), "# P\n");
1464
+ writeFileSync(join(dir, "papers", "p1", "paper.tex"), "% P\n");
1485
1465
  const asked = [];
1486
1466
  await init(dir, {
1487
1467
  ...base,
@@ -1569,7 +1549,7 @@ console.log(
1569
1549
  `{"${PAPERS_DIR_FIELD}":"papers"}\n`,
1570
1550
  );
1571
1551
  mkdirSync(join(root, "papers", "p1"), { recursive: true });
1572
- writeFileSync(join(root, "papers", "p1", "paper.md"), "# P\n");
1552
+ writeFileSync(join(root, "papers", "p1", "paper.tex"), "% P\n");
1573
1553
  writeFileSync(
1574
1554
  join(root, "papers", "p1", "PIPELINE-STATUS.md"),
1575
1555
  "---\n---\n",
@@ -1591,7 +1571,7 @@ console.log(
1591
1571
  join(root, "papers", "p2", "PIPELINE-STATUS.md"),
1592
1572
  readFileSync(join(root, "papers", ".template", "PIPELINE-STATUS.md")),
1593
1573
  );
1594
- writeFileSync(join(root, "papers", "p2", "paper.md"), "# P\n");
1574
+ writeFileSync(join(root, "papers", "p2", "paper.tex"), "% P\n");
1595
1575
  const r2 = await cli(["lint"], root);
1596
1576
  check(
1597
1577
  "and the same scorecard in a real paper folder fails — the ignore is scoped, not a blind spot",
@@ -1625,11 +1605,11 @@ console.log(
1625
1605
  /names no venue preset yet/.test(r.out) &&
1626
1606
  /\(0 errors, 1 warning\)/.test(r.out),
1627
1607
  );
1628
- const again = await cli(["new", "demo", "--format", "md"], root);
1608
+ const again = await cli(["new", "demo"], root);
1629
1609
  check(
1630
1610
  "a second run on the same folder adds nothing — the tex source already stands",
1631
1611
  again.code === 0 &&
1632
- !existsSync(join(root, "writing", "demo", "paper.md")) &&
1612
+ !/\+ /.test(again.out) &&
1633
1613
  /kept — never overwritten/.test(again.out),
1634
1614
  );
1635
1615
  const bad = await cli(["new", "Demo"], root);
package/src/cli.test.ts CHANGED
@@ -6,7 +6,7 @@
6
6
  */
7
7
  import assert from "node:assert/strict";
8
8
  import { spawn } from "node:child_process";
9
- import { existsSync } from "node:fs";
9
+ import { existsSync, readdirSync } from "node:fs";
10
10
  import { createServer } from "node:http";
11
11
  import { dirname, join } from "node:path";
12
12
  import { test } from "vitest";
@@ -50,14 +50,14 @@ test("parseArgs: the boolean flags of build and toolchain, and a value flag with
50
50
  [
51
51
  parseArgs(["build", "--all", "--dry-run", "papers/p"]),
52
52
  parseArgs(["toolchain", "--check"]),
53
- parseArgs(["new", "x", "--format"]),
53
+ parseArgs(["new", "x", "--paper"]),
54
54
  parseArgs(["new", "x", "--venue="]),
55
55
  parseArgs(["new", "x", "--kind", "short", "--max-warnings=3"]),
56
56
  ],
57
57
  [
58
58
  { ...base, cmd: "build", all: true, dryRun: true, paths: ["papers/p"] },
59
59
  { ...base, cmd: "toolchain", check: true },
60
- { ...base, cmd: "new", paths: ["x"], missingValue: "--format" },
60
+ { ...base, cmd: "new", paths: ["x"], missingValue: "--paper" },
61
61
  { ...base, cmd: "new", paths: ["x"], missingValue: "--venue" },
62
62
  { ...base, cmd: "new", paths: ["x"], kind: "short", maxWarnings: 3 },
63
63
  ],
@@ -142,20 +142,18 @@ test("build refuses with no target, and --all over no papers", async () => {
142
142
  );
143
143
  });
144
144
 
145
- test("new refuses an unknown format, a broken config, and a config that names no papers directory", async () => {
145
+ test("new refuses a broken config, and a config that names no papers directory", async () => {
146
146
  const broken = join(root, "new-broken");
147
147
  writeTree(broken, { "paperlint.json": "{ not json" });
148
148
  const none = join(root, "new-none");
149
149
  writeTree(none, { "paperlint.json": JSON.stringify({ papersDir: [] }) });
150
150
  const results = [
151
- await cli(["new", "p", "--format", "docx"], root),
152
- await cli(["new", "p", "--format", "md"], broken),
153
- await cli(["new", "p", "--format", "md"], none),
151
+ await cli(["new", "p"], broken),
152
+ await cli(["new", "p"], none),
154
153
  ];
155
154
  assert.deepEqual(
156
155
  results.map((r) => ({ code: r.code, err: r.err.split(":")[0] })),
157
156
  [
158
- { code: 2, err: "--format must be one of tex, md — got `docx`" },
159
157
  { code: 2, err: join(broken, "paperlint.json is not valid JSON") },
160
158
  {
161
159
  code: 2,
@@ -170,7 +168,7 @@ test("new with several papers directories says which one it used", async () => {
170
168
  writeTree(dir, {
171
169
  "paperlint.json": JSON.stringify({ papersDir: ["a", "b"] }),
172
170
  });
173
- const r = await cli(["new", "p", "--format", "md", "--yes"], dir);
171
+ const r = await cli(["new", "p", "--yes"], dir);
174
172
  assert.equal(
175
173
  r.out.split("\n")[0],
176
174
  "several papers directories are declared — using the first: a",
@@ -274,20 +272,20 @@ async function withoutTex<T>(dir: string, fn: () => Promise<T>): Promise<T> {
274
272
  const noTexDir = join(root, "build-notex");
275
273
  writeTree(noTexDir, {
276
274
  "package.json": "{}",
277
- "papers/md/paper.md": "# P\n\nx\n",
275
+ "papers/nosource/notes.txt": "x\n",
278
276
  "papers/tex/paper.tex":
279
277
  "\\documentclass{article}\\begin{document}x\\end{document}\n",
280
278
  });
281
279
  const venues = texInputsDir();
282
280
 
283
- test("build: a markdown-only paper is refused", async () => {
284
- const md = await withoutTex(noTexDir, () =>
285
- cli(["build", "papers/md"], noTexDir),
281
+ test("build: a paper with no paper.tex is refused, naming paper.tex", async () => {
282
+ const none = await withoutTex(noTexDir, () =>
283
+ cli(["build", "papers/nosource"], noTexDir),
286
284
  );
287
- assert.deepEqual(md, {
285
+ assert.deepEqual(none, {
288
286
  code: 1,
289
287
  out: [
290
- "papers/md",
288
+ "papers/nosource",
291
289
  ` inputs: TEXINPUTS += the paper's directory, then ${venues}`,
292
290
  " compile: refused — no paper.tex; paperlint compiles LaTeX, and this paper has none",
293
291
  " measure: skipped — nothing is compiled",
@@ -295,7 +293,7 @@ test("build: a markdown-only paper is refused", async () => {
295
293
  " ✗ nothing to compile: no paper.tex",
296
294
  ].join("\n"),
297
295
  err:
298
- "\nNo paper.tex in: papers/md.\n" +
296
+ "\nNo paper.tex in: papers/nosource.\n" +
299
297
  'This is NOT "nothing to build" — paperlint compiles LaTeX, and these papers have no LaTeX source.\n' +
300
298
  "Write the paper in paper.tex; `paperlint new <name>` creates one.",
301
299
  });
@@ -399,7 +397,7 @@ test("build --dry-run on a paper with a venue plans for the preset's packages",
399
397
 
400
398
  test("a paper's paperlint.json that does not parse stops the lint with its path", () => {
401
399
  const dir = writeTree(join(root, "rules-broken"), {
402
- "p/paper.md": "# P\n",
400
+ "p/paper.tex": "% P\n",
403
401
  "p/paperlint.json": "{ nope",
404
402
  });
405
403
  const r = paperRuleBlocks([join(dir, "p")]);
@@ -444,15 +442,12 @@ test("silentOptionalRules: a rule no block names for any paper is silent — a g
444
442
  );
445
443
  });
446
444
 
447
- test("init with no path and --format md sets up the current directory", async () => {
445
+ test("init with no path sets up the current directory, and its first paper is paper.tex", async () => {
448
446
  const dir = writeTree(join(root, "init-here"), { "package.json": "{}" });
449
- const r = await cli(
450
- ["init", "--yes", "--no-hooks", "--paper", "first", "--format", "md"],
451
- dir,
452
- );
453
- assert.equal(
454
- existsSync(join(dir, "papers", "first", "paper.md")),
455
- true,
447
+ const r = await cli(["init", "--yes", "--no-hooks", "--paper", "first"], dir);
448
+ assert.deepEqual(
449
+ readdirSync(join(dir, "papers", "first")).sort(),
450
+ ["PIPELINE-STATUS.md", "paper.tex", "paperlint.json"],
456
451
  r.out + r.err,
457
452
  );
458
453
  });
@@ -488,12 +483,9 @@ test("lint: a named directory with nothing paperlint lints is named in full when
488
483
  test("new from inside the paper it names adds the missing files and names the paper in full", async () => {
489
484
  const dir = writeTree(join(root, "new-inside"), {
490
485
  "paperlint.json": JSON.stringify({ papersDir: "papers" }),
491
- "papers/p/paper.md": "# P\n",
486
+ "papers/p/paper.tex": "% P\n",
492
487
  });
493
- const r = await cli(
494
- ["new", "p", "--format", "md", "--yes"],
495
- join(dir, "papers", "p"),
496
- );
488
+ const r = await cli(["new", "p", "--yes"], join(dir, "papers", "p"));
497
489
  assert.match(
498
490
  r.out,
499
491
  new RegExp(
@@ -507,7 +499,7 @@ test("new with several directories, run from the first, names it in full", async
507
499
  "paperlint.json": JSON.stringify({ papersDir: ["a", "b"] }),
508
500
  "a/.keep": "",
509
501
  });
510
- const r = await cli(["new", "p", "--format", "md", "--yes"], join(dir, "a"));
502
+ const r = await cli(["new", "p", "--yes"], join(dir, "a"));
511
503
  assert.equal(
512
504
  r.out.split("\n")[0],
513
505
  `several papers directories are declared — using the first: ${join(dir, "a")}`,
@@ -519,7 +511,7 @@ test("lint: an ESLint failure that is not an empty set is not swallowed", async
519
511
  "paperlint.json": JSON.stringify({
520
512
  rules: { "paper/stages": ["error", { x: 1 }] },
521
513
  }),
522
- "papers/p/paper.md": "# P\n",
514
+ "papers/p/paper.tex": "% P\n",
523
515
  "papers/p/PIPELINE-STATUS.md": "---\nstages: []\n---\n# S\n",
524
516
  });
525
517
  await assert.rejects(cli(["lint"], dir), {
@@ -582,11 +574,11 @@ function newAtTerminal(cwd: string, answers: Record<string, string>) {
582
574
  });
583
575
  }
584
576
 
585
- test("new at a terminal asks for the format and the venue; the answers pick them", async () => {
586
- const md = writeTree(join(root, "new-tty-md"), { "package.json": "{}" });
587
- const r = await newAtTerminal(md, { "format:": "md\n", "venue:": "none\n" });
577
+ test("new at a terminal asks for the venue; the answer picks it", async () => {
578
+ const dir = writeTree(join(root, "new-tty"), { "package.json": "{}" });
579
+ const r = await newAtTerminal(dir, { "venue:": "none\n" });
588
580
  assert.equal(r.status, 0, r.out);
589
- assert.equal(existsSync(join(md, "papers", "first", "paper.md")), true);
581
+ assert.equal(existsSync(join(dir, "papers", "first", "paper.tex")), true);
590
582
  });
591
583
 
592
584
  test("chooseVenue: a question that fails (the stream ended) takes the default, no venue", async () => {
@@ -625,9 +617,9 @@ test("init's TeX Live install runs the toolchain over the project's papers, neve
625
617
  assert.equal(typeof here.installed(), "boolean");
626
618
  });
627
619
 
628
- test("lint with its LaTeX language missing from the install still lints the markdown papers", () => {
620
+ test("lint with its LaTeX language missing from the install still lints the scorecards", () => {
629
621
  const dir = writeTree(join(root, "lint-no-tex-language"), {
630
- "papers/p/paper.md": "# P\n\nSome prose.\n",
622
+ "papers/p/PIPELINE-STATUS.md": "---\nstages: []\n---\n# S\n",
631
623
  "no-latex.mjs":
632
624
  "import { register } from 'node:module';\n" +
633
625
  "register('data:text/javascript,' + encodeURIComponent(\"export async function resolve(s, c, next) { if (s === '#eslint-rules/latex-language') throw Object.assign(new Error('gone'), { code: 'ERR_MODULE_NOT_FOUND' }); return next(s, c); }\"));\n",
@@ -637,9 +629,9 @@ test("lint with its LaTeX language missing from the install still lints the mark
637
629
  nodeArgs: ["--import", join(dir, "no-latex.mjs")],
638
630
  });
639
631
  const whole = runNode(BIN, ["lint", "papers"], { cwd: dir });
640
- // A markdown paper needs no LaTeX language: the same report, the same code.
632
+ // A scorecard needs no LaTeX language: the same report, the same code.
641
633
  assert.deepEqual(without, whole);
642
- assert.match(whole.stdout, /missing `PIPELINE-STATUS\.md`/);
634
+ assert.match(whole.stdout, /missing all of `paper\.tex`/);
643
635
  });
644
636
 
645
637
  test("`paperlint hook` with no name: exit 2, and the usage names an example", async () => {
package/src/cli.ts CHANGED
@@ -132,15 +132,7 @@ import {
132
132
  init,
133
133
  processInteractivity,
134
134
  } from "./init.ts";
135
- import {
136
- DEFAULT_FORMAT,
137
- FORMATS,
138
- isFormat,
139
- newPaper,
140
- reportNewPaper,
141
- type PaperFormat,
142
- type VenueSetting,
143
- } from "./new-paper.ts";
135
+ import { newPaper, reportNewPaper, type VenueSetting } from "./new-paper.ts";
144
136
  // The one source for the consumer's config key lives in lib/ (the ESLint rules and the skill scripts
145
137
  // import it too). It is imported compiled, from dist/, the one path that resolves the same from src/
146
138
  // and from dist/ — see CONTRIBUTING.md.
@@ -189,7 +181,7 @@ const USAGE = `paperlint — machine-checkable gates for a paper kept in git
189
181
  npx paperlint init [dir] set the project up: detect the papers directory, declare it
190
182
  in package.json, link the skills, wire the hooks into
191
183
  .claude/settings.json, offer the CI step, report what is missing
192
- npx paperlint new <name> [--venue <preset>] [--kind <kind>] [--format tex|md]
184
+ npx paperlint new <name> [--venue <preset>] [--kind <kind>]
193
185
  create <papers>/<name>/ from the template; never overwrites,
194
186
  on an existing folder adds only the missing files, then lints it.
195
187
  A <name> that names a venue (aisec-2026) is refused: name the
@@ -225,7 +217,6 @@ init:
225
217
  or with CI set, nothing is asked either
226
218
  --no-hooks do not wire the hooks (the default without a human is to wire them)
227
219
  --paper <name> create this paper too (without a human, the only way init creates one)
228
- --format tex|md the new paper's source format; default tex
229
220
 
230
221
  new:
231
222
  --venue <preset> the venue preset, written as "extends" into the paper's paperlint.json:
@@ -234,7 +225,6 @@ new:
234
225
  On a terminal without --venue, new asks; "none" leaves it unset
235
226
  --kind <kind> the paper's kind at that venue (its page limit), e.g. short — one of the
236
227
  preset's kinds; needs --venue
237
- --format tex|md the paper's source format; default tex
238
228
  --allow-venue-name create the folder even though its name names a venue (a whole word such as
239
229
  "realm" can be the work's own). Without it such a name is refused: a rejected
240
230
  paper moves to another venue and keeps its folder
@@ -399,8 +389,8 @@ export function buildConfig(
399
389
  },
400
390
  {
401
391
  files: ["**/PIPELINE-STATUS.md"],
402
- // Every paper folder has one, whatever its source format: the rules about the folder itself
403
- // (its stages, its frozen sources, its name) are judged here, once per paper.
392
+ // Every paper folder has one: the rules about the folder itself (its stages, its frozen
393
+ // sources, its name) are judged here, once per paper.
404
394
  plugins: {
405
395
  markdown,
406
396
  paper: {
@@ -419,12 +409,6 @@ export function buildConfig(
419
409
  ...CYCLE_RULE_LEVELS,
420
410
  },
421
411
  },
422
- {
423
- files: ["**/paper.md", "**/draft.md"],
424
- plugins: { markdown, paper: { rules: paperRules } },
425
- ...md,
426
- rules: prose,
427
- },
428
412
  {
429
413
  files: ["**/reviews/*.md"],
430
414
  plugins: { markdown, review: reviewFrontmatter },
@@ -585,8 +569,7 @@ export interface RulePlugin {
585
569
  * file knows every rule id. `@eslint/markdown` is a dependency's plugin and is left out. The LaTeX
586
570
  * language rides on `tex` when `texLanguage` is given, and that is how a consumer registers it: a
587
571
  * second `tex` plugin of its own is refused by ESLint ("Cannot redefine plugin "tex"", measured
588
- * with and without the language here). Without it, `tex` carries rules only — for a config that
589
- * lints markdown papers alone.
572
+ * with and without the language here). Without it, `tex` carries rules only.
590
573
  */
591
574
  export function rulePlugins(texLanguage?: unknown): Record<string, RulePlugin> {
592
575
  const out: Record<string, { rules: Record<string, unknown> }> = {};
@@ -888,7 +871,6 @@ export function parseArgs(argv: readonly string[]): Args {
888
871
  noHooks: false,
889
872
  allowVenueName: false,
890
873
  paper: null,
891
- format: null,
892
874
  venue: null,
893
875
  kind: null,
894
876
  pdf: null,
@@ -987,10 +969,9 @@ const SWITCHES: ReadonlyMap<
987
969
  /** The flags whose value is one string field of `Args`. */
988
970
  const STRING_FLAGS: ReadonlyMap<
989
971
  string,
990
- "paper" | "format" | "venue" | "kind" | "pdf" | "abstract"
972
+ "paper" | "venue" | "kind" | "pdf" | "abstract"
991
973
  > = new Map([
992
974
  ["--paper", "paper"],
993
- ["--format", "format"],
994
975
  ["--venue", "venue"],
995
976
  ["--kind", "kind"],
996
977
  ["--pdf", "pdf"],
@@ -1184,7 +1165,6 @@ export function runHook(
1184
1165
  export async function createPaperAt(
1185
1166
  papersRoot: string,
1186
1167
  name: string,
1187
- format: PaperFormat,
1188
1168
  {
1189
1169
  log,
1190
1170
  err,
@@ -1201,7 +1181,7 @@ export async function createPaperAt(
1201
1181
  allowVenueName?: boolean;
1202
1182
  },
1203
1183
  ): Promise<number> {
1204
- const result = newPaper(papersRoot, name, format, {
1184
+ const result = newPaper(papersRoot, name, {
1205
1185
  venue,
1206
1186
  venues: allowVenueName ? [] : shippedVenueNames(PRESET_DEPS),
1207
1187
  });
@@ -1366,25 +1346,10 @@ async function runNew(
1366
1346
  const [name, ...extra] = a.paths;
1367
1347
  if (!name || extra.length > 0) {
1368
1348
  err(
1369
- `\`new\` takes exactly one paper name: \`paperlint new my-paper [--venue <preset>] [--kind <kind>] [--format tex|md]\``,
1349
+ `\`new\` takes exactly one paper name: \`paperlint new my-paper [--venue <preset>] [--kind <kind>]\``,
1370
1350
  );
1371
1351
  return 2;
1372
1352
  }
1373
- let format: PaperFormat = DEFAULT_FORMAT;
1374
- if (a.format !== null) {
1375
- if (!isFormat(a.format)) {
1376
- err(
1377
- `--format must be one of ${FORMATS.join(", ")} — got \`${a.format}\``,
1378
- );
1379
- return 2;
1380
- }
1381
- format = a.format;
1382
- } else if (processInteractivity(a.yes).interactive) {
1383
- const f = (
1384
- await askOrDefault(ask, `format: tex / md [${DEFAULT_FORMAT}] `)
1385
- )?.trim();
1386
- if (isFormat(f)) format = f;
1387
- }
1388
1353
  const cfg = readConfig({ ...a, json: false }, { log: () => {}, err, cwd });
1389
1354
  if (cfg.code !== undefined) return cfg.code;
1390
1355
  const roots = papersRoots(cfg);
@@ -1417,7 +1382,7 @@ async function runNew(
1417
1382
  ask,
1418
1383
  });
1419
1384
  if (!venue.ok) return (err(venue.error), 2);
1420
- return createPaperAt(papersRoot, name, format, {
1385
+ return createPaperAt(papersRoot, name, {
1421
1386
  log,
1422
1387
  err,
1423
1388
  cwd,
@@ -1668,10 +1633,6 @@ async function runInit(
1668
1633
  );
1669
1634
  return 2;
1670
1635
  }
1671
- if (a.format !== null && !isFormat(a.format)) {
1672
- err(`--format must be one of ${FORMATS.join(", ")} — got \`${a.format}\``);
1673
- return 2;
1674
- }
1675
1636
  return await init(a.paths[0] ?? ".", {
1676
1637
  log,
1677
1638
  err,
@@ -1679,9 +1640,8 @@ async function runInit(
1679
1640
  yes: a.yes,
1680
1641
  hooks: !a.noHooks,
1681
1642
  paper: a.paper,
1682
- format: isFormat(a.format) ? a.format : null,
1683
- createPaper: (papersRoot, name, format) =>
1684
- createPaperAt(papersRoot, name, format, { log, err, cwd }),
1643
+ createPaper: (papersRoot, name) =>
1644
+ createPaperAt(papersRoot, name, { log, err, cwd }),
1685
1645
  venues: shippedVenueNames(PRESET_DEPS),
1686
1646
  tex: initTexLive(a, { log, err, cwd }),
1687
1647
  resolveCliPapers: (root: string): string | null =>
@@ -1803,8 +1763,8 @@ export async function run(
1803
1763
  };
1804
1764
 
1805
1765
  // Loaded lazily because only `lint` needs it. If the module cannot be loaded (a broken install:
1806
- // the file missing from the package), lint still runs over the markdown papers rather than
1807
- // failing outright — the LaTeX rules simply have no language to run in.
1766
+ // the file missing from the package), lint still runs over the scorecards and the review notes
1767
+ // rather than failing outright — the LaTeX rules simply have no language to run in.
1808
1768
  let texLanguage: unknown = null;
1809
1769
  try {
1810
1770
  ({ texLanguage } = await import("#eslint-rules/latex-language"));
@@ -1870,7 +1830,7 @@ export async function run(
1870
1830
  // is not invoked — and, not being invoked, it physically cannot report that.
1871
1831
  if (results.length === 0) {
1872
1832
  err(
1873
- `nothing was linted under ${paths.map((x) => shown(cwd, x)).join(", ")} — no PIPELINE-STATUS.md, paper.md/tex or reviews/ found there. A clean report over zero files is not a clean report.`,
1833
+ `nothing was linted under ${paths.map((x) => shown(cwd, x)).join(", ")} — no PIPELINE-STATUS.md, paper.tex or reviews/ found there. A clean report over zero files is not a clean report.`,
1874
1834
  );
1875
1835
  return 1;
1876
1836
  }
@@ -160,7 +160,7 @@ describe("paper/folder-venue-leftover — silent", () => {
160
160
  });
161
161
 
162
162
  it("judged once per paper, on PIPELINE-STATUS.md only", () => {
163
- expect(lint("aisec-2026", AIDC, { file: "paper.md" })).toEqual([]);
163
+ expect(lint("aisec-2026", AIDC, { file: "paper.tex" })).toEqual([]);
164
164
  });
165
165
 
166
166
  it("a paper on a bare template family declares no venue: a venue in its folder may be its own", () => {
package/src/init.test.ts CHANGED
@@ -55,8 +55,8 @@ function project(): string {
55
55
  "paperlint.json": JSON.stringify({ papersDir: "papers" }),
56
56
  "papers/a/PIPELINE-STATUS.md": "---\nstages: []\n---\n",
57
57
  "eslint-rules/fixtures/x/PIPELINE-STATUS.md": "---\nstages: []\n---\n",
58
- "eslint-rules/fixtures/y/paper.md": "# y\n",
59
- "drafts/z/paper.md": "# z\n",
58
+ "eslint-rules/fixtures/y/paper.tex": "% y\n",
59
+ "drafts/z/paper.tex": "% z\n",
60
60
  };
61
61
  for (const [p, text] of Object.entries(files)) {
62
62
  mkdirSync(join(root, p, ".."), { recursive: true });
@@ -291,7 +291,7 @@ describe("paperlint init — a paperlint.json it cannot use stops it before anyt
291
291
  for (const json of ["null\n", "[1]\n"]) {
292
292
  const root = tree({
293
293
  "paperlint.json": json,
294
- "drafts/a/paper.md": "# a\n",
294
+ "drafts/a/paper.tex": "% a\n",
295
295
  });
296
296
  const decl = await declarePapers(root, () =>
297
297
  choosePapers(root, { interactive: false }),
@@ -308,7 +308,7 @@ describe("paperlint init — a paperlint.json it cannot use stops it before anyt
308
308
  it("a file without a trailing newline keeps having none", async () => {
309
309
  const root = tree({
310
310
  "paperlint.json": '{"x":1}',
311
- "drafts/a/paper.md": "# a\n",
311
+ "drafts/a/paper.tex": "% a\n",
312
312
  });
313
313
  const decl = await declarePapers(root, () =>
314
314
  choosePapers(root, { interactive: false }),
@@ -322,7 +322,7 @@ describe("paperlint init — a paperlint.json it cannot use stops it before anyt
322
322
 
323
323
  describe("paperlint init — how several candidate directories were decided, said", () => {
324
324
  const several = () =>
325
- tree({ "drafts/a/paper.md": "# a\n", "writing/b/paper.md": "# b\n" });
325
+ tree({ "drafts/a/paper.tex": "% a\n", "writing/b/paper.tex": "% b\n" });
326
326
  const section = (out: string[]) =>
327
327
  out.slice(out.indexOf("papers directory") + 1, out.indexOf("settings") - 1);
328
328
 
@@ -530,7 +530,6 @@ describe("paperlint init — a first paper named after a venue", () => {
530
530
  createPaper: (_root, name) => Promise.resolve((made.push(name), 0)),
531
531
  });
532
532
  expect(made).toEqual([]);
533
- expect(asked.filter((q) => /format/.test(q))).toEqual([]);
534
533
  expect(r.out[r.out.indexOf("first paper") + 1]).toBe(
535
534
  ' ✗ no paper created — `aisec-2026` — the paper folder names a venue ("AISec"); venues change on resubmission, so name it after the work\n if the venue really belongs in the name, rerun `paperlint new` with --allow-venue-name',
536
535
  );
@@ -539,7 +538,7 @@ describe("paperlint init — a first paper named after a venue", () => {
539
538
 
540
539
  describe("paperlint init — paths are shown relative to where it was run", () => {
541
540
  it("a path that IS the working directory is shown whole, not as an empty string", async () => {
542
- const root = tree({ "papers/a/paper.md": "# a\n" });
541
+ const root = tree({ "papers/a/paper.tex": "% a\n" });
543
542
  const home = join(root, ".claude", "skills");
544
543
  mkdirSync(home, { recursive: true });
545
544
  const r = await initRun(root, {