paperlint 7.2.0 → 9.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 (474) hide show
  1. package/CLAUDE.md +9 -25
  2. package/CONTRIBUTING.md +5 -4
  3. package/README.md +4 -3
  4. package/dist/adapters/bibtex/index.d.ts +3 -0
  5. package/dist/adapters/bibtex/index.d.ts.map +1 -0
  6. package/dist/adapters/bibtex/index.js +103 -0
  7. package/dist/adapters/bibtex/index.js.map +1 -0
  8. package/dist/adapters/latex/filecontents.d.ts +33 -0
  9. package/dist/adapters/latex/filecontents.d.ts.map +1 -0
  10. package/dist/adapters/latex/filecontents.js +65 -0
  11. package/dist/adapters/latex/filecontents.js.map +1 -0
  12. package/dist/adapters/latex/includes.d.ts +3 -3
  13. package/dist/adapters/latex/includes.d.ts.map +1 -1
  14. package/dist/adapters/latex/includes.js +18 -10
  15. package/dist/adapters/latex/includes.js.map +1 -1
  16. package/dist/adapters/latex/index.d.ts.map +1 -1
  17. package/dist/adapters/latex/index.js +2 -0
  18. package/dist/adapters/latex/index.js.map +1 -1
  19. package/dist/adapters/latex/nodes.d.ts +12 -0
  20. package/dist/adapters/latex/nodes.d.ts.map +1 -1
  21. package/dist/adapters/latex/nodes.js +39 -0
  22. package/dist/adapters/latex/nodes.js.map +1 -1
  23. package/dist/adapters/latex/parse.d.ts +7 -1
  24. package/dist/adapters/latex/parse.d.ts.map +1 -1
  25. package/dist/adapters/latex/parse.js +55 -6
  26. package/dist/adapters/latex/parse.js.map +1 -1
  27. package/dist/adapters/memory/index.d.ts +3 -0
  28. package/dist/adapters/memory/index.d.ts.map +1 -1
  29. package/dist/adapters/memory/index.js +9 -0
  30. package/dist/adapters/memory/index.js.map +1 -1
  31. package/dist/adapters/references/index.d.ts.map +1 -1
  32. package/dist/adapters/references/index.js +27 -24
  33. package/dist/adapters/references/index.js.map +1 -1
  34. package/dist/adapters/sources-record/index.d.ts +9 -0
  35. package/dist/adapters/sources-record/index.d.ts.map +1 -0
  36. package/dist/adapters/sources-record/index.js +68 -0
  37. package/dist/adapters/sources-record/index.js.map +1 -0
  38. package/dist/adapters/tex-output/blg.d.ts +9 -0
  39. package/dist/adapters/tex-output/blg.d.ts.map +1 -0
  40. package/dist/adapters/tex-output/blg.js +20 -0
  41. package/dist/adapters/tex-output/blg.js.map +1 -0
  42. package/dist/adapters/tex-output/fls.d.ts +8 -0
  43. package/dist/adapters/tex-output/fls.d.ts.map +1 -0
  44. package/dist/adapters/tex-output/fls.js +16 -0
  45. package/dist/adapters/tex-output/fls.js.map +1 -0
  46. package/dist/adapters/tex-output/index.d.ts +7 -0
  47. package/dist/adapters/tex-output/index.d.ts.map +1 -0
  48. package/dist/adapters/tex-output/index.js +7 -0
  49. package/dist/adapters/tex-output/index.js.map +1 -0
  50. package/dist/bib-rules.d.ts +47 -0
  51. package/dist/bib-rules.d.ts.map +1 -0
  52. package/dist/bib-rules.js +127 -0
  53. package/dist/bib-rules.js.map +1 -0
  54. package/dist/build.d.ts +29 -1
  55. package/dist/build.d.ts.map +1 -1
  56. package/dist/build.js +41 -30
  57. package/dist/build.js.map +1 -1
  58. package/dist/claim-provenance.d.ts +2 -2
  59. package/dist/cli.d.ts +9 -5
  60. package/dist/cli.d.ts.map +1 -1
  61. package/dist/cli.js +43 -68
  62. package/dist/cli.js.map +1 -1
  63. package/dist/domain/paper-source.d.ts +1 -10
  64. package/dist/domain/paper-source.d.ts.map +1 -1
  65. package/dist/domain/paper-source.js +34 -24
  66. package/dist/domain/paper-source.js.map +1 -1
  67. package/dist/domain/paper-sources.d.ts +72 -0
  68. package/dist/domain/paper-sources.d.ts.map +1 -0
  69. package/dist/domain/paper-sources.js +18 -0
  70. package/dist/domain/paper-sources.js.map +1 -0
  71. package/dist/domain/sources-record.d.ts +102 -0
  72. package/dist/domain/sources-record.d.ts.map +1 -0
  73. package/dist/domain/sources-record.js +38 -0
  74. package/dist/domain/sources-record.js.map +1 -0
  75. package/dist/domain/tex-run.d.ts +25 -0
  76. package/dist/domain/tex-run.d.ts.map +1 -0
  77. package/dist/domain/tex-run.js +79 -0
  78. package/dist/domain/tex-run.js.map +1 -0
  79. package/dist/eslint-rules/latex-language.d.ts.map +1 -1
  80. package/dist/eslint-rules/latex-language.js +10 -13
  81. package/dist/eslint-rules/latex-language.js.map +1 -1
  82. package/dist/eslint-rules/paper-context.d.ts.map +1 -1
  83. package/dist/eslint-rules/paper-context.js +6 -5
  84. package/dist/eslint-rules/paper-context.js.map +1 -1
  85. package/dist/eslint-rules/paper-typography.d.ts +0 -9
  86. package/dist/eslint-rules/paper-typography.d.ts.map +1 -1
  87. package/dist/eslint-rules/paper-typography.js +34 -96
  88. package/dist/eslint-rules/paper-typography.js.map +1 -1
  89. package/dist/eslint-rules/papers.d.ts +1 -3
  90. package/dist/eslint-rules/papers.d.ts.map +1 -1
  91. package/dist/eslint-rules/papers.js +2 -4
  92. package/dist/eslint-rules/papers.js.map +1 -1
  93. package/dist/eslint-rules/tex-build.d.ts.map +1 -1
  94. package/dist/eslint-rules/tex-build.js +8 -25
  95. package/dist/eslint-rules/tex-build.js.map +1 -1
  96. package/dist/init.d.ts +1 -4
  97. package/dist/init.d.ts.map +1 -1
  98. package/dist/init.js +3 -9
  99. package/dist/init.js.map +1 -1
  100. package/dist/latex-log.d.ts +10 -0
  101. package/dist/latex-log.d.ts.map +1 -1
  102. package/dist/latex-log.js +10 -0
  103. package/dist/latex-log.js.map +1 -1
  104. package/dist/latex-loop.d.ts +2 -0
  105. package/dist/latex-loop.d.ts.map +1 -1
  106. package/dist/latex-loop.js.map +1 -1
  107. package/dist/lib/markdown.d.ts +5 -81
  108. package/dist/lib/markdown.d.ts.map +1 -1
  109. package/dist/lib/markdown.js +5 -116
  110. package/dist/lib/markdown.js.map +1 -1
  111. package/dist/lib/paper-config.d.ts.map +1 -1
  112. package/dist/lib/paper-config.js +0 -2
  113. package/dist/lib/paper-config.js.map +1 -1
  114. package/dist/lib/skill-eval-fixture.d.ts +6 -4
  115. package/dist/lib/skill-eval-fixture.d.ts.map +1 -1
  116. package/dist/lib/skill-eval-fixture.js +25 -18
  117. package/dist/lib/skill-eval-fixture.js.map +1 -1
  118. package/dist/new-paper.d.ts +5 -11
  119. package/dist/new-paper.d.ts.map +1 -1
  120. package/dist/new-paper.js +12 -23
  121. package/dist/new-paper.js.map +1 -1
  122. package/dist/package-dirs.d.ts.map +1 -1
  123. package/dist/package-dirs.js +3 -2
  124. package/dist/package-dirs.js.map +1 -1
  125. package/dist/paper-files.d.ts +4 -4
  126. package/dist/paper-includes.d.ts +4 -7
  127. package/dist/paper-includes.d.ts.map +1 -1
  128. package/dist/paper-includes.js +29 -21
  129. package/dist/paper-includes.js.map +1 -1
  130. package/dist/paper-prose.d.ts +17 -0
  131. package/dist/paper-prose.d.ts.map +1 -0
  132. package/dist/paper-prose.js +46 -0
  133. package/dist/paper-prose.js.map +1 -0
  134. package/dist/paper-record.d.ts +25 -0
  135. package/dist/paper-record.d.ts.map +1 -0
  136. package/dist/paper-record.js +39 -0
  137. package/dist/paper-record.js.map +1 -0
  138. package/dist/paper-sources.d.ts +66 -0
  139. package/dist/paper-sources.d.ts.map +1 -0
  140. package/dist/paper-sources.js +92 -0
  141. package/dist/paper-sources.js.map +1 -0
  142. package/dist/ports/bib-reader.d.ts +18 -0
  143. package/dist/ports/bib-reader.d.ts.map +1 -0
  144. package/dist/ports/bib-reader.js +2 -0
  145. package/dist/ports/bib-reader.js.map +1 -0
  146. package/dist/ports/check-references.d.ts +9 -4
  147. package/dist/ports/check-references.d.ts.map +1 -1
  148. package/dist/ports/latex.d.ts +15 -0
  149. package/dist/ports/latex.d.ts.map +1 -1
  150. package/dist/ports/sources-codec.d.ts +11 -0
  151. package/dist/ports/sources-codec.d.ts.map +1 -0
  152. package/dist/ports/sources-codec.js +2 -0
  153. package/dist/ports/sources-codec.js.map +1 -0
  154. package/dist/ports/tex-output.d.ts +17 -0
  155. package/dist/ports/tex-output.d.ts.map +1 -0
  156. package/dist/ports/tex-output.js +2 -0
  157. package/dist/ports/tex-output.js.map +1 -0
  158. package/dist/recorded-bibliography.d.ts +89 -0
  159. package/dist/recorded-bibliography.d.ts.map +1 -0
  160. package/dist/recorded-bibliography.js +136 -0
  161. package/dist/recorded-bibliography.js.map +1 -0
  162. package/dist/reference-rules.d.ts +5 -5
  163. package/dist/reference-rules.d.ts.map +1 -1
  164. package/dist/reference-rules.js +150 -77
  165. package/dist/reference-rules.js.map +1 -1
  166. package/dist/references.d.ts +71 -16
  167. package/dist/references.d.ts.map +1 -1
  168. package/dist/references.js +110 -26
  169. package/dist/references.js.map +1 -1
  170. package/dist/register-bands.d.ts +3 -3
  171. package/dist/register-bands.d.ts.map +1 -1
  172. package/dist/register-bands.js.map +1 -1
  173. package/dist/register.d.ts +2 -2
  174. package/dist/sources-record.d.ts +38 -0
  175. package/dist/sources-record.d.ts.map +1 -0
  176. package/dist/sources-record.js +123 -0
  177. package/dist/sources-record.js.map +1 -0
  178. package/dist/sources-rules.d.ts +44 -0
  179. package/dist/sources-rules.d.ts.map +1 -0
  180. package/dist/sources-rules.js +76 -0
  181. package/dist/sources-rules.js.map +1 -0
  182. package/dist/structure.d.ts.map +1 -1
  183. package/dist/structure.js +4 -3
  184. package/dist/structure.js.map +1 -1
  185. package/dist/tex-paper.d.ts +10 -15
  186. package/dist/tex-paper.d.ts.map +1 -1
  187. package/dist/tex-paper.js +24 -40
  188. package/dist/tex-paper.js.map +1 -1
  189. package/dist/tex-venue-rules.d.ts +3 -3
  190. package/dist/tex-venue-rules.d.ts.map +1 -1
  191. package/dist/tex-venue-rules.js.map +1 -1
  192. package/dist/types.d.ts +0 -2
  193. package/dist/types.d.ts.map +1 -1
  194. package/docs/configuration.md +18 -12
  195. package/docs/design/paper-sources.md +197 -0
  196. package/docs/e2e.md +4 -6
  197. package/docs/package-shape-options.md +1 -1
  198. package/docs/prior-art/paper-folder-scaffolding.md +24 -29
  199. package/docs/references.md +34 -8
  200. package/docs/rules/bib/reachable-entry.md +64 -0
  201. package/docs/rules/paper/sources-fresh.md +81 -0
  202. package/docs/rules/tex/claim-provenance.md +0 -1
  203. package/docs/rules.md +62 -61
  204. package/eslint-rules/latex-language.harness.mjs +3 -3
  205. package/eslint-rules/latex-language.ts +10 -13
  206. package/eslint-rules/paper-context.ts +6 -5
  207. package/eslint-rules/paper-typography.test.mjs +29 -157
  208. package/eslint-rules/paper-typography.ts +35 -104
  209. package/eslint-rules/papers.harness.mjs +1 -1
  210. package/eslint-rules/papers.ts +2 -4
  211. package/eslint-rules/tex-build.harness.mjs +2 -2
  212. package/eslint-rules/tex-build.ts +8 -25
  213. package/eslint.config.mjs +22 -1
  214. package/fixtures/build-e2e/aidc/paper.tex +1 -1
  215. package/fixtures/build-e2e/anon-ack/paper.tex +1 -1
  216. package/fixtures/build-e2e/anon-author/paper.tex +1 -1
  217. package/fixtures/build-e2e/anon-bib/paper.tex +1 -1
  218. package/fixtures/build-e2e/anon-href/paper.tex +1 -1
  219. package/fixtures/build-e2e/anon-macro/paper.tex +1 -1
  220. package/fixtures/build-e2e/anon-pdfauthor/paper.tex +1 -1
  221. package/fixtures/paper-sources/p1/paper.tex +30 -0
  222. package/fixtures/paper-sources/p1/refs.bib +6 -0
  223. package/fixtures/paper-sources/p1/sections/dead.tex +6 -0
  224. package/fixtures/paper-sources/p1/sections/intro.tex +7 -0
  225. package/fixtures/paper-sources/p1/sections/parked.tex +6 -0
  226. package/fixtures/paper-sources/p1/tex-truth.json +37 -0
  227. package/fixtures/paper-sources/p1/versions/2026-01-01-review.tex +4 -0
  228. package/fixtures/paper-sources/v1-stale/paper.tex +14 -0
  229. package/fixtures/paper-sources/v1-stale/refs.bib +6 -0
  230. package/fixtures/paper-sources/v1-stale/tex-truth.json +32 -0
  231. package/fixtures/paper-sources/v10-ifthenelse/anon.bib +1 -0
  232. package/fixtures/paper-sources/v10-ifthenelse/paper.tex +9 -0
  233. package/fixtures/paper-sources/v10-ifthenelse/real.bib +1 -0
  234. package/fixtures/paper-sources/v10-ifthenelse/tex-truth.json +28 -0
  235. package/fixtures/paper-sources/v11-two-blocks/paper.tex +12 -0
  236. package/fixtures/paper-sources/v11-two-blocks/tex-truth.json +30 -0
  237. package/fixtures/paper-sources/v13-percent-text/paper.tex +6 -0
  238. package/fixtures/paper-sources/v13-percent-text/refs.bib +3 -0
  239. package/fixtures/paper-sources/v13-percent-text/tex-truth.json +30 -0
  240. package/fixtures/paper-sources/v15-after-end/paper.tex +6 -0
  241. package/fixtures/paper-sources/v15-after-end/refs.bib +1 -0
  242. package/fixtures/paper-sources/v15-after-end/tex-truth.json +17 -0
  243. package/fixtures/paper-sources/v16-included-block/bibblock.tex +3 -0
  244. package/fixtures/paper-sources/v16-included-block/paper.tex +7 -0
  245. package/fixtures/paper-sources/v16-included-block/refs.bib +1 -0
  246. package/fixtures/paper-sources/v16-included-block/tex-truth.json +35 -0
  247. package/fixtures/paper-sources/v17-include-order/bibsetup.tex +1 -0
  248. package/fixtures/paper-sources/v17-include-order/first.bib +1 -0
  249. package/fixtures/paper-sources/v17-include-order/later.bib +1 -0
  250. package/fixtures/paper-sources/v17-include-order/paper.tex +7 -0
  251. package/fixtures/paper-sources/v17-include-order/tex-truth.json +40 -0
  252. package/fixtures/paper-sources/v2-overwrite/paper.tex +14 -0
  253. package/fixtures/paper-sources/v2-overwrite/refs.bib +6 -0
  254. package/fixtures/paper-sources/v2-overwrite/tex-truth.json +30 -0
  255. package/fixtures/paper-sources/v22-parked-thebibliography/paper.tex +7 -0
  256. package/fixtures/paper-sources/v22-parked-thebibliography/tex-truth.json +17 -0
  257. package/fixtures/paper-sources/v23-parked-include/old.bib +1 -0
  258. package/fixtures/paper-sources/v23-parked-include/paper.tex +7 -0
  259. package/fixtures/paper-sources/v23-parked-include/parked.tex +1 -0
  260. package/fixtures/paper-sources/v23-parked-include/refs.bib +1 -0
  261. package/fixtures/paper-sources/v23-parked-include/tex-truth.json +28 -0
  262. package/fixtures/paper-sources/v24-quoted-fields/paper.tex +6 -0
  263. package/fixtures/paper-sources/v24-quoted-fields/refs.bib +5 -0
  264. package/fixtures/paper-sources/v24-quoted-fields/tex-truth.json +32 -0
  265. package/fixtures/paper-sources/v25-string-differs/paper.tex +10 -0
  266. package/fixtures/paper-sources/v25-string-differs/refs.bib +2 -0
  267. package/fixtures/paper-sources/v25-string-differs/tex-truth.json +32 -0
  268. package/fixtures/paper-sources/v26-comment-in-names/one.bib +1 -0
  269. package/fixtures/paper-sources/v26-comment-in-names/paper.tex +7 -0
  270. package/fixtures/paper-sources/v26-comment-in-names/tex-truth.json +32 -0
  271. package/fixtures/paper-sources/v26-comment-in-names/two.bib +1 -0
  272. package/fixtures/paper-sources/v27-shared-string/abbreviations.bib +2 -0
  273. package/fixtures/paper-sources/v27-shared-string/paper.tex +6 -0
  274. package/fixtures/paper-sources/v27-shared-string/references.bib +6 -0
  275. package/fixtures/paper-sources/v27-shared-string/tex-truth.json +31 -0
  276. package/fixtures/paper-sources/v28-shared-string-block/abbreviations.bib +2 -0
  277. package/fixtures/paper-sources/v28-shared-string-block/paper.tex +14 -0
  278. package/fixtures/paper-sources/v28-shared-string-block/tex-truth.json +33 -0
  279. package/fixtures/paper-sources/v29-generated-input/paper.tex +9 -0
  280. package/fixtures/paper-sources/v29-generated-input/tex-truth.json +24 -0
  281. package/fixtures/paper-sources/v3-declared/paper.bib +6 -0
  282. package/fixtures/paper-sources/v3-declared/paper.tex +6 -0
  283. package/fixtures/paper-sources/v3-declared/tex-truth.json +28 -0
  284. package/fixtures/paper-sources/v30-name-next-line/paper.tex +14 -0
  285. package/fixtures/paper-sources/v30-name-next-line/tex-truth.json +30 -0
  286. package/fixtures/paper-sources/v4-commented/paper.tex +14 -0
  287. package/fixtures/paper-sources/v4-commented/refs.bib +6 -0
  288. package/fixtures/paper-sources/v4-commented/tex-truth.json +28 -0
  289. package/fixtures/paper-sources/v5-percent-entry/paper.tex +20 -0
  290. package/fixtures/paper-sources/v5-percent-entry/tex-truth.json +37 -0
  291. package/fixtures/paper-sources/v6-unclosed/paper.tex +6 -0
  292. package/fixtures/paper-sources/v6-unclosed/refs.bib +5 -0
  293. package/fixtures/paper-sources/v6-unclosed/tex-truth.json +37 -0
  294. package/fixtures/paper-sources/v8-jobname/paper.tex +9 -0
  295. package/fixtures/paper-sources/v8-jobname/tex-truth.json +30 -0
  296. package/fixtures/paper-sources/v9-two-bibliographies/one.bib +1 -0
  297. package/fixtures/paper-sources/v9-two-bibliographies/paper.tex +7 -0
  298. package/fixtures/paper-sources/v9-two-bibliographies/tex-truth.json +35 -0
  299. package/fixtures/paper-sources/v9-two-bibliographies/two.bib +1 -0
  300. package/fixtures/paper-typography/clean-paper/paper.tex +1 -1
  301. package/fixtures/paper-typography/messy-paper/paper.tex +1 -1
  302. package/hooks/hooks.harness.ts +15 -15
  303. package/hooks/paper-edit-guard.hook.mjs +5 -8
  304. package/hooks/paper-edit-guard.test.ts +12 -3
  305. package/hooks/paper-skills-nudge.hook.mjs +2 -3
  306. package/hooks/paper-skills-nudge.test.ts +13 -3
  307. package/hooks/paper-status-gates.hook.mjs +2 -2
  308. package/hooks/paper-status-gates.test.ts +14 -2
  309. package/lib/markdown.test.mjs +5 -60
  310. package/lib/markdown.ts +9 -136
  311. package/lib/paper-config.ts +0 -2
  312. package/lib/skill-checks.test.mjs +4 -1
  313. package/lib/skill-eval-fixture.ts +25 -18
  314. package/package.json +2 -12
  315. package/presets/realm.jsonc +1 -1
  316. package/scripts/check.test.ts +23 -1
  317. package/scripts/check.ts +7 -3
  318. package/scripts/coverage-config.test.ts +19 -10
  319. package/scripts/harness-api.frozen.json +0 -5
  320. package/scripts/layer-legacy-frozen.test.ts +41 -2
  321. package/scripts/layer-legacy-frozen.ts +31 -2
  322. package/skills/cold-read-diff/SKILL.md +3 -3
  323. package/skills/cold-read-diff/SKILL.md.spec.ts +2 -2
  324. package/skills/grade-paper-writing/SKILL.md +15 -22
  325. package/skills/grade-paper-writing/SKILL.md.spec.ts +14 -21
  326. package/skills/grade-paper-writing/__snapshots__/prose-lint.test.ts.snap +40 -26
  327. package/skills/grade-paper-writing/prose-lint.mjs +117 -278
  328. package/skills/grade-paper-writing/prose-lint.test.ts +230 -79
  329. package/skills/paper-adversarial-review/SKILL.md +2 -2
  330. package/skills/paper-adversarial-review/SKILL.md.spec.ts +1 -1
  331. package/skills/paper-pipeline/SKILL.md +9 -12
  332. package/skills/paper-pipeline/SKILL.md.spec.ts +8 -11
  333. package/skills/paper-pipeline/description-language.eval.mjs +25 -15
  334. package/skills/paper-pipeline/framing-vs-vocabulary.eval.mjs +25 -15
  335. package/skills/paper-pipeline/grade-paper-writing-ablation.eval.mjs +25 -15
  336. package/skills/paper-pipeline/pipeline-firing.eval.mjs +27 -16
  337. package/skills/paper-pipeline/pipeline-language.eval.mjs +25 -15
  338. package/skills/paper-pipeline/references/acceptance-gate.md +16 -20
  339. package/skills/paper-pipeline/references/artifact-checklist.md +4 -5
  340. package/skills/paper-pipeline/references/pipeline-status-template.md +1 -1
  341. package/skills/paper-pipeline/scripts/README.md +0 -4
  342. package/skills/paper-pipeline/scripts/extract-ref-facts.harness.mjs +91 -105
  343. package/skills/paper-pipeline/scripts/extract-ref-facts.mjs +85 -241
  344. package/skills/paper-pipeline/scripts/extract-ref-facts.test.mjs +166 -153
  345. package/skills/paper-pipeline/scripts/generated-code.harness.mjs +3 -3
  346. package/skills/paper-status/SKILL.md +2 -2
  347. package/skills/paper-status/SKILL.md.spec.ts +1 -1
  348. package/skills/render-paper/SKILL.md +2 -2
  349. package/skills/render-paper/SKILL.md.spec.ts +1 -1
  350. package/skills/render-paper/extract-pdf-facts.mjs +2 -2
  351. package/skills/tighten-paper/SKILL.md +29 -42
  352. package/skills/tighten-paper/SKILL.md.spec.ts +28 -41
  353. package/skills/verify-citations/SKILL.md +7 -4
  354. package/skills/verify-citations/SKILL.md.spec.ts +6 -3
  355. package/skills/verify-citations/scripts/bib-authors.check.test.mjs +17 -32
  356. package/skills/verify-citations/scripts/bib-authors.cli.test.ts +138 -43
  357. package/skills/verify-citations/scripts/bib-authors.d.mts +22 -2
  358. package/skills/verify-citations/scripts/bib-authors.mjs +54 -193
  359. package/skills/verify-citations/scripts/bib-authors.test.mjs +1 -44
  360. package/skills/verify-citations/scripts/verify-cites.cli.test.ts +16 -0
  361. package/skills/verify-citations/scripts/verify-cites.d.mts +19 -3
  362. package/skills/verify-citations/scripts/verify-cites.mjs +42 -108
  363. package/skills/verify-citations/scripts/verify-cites.test.mjs +23 -36
  364. package/src/adapters/bibtex/index.test.ts +163 -0
  365. package/src/adapters/bibtex/index.ts +155 -0
  366. package/src/adapters/latex/filecontents.ts +114 -0
  367. package/src/adapters/latex/includes.ts +18 -10
  368. package/src/adapters/latex/index.test.ts +73 -0
  369. package/src/adapters/latex/index.ts +2 -0
  370. package/src/adapters/latex/nodes.ts +64 -0
  371. package/src/adapters/latex/parse.test.ts +38 -0
  372. package/src/adapters/latex/parse.ts +67 -6
  373. package/src/adapters/memory/index.ts +17 -0
  374. package/src/adapters/references/index.test.ts +75 -21
  375. package/src/adapters/references/index.ts +37 -32
  376. package/src/adapters/sources-record/index.test.ts +87 -0
  377. package/src/adapters/sources-record/index.ts +82 -0
  378. package/src/adapters/tex-output/blg.ts +27 -0
  379. package/src/adapters/tex-output/fls.ts +24 -0
  380. package/src/adapters/tex-output/index.test.ts +171 -0
  381. package/src/adapters/tex-output/index.ts +12 -0
  382. package/src/bib-rules.test.ts +356 -0
  383. package/src/bib-rules.ts +228 -0
  384. package/src/bibliography-at.test.ts +286 -0
  385. package/src/build.harness.mjs +2 -2
  386. package/src/build.test.ts +211 -1
  387. package/src/build.ts +101 -41
  388. package/src/claim-provenance.test.ts +33 -7
  389. package/src/claim-provenance.ts +2 -2
  390. package/src/cli.harness.mjs +46 -56
  391. package/src/cli.test.ts +44 -48
  392. package/src/cli.ts +51 -82
  393. package/src/domain/paper-source.test.ts +58 -20
  394. package/src/domain/paper-source.ts +38 -42
  395. package/src/domain/paper-sources.ts +94 -0
  396. package/src/domain/sources-record.test.ts +71 -0
  397. package/src/domain/sources-record.ts +136 -0
  398. package/src/domain/tex-run.test.ts +228 -0
  399. package/src/domain/tex-run.ts +139 -0
  400. package/src/folder-venue-rule.test.ts +1 -1
  401. package/src/heading-case.test.ts +24 -10
  402. package/src/init.test.ts +6 -7
  403. package/src/init.ts +3 -22
  404. package/src/latex-log.ts +20 -0
  405. package/src/latex-loop.harness.ts +1 -0
  406. package/src/latex-loop.ts +2 -0
  407. package/src/lint-fix.test.ts +10 -4
  408. package/src/linted-set.test.ts +9 -1
  409. package/src/new-paper.harness.mjs +12 -30
  410. package/src/new-paper.test.ts +15 -11
  411. package/src/new-paper.ts +11 -30
  412. package/src/package-dirs.ts +3 -2
  413. package/src/paper-files.test.ts +5 -4
  414. package/src/paper-files.ts +4 -4
  415. package/src/paper-includes.test.ts +157 -38
  416. package/src/paper-includes.ts +39 -35
  417. package/src/paper-prose.test.ts +78 -0
  418. package/src/paper-prose.ts +67 -0
  419. package/src/paper-record.test.ts +173 -0
  420. package/src/paper-record.ts +66 -0
  421. package/src/paper-settings-commands.test.ts +5 -2
  422. package/src/paper-sources.ts +151 -0
  423. package/src/parse-args.test.ts +0 -1
  424. package/src/ports/bib-reader.ts +27 -0
  425. package/src/ports/check-references.ts +7 -3
  426. package/src/ports/latex.ts +19 -0
  427. package/src/ports/sources-codec.ts +11 -0
  428. package/src/ports/tex-output.ts +17 -0
  429. package/src/recorded-bibliography.ts +297 -0
  430. package/src/reference-rules.ts +217 -104
  431. package/src/references.test.ts +520 -177
  432. package/src/references.ts +185 -37
  433. package/src/register-bands.test.ts +5 -1
  434. package/src/register-bands.ts +3 -5
  435. package/src/register.test.ts +6 -1
  436. package/src/register.ts +2 -2
  437. package/src/rule-cases.test.ts +68 -12
  438. package/src/rule-docs.test.ts +0 -1
  439. package/src/sources-record.test.ts +308 -0
  440. package/src/sources-record.ts +231 -0
  441. package/src/sources-rules.test.ts +152 -0
  442. package/src/sources-rules.ts +117 -0
  443. package/src/structure.harness.ts +5 -11
  444. package/src/structure.ts +4 -3
  445. package/src/tex-paper.test.ts +148 -78
  446. package/src/tex-paper.ts +31 -61
  447. package/src/tex-venue-rules.test.ts +36 -8
  448. package/src/tex-venue-rules.ts +3 -5
  449. package/src/types.ts +0 -2
  450. package/dist/eslint-rules/bib-reachable-entry.d.ts +0 -22
  451. package/dist/eslint-rules/bib-reachable-entry.d.ts.map +0 -1
  452. package/dist/eslint-rules/bib-reachable-entry.js +0 -75
  453. package/dist/eslint-rules/bib-reachable-entry.js.map +0 -1
  454. package/eslint-rules/bib-reachable-entry.ts +0 -76
  455. package/fixtures/real-markdown-paper/baseline.json +0 -22
  456. package/fixtures/real-markdown-paper/baseline.ts +0 -70
  457. package/skills/paper-pipeline/PIPELINE-MAP.md +0 -381
  458. package/skills/paper-pipeline/pipeline-firing.baseline.json +0 -270
  459. package/skills/paper-pipeline/references/sweep-2026-08-09-ideal-pipeline.md +0 -585
  460. package/skills/paper-pipeline/scripts/artifact-coverage.harness.mjs +0 -492
  461. package/skills/paper-pipeline/scripts/artifact-coverage.mjs +0 -397
  462. package/skills/paper-pipeline/scripts/artifact-coverage.test.ts +0 -86
  463. package/skills/paper-pipeline/scripts/check-provenance.mjs +0 -184
  464. package/skills/paper-pipeline/scripts/population-map.harness.mjs +0 -135
  465. package/skills/paper-pipeline/scripts/population-map.mjs +0 -188
  466. package/skills/paper-pipeline/scripts/population-map.test.mjs +0 -154
  467. package/skills/paper-pipeline/scripts/provenance.harness.mjs +0 -266
  468. package/skills/paper-pipeline/scripts/round-diff.harness.ts +0 -907
  469. package/skills/paper-pipeline/scripts/round-diff.mjs +0 -583
  470. package/skills/paper-pipeline/scripts/round-diff.test.mjs +0 -159
  471. package/skills/tighten-paper/__snapshots__/structure.test.ts.snap +0 -133
  472. package/skills/tighten-paper/structure.mjs +0 -364
  473. package/skills/tighten-paper/structure.test.ts +0 -117
  474. package/templates/paper/paper.md +0 -4
package/src/build.ts CHANGED
@@ -66,7 +66,19 @@ import type { Runner } from "./engine.ts";
66
66
  import type { Files } from "./ports/files.ts";
67
67
  import type { MeasureGeometry } from "./ports/measure-geometry.ts";
68
68
  import type { CheckReferences } from "./ports/check-references.ts";
69
- import { recordReferences, REFERENCES_FILE } from "./references.ts";
69
+ import {
70
+ notWiredRecord,
71
+ recordingNote,
72
+ type RecordSources,
73
+ type TexRun,
74
+ } from "./sources-record.ts";
75
+ import { JOBNAME } from "./domain/sources-record.ts";
76
+ import {
77
+ notWiredBibliography,
78
+ recordReferences,
79
+ REFERENCES_FILE,
80
+ type ReadBibliography,
81
+ } from "./references.ts";
70
82
  import {
71
83
  auxBib,
72
84
  bibtexExcerpt,
@@ -89,16 +101,11 @@ import { messageOf, printed } from "./domain/text.ts";
89
101
  const PIPED: ["ignore", "pipe", "pipe"] = ["ignore", "pipe", "pipe"];
90
102
 
91
103
  /** A directory counts as a paper by the same markers as `structure.ts` — one shared dictionary. */
92
- export const PAPER_MARKERS = [
93
- "PIPELINE-STATUS.md",
94
- "paper.tex",
95
- "paper.md",
96
- CONFIG_FILE,
97
- ];
104
+ export const PAPER_MARKERS = ["PIPELINE-STATUS.md", "paper.tex", CONFIG_FILE];
98
105
 
99
106
  /** The source paperlint compiles, and the job name every output file carries. */
100
107
  export const MAIN = "paper.tex";
101
- export const JOB = "paper";
108
+ export const JOB = JOBNAME;
102
109
 
103
110
  /**
104
111
  * Scripts that `paperlint build` USED to run. Their presence is reported and nothing more: running a
@@ -106,11 +113,16 @@ export const JOB = "paper";
106
113
  */
107
114
  export const IGNORED_SCRIPTS = ["build.sh", "repro/build-submission.sh"];
108
115
 
109
- /** The pdflatex flags: never stop for input, stop at the first error, and say file:line. */
116
+ /**
117
+ * The pdflatex flags: never stop for input, stop at the first error, say file:line, and write
118
+ * `paper.fls` — every file the pass opened, which is how the build learns what the paper is made of
119
+ * (`sources-record.ts`) without reading its TeX.
120
+ */
110
121
  export const PDFLATEX_FLAGS = [
111
122
  "-interaction=nonstopmode",
112
123
  "-halt-on-error",
113
124
  "-file-line-error",
125
+ "-recorder",
114
126
  ];
115
127
 
116
128
  /** Everything the steps may know about a paper — parsed from it, not configured. */
@@ -143,6 +155,16 @@ export interface BuildContext {
143
155
  readonly files: Files;
144
156
  /** The online reference checks; the CLI wires the real ones, a test passes a function. */
145
157
  readonly checkReferences: CheckReferences;
158
+ /**
159
+ * The databases the build's bibtex opened (`readBibliography`, from `_build/sources.json`); the CLI
160
+ * wires the real reader.
161
+ */
162
+ readonly readBibliography: ReadBibliography;
163
+ /**
164
+ * Writes `_build/sources.json` from TeX's own files after a compile; the CLI wires the real one.
165
+ * `buildPapers` hands the step `notWiredRecord` when it is given none: it records nothing and says so.
166
+ */
167
+ readonly recordSources: RecordSources;
146
168
  }
147
169
 
148
170
  export type StepOutcome =
@@ -270,7 +292,6 @@ export const inputsStep: BuildStep = {
270
292
  yes: true,
271
293
  why: `TEXINPUTS += the paper's directory, then ${texInputsDir()}`,
272
294
  }),
273
- // The same search path the lint rules read includes through: one answer to which file is meant.
274
295
  // A pass runs IN the paper's directory, so that directory is `.` here. TeX names a file it finds
275
296
  // through an absolute entry by that absolute path: with the directory itself on TEXINPUTS, the
276
297
  // `broken` build fixture's log said `/tmp/…/paper.tex:6:` instead of `./paper.tex:6:` (measured).
@@ -379,6 +400,7 @@ function latexPass(
379
400
  after: hashes(ctx.paperDir),
380
401
  markers: logMarkers(log),
381
402
  bib: bibInput(ctx.paperDir),
403
+ fls: readOr(join(ctx.paperDir, `${JOB}.fls`), "utf8"),
382
404
  errorLines:
383
405
  exitCode === 0
384
406
  ? []
@@ -425,28 +447,47 @@ function bibtexPass(ctx: BuildContext, opts: SpawnOptions): Observation | null {
425
447
  };
426
448
  }
427
449
 
450
+ /** What the loop's passes left for the record of the build's sources (`sources-record.ts`). */
451
+ function runOf(history: readonly Observation[]): TexRun {
452
+ const bibtexRuns = history.flatMap((o) =>
453
+ o.step === "bibtex" ? [o.exitCode] : [],
454
+ );
455
+ return {
456
+ fls: history.flatMap((o) =>
457
+ o.step === "latex" && o.fls !== null ? [o.fls] : [],
458
+ ),
459
+ bibtexExit: bibtexRuns.at(-1) ?? null,
460
+ };
461
+ }
462
+
428
463
  /** Run the loop until `nextStep` says done or fail. */
429
464
  export function compile(ctx: BuildContext): {
430
465
  end: Terminal;
431
466
  latex: number;
432
467
  bibtex: number;
468
+ run: TexRun;
433
469
  } {
434
470
  const history: Observation[] = [];
435
471
  const opts = spawnOptions(ctx);
436
- // Every log this loop reads is one its own pdflatex wrote. A pdflatex that dies before opening
437
- // paper.log would otherwise have an earlier build's error quoted as this one's.
438
- rmSync(join(ctx.paperDir, `${JOB}.log`), { force: true });
472
+ // Every file this loop reads is one its own pdflatex wrote. A pdflatex that dies before opening
473
+ // paper.log would otherwise have an earlier build's error quoted as this one's, and one that wrote
474
+ // no paper.fls would hand the record the files an earlier build read.
475
+ const remove = (ext: string): void => {
476
+ rmSync(join(ctx.paperDir, `${JOB}.${ext}`), { force: true });
477
+ };
478
+ remove("log");
479
+ remove("fls");
439
480
  const runs = { latex: 0, bibtex: 0 };
481
+ const finish = (end: Terminal) => ({ end, ...runs, run: runOf(history) });
440
482
  for (;;) {
441
483
  const step = nextStep(summarize(history));
442
- if (step.kind === "done" || step.kind === "fail")
443
- return { end: step, ...runs };
484
+ if (step.kind === "done" || step.kind === "fail") return finish(step);
444
485
  runs[step.kind]++;
445
486
  const seen =
446
487
  step.kind === "latex"
447
488
  ? latexPass(ctx, step.final, opts)
448
489
  : bibtexPass(ctx, opts);
449
- if (seen === null) return { end: cannotStart(step.kind), ...runs };
490
+ if (seen === null) return finish(cannotStart(step.kind));
450
491
  history.push(seen);
451
492
  }
452
493
  }
@@ -473,7 +514,7 @@ export const compileStep: BuildStep = {
473
514
  return { yes: true, why: `${facts.main} (${cls}${venue})` };
474
515
  },
475
516
  run: (ctx) => {
476
- const { end, latex, bibtex } = compile(ctx);
517
+ const { end, latex, bibtex, run } = compile(ctx);
477
518
  if (end.kind === "fail") {
478
519
  const bin = end.step === "latex" ? "pdflatex" : "bibtex";
479
520
  const where =
@@ -509,7 +550,7 @@ export const compileStep: BuildStep = {
509
550
  : "";
510
551
  return {
511
552
  ok: true,
512
- note: `${plural(latex, "pdflatex pass", "pdflatex passes")}, ${plural(bibtex, "bibtex run", "bibtex runs")}${warn}`,
553
+ note: `${plural(latex, "pdflatex pass", "pdflatex passes")}, ${plural(bibtex, "bibtex run", "bibtex runs")}${warn}; ${recordingNote(ctx.recordSources(ctx.paperDir, run))}`,
513
554
  };
514
555
  },
515
556
  };
@@ -590,7 +631,12 @@ export const referencesStep: BuildStep = {
590
631
  : { yes: false, why: "nothing is compiled" },
591
632
  run: async (ctx) => ({
592
633
  ok: true,
593
- note: await recordReferences(ctx.files, ctx.paperDir, ctx.checkReferences),
634
+ note: await recordReferences(
635
+ ctx.files,
636
+ ctx.paperDir,
637
+ ctx.readBibliography(ctx.paperDir),
638
+ ctx.checkReferences,
639
+ ),
594
640
  }),
595
641
  };
596
642
 
@@ -653,6 +699,17 @@ export interface BuildOptions {
653
699
  * warns. The CLI passes the real ones (`adapters/references`).
654
700
  */
655
701
  checkReferences?: CheckReferences;
702
+ /**
703
+ * The databases the build's bibtex opened, for the references step. Default: none — the step says
704
+ * it was not wired and records nothing, and lint warns. The CLI passes `bibliographyReader` over the
705
+ * real ports.
706
+ */
707
+ readBibliography?: ReadBibliography;
708
+ /**
709
+ * Writes `_build/sources.json` after a compile. Default: none — the build says it recorded nothing,
710
+ * and lint reports the paper as not built. The CLI passes `recordSources` over the real ports.
711
+ */
712
+ recordSources?: RecordSources;
656
713
  }
657
714
 
658
715
  /**
@@ -674,7 +731,14 @@ function baseDefaults({
674
731
  readPdf = pdfjsReader,
675
732
  projectRoot = env["CLAUDE_PROJECT_DIR"] || cwd,
676
733
  }: BuildOptions): Required<
677
- Omit<BuildOptions, "measure" | "files" | "checkReferences">
734
+ Omit<
735
+ BuildOptions,
736
+ | "measure"
737
+ | "files"
738
+ | "checkReferences"
739
+ | "readBibliography"
740
+ | "recordSources"
741
+ >
678
742
  > {
679
743
  return { run, cwd, env, steps, log, dryRun, readPdf, projectRoot };
680
744
  }
@@ -691,7 +755,16 @@ const notWired: CheckReferences = (_bib, cache) =>
691
755
 
692
756
  /** banal as the measurer, wired from the build's environment: the one piece of root work left here (#76). */
693
757
  function defaultMeasurer(
694
- b: Required<Omit<BuildOptions, "measure" | "files" | "checkReferences">>,
758
+ b: Required<
759
+ Omit<
760
+ BuildOptions,
761
+ | "measure"
762
+ | "files"
763
+ | "checkReferences"
764
+ | "readBibliography"
765
+ | "recordSources"
766
+ >
767
+ >,
695
768
  ): MeasureGeometry {
696
769
  const dirs = hostDirs({ cwd: b.cwd });
697
770
  return banalMeasurer(
@@ -710,6 +783,8 @@ function withDefaults(o: BuildOptions): Required<BuildOptions> {
710
783
  measure,
711
784
  files: o.files ?? nodeFiles,
712
785
  checkReferences: o.checkReferences ?? notWired,
786
+ readBibliography: o.readBibliography ?? notWiredBibliography,
787
+ recordSources: o.recordSources ?? notWiredRecord,
713
788
  };
714
789
  }
715
790
 
@@ -718,29 +793,14 @@ async function runSteps(
718
793
  paperDir: string,
719
794
  dir: string,
720
795
  plan: PlanLine[],
721
- {
722
- run,
723
- env,
724
- steps,
725
- readPdf,
726
- measure,
727
- files,
728
- checkReferences,
729
- }: Required<BuildOptions>,
796
+ o: Required<BuildOptions>,
730
797
  ): Promise<BuildResult> {
731
- let stepEnv = env;
798
+ let stepEnv = o.env;
732
799
  const notes: string[] = [];
733
- for (const [i, step] of steps.entries()) {
800
+ for (const [i, step] of o.steps.entries()) {
734
801
  if (!plan[i]?.applies) continue;
735
- const out = await step.run({
736
- paperDir,
737
- env: stepEnv,
738
- run,
739
- readPdf,
740
- measure,
741
- files,
742
- checkReferences,
743
- });
802
+ // Every port the options hold, the paper, and the environment so far.
803
+ const out = await step.run({ ...o, paperDir, env: stepEnv });
744
804
  if (!out.ok) {
745
805
  // The PDF THIS run wrote and the step then rejected (a partial pass).
746
806
  // A PDF from an earlier run is already gone — `buildPapers` removed it before anything ran.
@@ -7,10 +7,12 @@
7
7
  import { describe, expect, it } from "vitest";
8
8
  import { latexReader } from "./adapters/latex/index.ts";
9
9
  import { memoryFiles } from "./adapters/memory/index.ts";
10
+ import { sourcesCodec } from "./adapters/sources-record/index.ts";
10
11
  import {
11
12
  claimProvenanceRule,
12
13
  judgeClaimProvenance,
13
14
  } from "./claim-provenance.ts";
15
+ import { builtPaper } from "../test/recorded-fixture.ts";
14
16
 
15
17
  const doc = (body: string): string =>
16
18
  `\\documentclass{article}\n\\begin{document}\n${body}\n\\end{document}\n`;
@@ -317,7 +319,11 @@ function run(
317
319
  }
318
320
 
319
321
  const onDisk = (files: Record<string, string> = {}) =>
320
- claimProvenanceRule({ files: memoryFiles(files), latex: latexReader });
322
+ claimProvenanceRule({
323
+ files: memoryFiles(files),
324
+ latex: latexReader,
325
+ codec: sourcesCodec,
326
+ });
321
327
 
322
328
  describe("the rule, as ESLint runs it", () => {
323
329
  it("reports each finding at its lines, with the message naming the owners that would do", () => {
@@ -337,16 +343,36 @@ describe("the rule, as ESLint runs it", () => {
337
343
  ]);
338
344
  });
339
345
 
340
- it("🔴 reads the files the paper \\inputs: a finding there is reported at the \\input, naming its file and line", () => {
341
- const rule = onDisk({
342
- "/p/sections/results.tex":
343
- "\\section{Results}\nIt holds.\nAgents ignore 42\\% of rules.\n",
344
- });
345
- expect(run(rule, doc("Intro.\n\\input{sections/results}"))).toEqual([
346
+ it("🔴 reads the files the build recorded TeX reading: a finding there is reported at the \\input, naming its file and line", () => {
347
+ const main = doc("Intro.\n\\input{sections/results}");
348
+ const rule = onDisk(
349
+ builtPaper(
350
+ "/p",
351
+ {
352
+ "paper.tex": main,
353
+ "sections/results.tex":
354
+ "\\section{Results}\nIt holds.\nAgents ignore 42\\% of rules.\n",
355
+ },
356
+ [
357
+ { path: "paper.tex", role: "body" },
358
+ { path: "sections/results.tex", role: "body" },
359
+ ],
360
+ ),
361
+ );
362
+ expect(run(rule, main)).toEqual([
346
363
  {
347
364
  line: 4,
348
365
  message: `sections/results.tex:3:1: ${String(rule.meta.messages["noOwner"]).replace("{{number}}", "42%")}`,
349
366
  },
350
367
  ]);
351
368
  });
369
+
370
+ it("🔴 without a record the paper is paper.tex: the file it \\inputs is not read", () => {
371
+ const main = doc("Intro.\n\\input{sections/results}");
372
+ const rule = onDisk({
373
+ "/p/paper.tex": main,
374
+ "/p/sections/results.tex": "Agents ignore 42\\% of rules.\n",
375
+ });
376
+ expect(run(rule, main)).toEqual([]);
377
+ });
352
378
  });
@@ -37,7 +37,7 @@ import {
37
37
  segmentOf,
38
38
  sentences,
39
39
  } from "./domain/sentences.ts";
40
- import { readPaper, reportInPaper, type PaperDeps } from "./tex-paper.ts";
40
+ import { readPaper, reportInPaper, type ProseDeps } from "./tex-paper.ts";
41
41
  import {
42
42
  ruleDocsUrl,
43
43
  type Located,
@@ -198,7 +198,7 @@ const META: TexRuleModule["meta"] = {
198
198
  };
199
199
 
200
200
  /** The rule, over the body's prose of the whole paper, its includes spliced (`readPaper`). */
201
- export const claimProvenanceRule = (deps: PaperDeps): TexRuleModule => ({
201
+ export const claimProvenanceRule = (deps: ProseDeps): TexRuleModule => ({
202
202
  meta: META,
203
203
  create(context) {
204
204
  return {
@@ -42,6 +42,19 @@ 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
+
49
+ /**
50
+ * A run that linted a paper nobody built clean: exit 0, and the one finding is the warning that no
51
+ * build recorded what TeX read (`paper/sources-fresh`) — what "no findings" meant before that rule.
52
+ */
53
+ const cleanButUnbuilt = (r) =>
54
+ r.code === 0 &&
55
+ /paper\/sources-fresh/.test(r.out) &&
56
+ /1 problem \(0 errors, 1 warning\)/.test(r.out);
57
+
45
58
  /** Runs the utility with output captured — quieter and faster than spawning a process. */
46
59
  async function cli(args, cwd) {
47
60
  // 🔴 THE STREAMS ARE SPLIT, AND THAT IS LOAD-BEARING. As long as the harness dumped log and
@@ -129,11 +142,11 @@ check(
129
142
  const ruleBlocks = (c) => c.filter((b) => Array.isArray(b.files));
130
143
  check(
131
144
  "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,
145
+ Array.isArray(cfg) && ruleBlocks(cfg).length === 3,
133
146
  );
134
147
  check(
135
148
  "with a LaTeX language two blocks are added: paper.tex, and the files it includes",
136
- ruleBlocks(buildConfig({}, {})).length === 6,
149
+ ruleBlocks(buildConfig({}, {})).length === 5,
137
150
  );
138
151
  check(
139
152
  "🔴 the project's paper TEMPLATE directory is ignored — flat config does not skip dot-directories",
@@ -178,7 +191,7 @@ check(
178
191
  join(paper, "versions", "2026-07-22-submitted.pdf"),
179
192
  "x".repeat(100),
180
193
  );
181
- writeFileSync(join(paper, "paper.md"), "# Intro\n\nRQ1: does it hold?\n");
194
+ writeFileSync(join(paper, "paper.tex"), tex("RQ1: does it hold?"));
182
195
  writeFileSync(
183
196
  join(paper, "PIPELINE-STATUS.md"),
184
197
  `---\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`,
@@ -192,7 +205,7 @@ check(
192
205
  const bare = await cli(["lint"], root);
193
206
  check(
194
207
  "🔴 NO paperlint.json IS A VALID PROJECT — the default papers directory is linted",
195
- bare.code === 0 && /no findings/.test(bare.out),
208
+ cleanButUnbuilt(bare),
196
209
  );
197
210
  check(
198
211
  "and no config line is printed when there is no file to name",
@@ -204,7 +217,7 @@ check(
204
217
  const empty = await cli(["lint"], root);
205
218
  check(
206
219
  `an empty paperlint.json — no \`${PAPERS_DIR_FIELD}\`, so the default`,
207
- empty.code === 0 && /no findings/.test(empty.out),
220
+ cleanButUnbuilt(empty),
208
221
  );
209
222
 
210
223
  // 🔴 THE DECLARED DIRECTORY IS NOT THE DEFAULT: with `papers` declared, a CLI that ignored
@@ -217,7 +230,7 @@ check(
217
230
  const r = await cli(["lint"], root);
218
231
  check(
219
232
  "🔴 THE UTILITY READS THE DECLARATION FROM paperlint.json — the file `paperlint init` writes and the hooks read",
220
- r.code === 0 && /no findings/.test(r.out),
233
+ cleanButUnbuilt(r),
221
234
  );
222
235
  check(
223
236
  "and NAMES the carrier out loud — swapping settings is never silent",
@@ -703,13 +716,13 @@ check(
703
716
  const status = (bytes) =>
704
717
  `---\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
718
  writeFileSync(join(paper, "versions", "s.tex"), "abcd");
706
- writeFileSync(join(paper, "paper.md"), "# Intro\n\nRQ1: does it hold?\n");
719
+ writeFileSync(join(paper, "paper.tex"), tex("RQ1: does it hold?"));
707
720
 
708
721
  writeFileSync(join(paper, "PIPELINE-STATUS.md"), status(100));
709
722
  const clean = await cli(["lint", "papers"], root);
710
723
  check(
711
724
  "on a clean corpus — zero and a legible report",
712
- clean.code === 0 && /no findings/.test(clean.out),
725
+ cleanButUnbuilt(clean),
713
726
  );
714
727
 
715
728
  writeFileSync(join(paper, "PIPELINE-STATUS.md"), status(999));
@@ -788,7 +801,7 @@ check(
788
801
  join(paper, "versions", "2026-07-22-submitted.pdf"),
789
802
  "x".repeat(100),
790
803
  );
791
- writeFileSync(join(paper, "paper.md"), "# Intro\n\nRQ1: does it hold?\n");
804
+ writeFileSync(join(paper, "paper.tex"), tex("RQ1: does it hold?"));
792
805
  const status = (bytes) =>
793
806
  `---\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
807
  writeFileSync(join(paper, "PIPELINE-STATUS.md"), status(100));
@@ -818,7 +831,7 @@ check(
818
831
  const found = await cli(["lint"], root);
819
832
  check(
820
833
  "the config is FOUND on its own: `lint` with no argument at all runs",
821
- found.code === 0 && /no findings/.test(found.out),
834
+ cleanButUnbuilt(found),
822
835
  );
823
836
  check(
824
837
  "and the found file is NAMED out loud — swapping settings silently is not acceptable",
@@ -832,7 +845,7 @@ check(
832
845
  const fromSub = await cli(["lint"], paper);
833
846
  check(
834
847
  "the path from the config resolves relative to the CONFIG'S DIRECTORY, not the current one",
835
- fromSub.code === 0 && /no findings/.test(fromSub.out),
848
+ cleanButUnbuilt(fromSub),
836
849
  );
837
850
 
838
851
  // Consumer data from the found config actually reaches the rules, not just gets read.
@@ -861,7 +874,7 @@ check(
861
874
  const noPapers = await cli(["lint"], root);
862
875
  check(
863
876
  "a config WITHOUT the papers-directory field lints the default `papers/`",
864
- noPapers.code === 0 && /no findings/.test(noPapers.out),
877
+ cleanButUnbuilt(noPapers),
865
878
  );
866
879
  check(
867
880
  'an empty string as the papers directory is refused, not read as the directory ""',
@@ -897,8 +910,8 @@ check(
897
910
  );
898
911
  // Three § signs — `paper/section-word`, warn level and only warn.
899
912
  writeFileSync(
900
- join(paper, "paper.md"),
901
- "# Intro\n\nRQ1: does it hold?\n\nSee \u00a7 5 and \u00a7 6 and \u00a7 7.\n",
913
+ join(paper, "paper.tex"),
914
+ tex("RQ1: does it hold?\n\nSee \u00a7 5 and \u00a7 6 and \u00a7 7."),
902
915
  );
903
916
  writeFileSync(
904
917
  join(paper, "PIPELINE-STATUS.md"),
@@ -921,7 +934,7 @@ check(
921
934
  );
922
935
  check(
923
936
  'and the failure names the NUMBER and the THRESHOLD, not just "too many"',
924
- /3 warning\(s\) exceed the --max-warnings limit of 0/.test(strict.out),
937
+ /4 warning\(s\) exceed the --max-warnings limit of 0/.test(strict.out),
925
938
  );
926
939
  const generous = await cli(["lint", "--max-warnings", "5"], root);
927
940
  check(
@@ -1040,7 +1053,7 @@ check(
1040
1053
  const paper = join(tree, "papers", "p");
1041
1054
  mkdirSync(paper, { recursive: true });
1042
1055
  // 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");
1056
+ writeFileSync(join(paper, "paper.tex"), tex("See § 3 and §4."));
1044
1057
 
1045
1058
  const inside = await cli(["lint", "--json", "papers"], tree);
1046
1059
  const outside = await cli(
@@ -1273,22 +1286,10 @@ console.log(
1273
1286
 
1274
1287
  // ── the new flags parse, and none of them becomes the directory argument ─────────────────
1275
1288
  {
1276
- const a = parseArgs([
1277
- "init",
1278
- "--yes",
1279
- "--no-hooks",
1280
- "--paper",
1281
- "demo",
1282
- "--format",
1283
- "md",
1284
- ]);
1289
+ const a = parseArgs(["init", "--yes", "--no-hooks", "--paper", "demo"]);
1285
1290
  check(
1286
1291
  "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,
1292
+ a.yes && a.noHooks && a.paper === "demo" && a.paths.length === 0,
1292
1293
  );
1293
1294
  check("-y is --yes", parseArgs(["init", "-y"]).yes === true);
1294
1295
  check(
@@ -1300,11 +1301,6 @@ console.log(
1300
1301
  "🔴 --hooks=local is NOT implemented, and it is refused by name rather than read as a path",
1301
1302
  r.code === 2 && /--hooks=local is not implemented/.test(r.out),
1302
1303
  );
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
1304
  }
1309
1305
 
1310
1306
  // ── init wires the hooks, and a human can say no ─────────────────────────────────────────
@@ -1315,7 +1311,7 @@ console.log(
1315
1311
  const mk = (name) => {
1316
1312
  const dir = join(root, name);
1317
1313
  mkdirSync(join(dir, "papers", "p1"), { recursive: true });
1318
- writeFileSync(join(dir, "papers", "p1", "paper.md"), "# P\n");
1314
+ writeFileSync(join(dir, "papers", "p1", "paper.tex"), "% P\n");
1319
1315
  writeFileSync(
1320
1316
  join(dir, "papers", "p1", "PIPELINE-STATUS.md"),
1321
1317
  "---\n---\n",
@@ -1426,19 +1422,17 @@ console.log(
1426
1422
  ask: async (q) => {
1427
1423
  asked.push(q);
1428
1424
  if (/create a first paper/.test(q)) return "first";
1429
- if (/format:/.test(q)) return "md";
1430
1425
  return "";
1431
1426
  },
1432
- createPaper: async (papersRoot, name, format) => {
1433
- made.push({ papersRoot, name, format });
1427
+ createPaper: async (papersRoot, name) => {
1428
+ made.push({ papersRoot, name });
1434
1429
  return 0;
1435
1430
  },
1436
1431
  });
1437
1432
  check(
1438
- "a human with no paper is offered one, and the answer and format reach `paperlint new`'s routine",
1433
+ "a human with no paper is offered one, and the answer reaches `paperlint new`'s routine",
1439
1434
  made.length === 1 &&
1440
1435
  made[0].name === "first" &&
1441
- made[0].format === "md" &&
1442
1436
  made[0].papersRoot === join(dir, "papers"),
1443
1437
  );
1444
1438
  }
@@ -1466,22 +1460,17 @@ console.log(
1466
1460
  log: () => {},
1467
1461
  interactive: false,
1468
1462
  paper: "given",
1469
- createPaper: async (papersRoot, name, format) => (
1470
- made.push({ name, format }),
1471
- 0
1472
- ),
1463
+ createPaper: async (papersRoot, name) => (made.push({ name }), 0),
1473
1464
  });
1474
1465
  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",
1466
+ "--paper creates it without a human",
1467
+ made.length === 1 && made[0].name === "given",
1479
1468
  );
1480
1469
  }
1481
1470
  {
1482
1471
  const dir = bare("has-one");
1483
1472
  mkdirSync(join(dir, "papers", "p1"), { recursive: true });
1484
- writeFileSync(join(dir, "papers", "p1", "paper.md"), "# P\n");
1473
+ writeFileSync(join(dir, "papers", "p1", "paper.tex"), "% P\n");
1485
1474
  const asked = [];
1486
1475
  await init(dir, {
1487
1476
  ...base,
@@ -1569,7 +1558,7 @@ console.log(
1569
1558
  `{"${PAPERS_DIR_FIELD}":"papers"}\n`,
1570
1559
  );
1571
1560
  mkdirSync(join(root, "papers", "p1"), { recursive: true });
1572
- writeFileSync(join(root, "papers", "p1", "paper.md"), "# P\n");
1561
+ writeFileSync(join(root, "papers", "p1", "paper.tex"), "% P\n");
1573
1562
  writeFileSync(
1574
1563
  join(root, "papers", "p1", "PIPELINE-STATUS.md"),
1575
1564
  "---\n---\n",
@@ -1591,7 +1580,7 @@ console.log(
1591
1580
  join(root, "papers", "p2", "PIPELINE-STATUS.md"),
1592
1581
  readFileSync(join(root, "papers", ".template", "PIPELINE-STATUS.md")),
1593
1582
  );
1594
- writeFileSync(join(root, "papers", "p2", "paper.md"), "# P\n");
1583
+ writeFileSync(join(root, "papers", "p2", "paper.tex"), "% P\n");
1595
1584
  const r2 = await cli(["lint"], root);
1596
1585
  check(
1597
1586
  "and the same scorecard in a real paper folder fails — the ignore is scoped, not a blind spot",
@@ -1617,19 +1606,20 @@ console.log(
1617
1606
  );
1618
1607
  const r = await cli(["new", "demo"], root);
1619
1608
  check(
1620
- "🔴 `paperlint new demo` creates writing/demo/ and its lint is the verdict — exit 0, and only the warning that no venue is chosen yet",
1609
+ "🔴 `paperlint new demo` creates writing/demo/ and its lint is the verdict — exit 0, and only two warnings: no venue is chosen yet, the paper is not built",
1621
1610
  r.code === 0 &&
1622
1611
  existsSync(join(root, "writing", "demo", "PIPELINE-STATUS.md")) &&
1623
1612
  existsSync(join(root, "writing", "demo", "paper.tex")) &&
1624
1613
  existsSync(join(root, "writing", "demo", "paperlint.json")) &&
1625
1614
  /names no venue preset yet/.test(r.out) &&
1626
- /\(0 errors, 1 warning\)/.test(r.out),
1615
+ /paper\/sources-fresh/.test(r.out) &&
1616
+ /\(0 errors, 2 warnings\)/.test(r.out),
1627
1617
  );
1628
- const again = await cli(["new", "demo", "--format", "md"], root);
1618
+ const again = await cli(["new", "demo"], root);
1629
1619
  check(
1630
1620
  "a second run on the same folder adds nothing — the tex source already stands",
1631
1621
  again.code === 0 &&
1632
- !existsSync(join(root, "writing", "demo", "paper.md")) &&
1622
+ !/\+ /.test(again.out) &&
1633
1623
  /kept — never overwritten/.test(again.out),
1634
1624
  );
1635
1625
  const bad = await cli(["new", "Demo"], root);