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
@@ -36,7 +36,6 @@ const ROOT = dirname(HERE);
36
36
  const {
37
37
  newPaper,
38
38
  nameProblem,
39
- wantedFiles,
40
39
  PACKAGE_TEMPLATES,
41
40
  OVERRIDE_DIR,
42
41
  STATUS_FILE,
@@ -63,7 +62,7 @@ async function lint(dir) {
63
62
 
64
63
  try {
65
64
  // ── the templates are FILES in the package, one per required file ──────────────────────
66
- for (const f of [STATUS_FILE, SOURCE_FILE.tex, SOURCE_FILE.md])
65
+ for (const f of [STATUS_FILE, SOURCE_FILE])
67
66
  check(
68
67
  `the package ships templates/paper/${f}`,
69
68
  existsSync(join(PACKAGE_TEMPLATES, f)),
@@ -85,13 +84,13 @@ try {
85
84
  );
86
85
  check(
87
86
  "a refused name creates nothing",
88
- newPaper(work, "Bad", "tex", { venues: [] }).ok === false &&
87
+ newPaper(work, "Bad", { venues: [] }).ok === false &&
89
88
  !existsSync(join(work, "Bad")),
90
89
  );
91
90
 
92
91
  // ── a fresh LaTeX paper ─────────────────────────────────────────────────────────────
93
92
  const papers = join(work, "papers");
94
- const r = newPaper(papers, "demo", "tex", { venues: [] });
93
+ const r = newPaper(papers, "demo", { venues: [] });
95
94
  check(
96
95
  "a fresh folder gets the scorecard, a .tex source and paperlint.json, all from the package",
97
96
  r.ok &&
@@ -113,10 +112,11 @@ try {
113
112
  );
114
113
  const l = await lint(join(papers, "demo"));
115
114
  check(
116
- "🔴 `paperlint lint` passes the scaffold — exit 0, not a missing-file error — with the one warning that no venue is chosen yet",
115
+ "🔴 `paperlint lint` passes the scaffold — exit 0, not a missing-file error — with two warnings: no venue is chosen yet, and the paper is not built",
117
116
  l.code === 0 &&
118
117
  /names no venue preset yet/.test(l.text) &&
119
- /1 problem \(0 errors, 1 warning\)/.test(l.text),
118
+ /paper\/sources-fresh/.test(l.text) &&
119
+ /2 problems \(0 errors, 2 warnings\)/.test(l.text),
120
120
  );
121
121
 
122
122
  // What the status hook reads on every paper edit: the verdict line.
@@ -129,36 +129,18 @@ try {
129
129
 
130
130
  // ── never overwrite; on an existing folder only the missing files ─────────────────────
131
131
  writeFileSync(join(papers, "demo", STATUS_FILE), "mine\n");
132
- const again = newPaper(papers, "demo", "md", { venues: [] });
132
+ const again = newPaper(papers, "demo", { venues: [] });
133
133
  check(
134
- "🔴 a second run overwrites nothing and adds no second source",
134
+ "🔴 a second run overwrites nothing",
135
135
  again.ok &&
136
136
  !again.fresh &&
137
137
  read(papers, "demo", STATUS_FILE) === "mine\n" &&
138
- !existsSync(join(papers, "demo", "paper.md")) &&
139
138
  again.files.every((f) => f.status === "kept"),
140
139
  );
141
- mkdirSync(join(papers, "old"), { recursive: true });
142
- writeFileSync(join(papers, "old", "paper.md"), "# Old\n");
143
- const migrated = newPaper(papers, "old", "tex", { venues: [] });
144
- check(
145
- "an old folder with only paper.md gets ONLY the scorecard — that is the migration",
146
- migrated.ok &&
147
- existsSync(join(papers, "old", STATUS_FILE)) &&
148
- !existsSync(join(papers, "old", "paper.tex")) &&
149
- read(papers, "old", "paper.md") === "# Old\n",
150
- );
151
- check(
152
- "wantedFiles asks for a source only when neither format is there",
153
- wantedFiles(join(papers, "old"), "tex").join() ===
154
- `${STATUS_FILE},paperlint.json` &&
155
- wantedFiles(join(papers, "nowhere"), "md").join() ===
156
- `${STATUS_FILE},paper.md,paperlint.json`,
157
- );
158
140
  writeFileSync(join(papers, "afile"), "x");
159
141
  check(
160
142
  "a name taken by a FILE is refused, not written through",
161
- newPaper(papers, "afile", "tex", { venues: [] }).ok === false,
143
+ newPaper(papers, "afile", { venues: [] }).ok === false,
162
144
  );
163
145
 
164
146
  // ── the project's template wins, file by file ─────────────────────────────────────────
@@ -167,7 +149,7 @@ try {
167
149
  join(papers, OVERRIDE_DIR, STATUS_FILE),
168
150
  "---\n---\n# House scorecard for {{name}}\n",
169
151
  );
170
- const own = newPaper(papers, "house", "md", { venues: [] });
152
+ const own = newPaper(papers, "house", { venues: [] });
171
153
  // Guards: the override slot — `fromTemplate` in new-paper.ts reads `<papers>/.template/<file>`
172
154
  // before the package's copy. Replace that lookup with the package's alone and this goes red.
173
155
  check(
@@ -176,11 +158,11 @@ try {
176
158
  read(papers, "house", STATUS_FILE) ===
177
159
  "---\n---\n# House scorecard for house\n" &&
178
160
  own.files.find((f) => f.file === STATUS_FILE)?.from === "project" &&
179
- own.files.find((f) => f.file === "paper.md")?.from === "package",
161
+ own.files.find((f) => f.file === SOURCE_FILE)?.from === "package",
180
162
  );
181
163
 
182
164
  // ── a missing template is a broken install, not an empty file ─────────────────────────
183
- const broken = newPaper(join(work, "b"), "x", "tex", {
165
+ const broken = newPaper(join(work, "b"), "x", {
184
166
  venues: [],
185
167
  packageTemplates: join(work, "no-such-dir"),
186
168
  });
@@ -41,7 +41,7 @@ const settingsOf = (dir: string) =>
41
41
  describe("paperlint new — paperlint.json", () => {
42
42
  it("is always written, from the package template: no venue chosen yet, and valid", () => {
43
43
  const papers = join(tmp(), "papers");
44
- const r = newPaper(papers, "demo", "tex", { venues: [] });
44
+ const r = newPaper(papers, "demo", { venues: [] });
45
45
  expect(r.ok && r.files.find((f) => f.file === "paperlint.json")).toEqual({
46
46
  file: "paperlint.json",
47
47
  status: "created",
@@ -72,7 +72,7 @@ describe("paperlint new — paperlint.json", () => {
72
72
  join(papers, OVERRIDE_DIR, "paperlint.json"),
73
73
  '{ "extends": "paperlint:aisec", "kind": "research" }\n',
74
74
  );
75
- const r = newPaper(papers, "house", "tex", { venues: [] });
75
+ const r = newPaper(papers, "house", { venues: [] });
76
76
  expect(r.ok && r.files.find((f) => f.file === "paperlint.json")?.from).toBe(
77
77
  "project",
78
78
  );
@@ -89,7 +89,7 @@ describe("paperlint new — paperlint.json", () => {
89
89
  join(papers, "p", "paperlint.json"),
90
90
  '{"extends":"paperlint:aisec"}',
91
91
  );
92
- const r = newPaper(papers, "p", "tex", { venues: [] });
92
+ const r = newPaper(papers, "p", { venues: [] });
93
93
  expect(
94
94
  r.ok && r.files.find((f) => f.file === "paperlint.json")?.status,
95
95
  ).toBe("kept");
@@ -106,7 +106,7 @@ describe("paperlint lint — a paper with no venue preset chosen", () => {
106
106
  join(root, "package.json"),
107
107
  JSON.stringify({ name: "c", private: true }),
108
108
  );
109
- newPaper(join(root, "papers"), "demo", "tex", { venues: [] });
109
+ newPaper(join(root, "papers"), "demo", { venues: [] });
110
110
  if (extendsValue !== null)
111
111
  writeFileSync(
112
112
  join(root, "papers", "demo", "paperlint.json"),
@@ -122,10 +122,13 @@ describe("paperlint lint — a paper with no venue preset chosen", () => {
122
122
  return { code, messages };
123
123
  }
124
124
 
125
- it("gets exactly one warning naming the file to set, and exits 0", async () => {
125
+ it("gets the warning naming the file to set, besides the one that it is not built, and exits 0", async () => {
126
126
  const { code, messages } = await lintNew(null);
127
127
  expect(code).toBe(0);
128
- expect(messages).toHaveLength(1);
128
+ expect(messages.map((m) => m.ruleId)).toEqual([
129
+ "pdf/measured",
130
+ "paper/sources-fresh",
131
+ ]);
129
132
  expect(messages[0]?.ruleId).toBe("pdf/measured");
130
133
  expect(messages[0]?.severity).toBe(1);
131
134
  expect(messages[0]?.message).toMatch(/names no venue preset yet/);
@@ -254,10 +257,11 @@ describe("paperlint new --venue msr", () => {
254
257
  log: (s: string) => out.push(s),
255
258
  err: () => {},
256
259
  });
257
- // Guards: what remains is the reminder that the PDF is not built yet — a warning, not a finding
258
- // about the paper.
259
- expect(out.join("\n")).toMatch(/\(0 errors, 1 warning\)/);
260
+ // Guards: what remains is the reminder that the paper is not built yet — two warnings, not a
261
+ // finding about the paper: no PDF measured, no record of what TeX read.
262
+ expect(out.join("\n")).toMatch(/\(0 errors, 2 warnings\)/);
260
263
  expect(out.join("\n")).toMatch(/pdf\/measured/);
264
+ expect(out.join("\n")).toMatch(/paper\/sources-fresh/);
261
265
  expect(code).toBe(0);
262
266
  });
263
267
  });
@@ -488,7 +492,7 @@ describe("a project's own template", () => {
488
492
  mkdirSync(join(papers, OVERRIDE_DIR), { recursive: true });
489
493
  const src = join(papers, OVERRIDE_DIR, "paperlint.json");
490
494
  writeFileSync(src, '{\n // our house venue\n "extends": null\n}\n');
491
- const r = newPaper(papers, "p", "tex", {
495
+ const r = newPaper(papers, "p", {
492
496
  venues: [],
493
497
  venue: { extends: "paperlint:aisec", kind: null },
494
498
  });
@@ -504,7 +508,7 @@ describe("a project's own template", () => {
504
508
  mkdirSync(join(papers, OVERRIDE_DIR), { recursive: true });
505
509
  writeFileSync(join(papers, OVERRIDE_DIR, "paperlint.json"), "{}\n");
506
510
  const lines = reportNewPaper(
507
- newPaper(papers, "p", "tex", { venues: [] }),
511
+ newPaper(papers, "p", { venues: [] }),
508
512
  (p) => p,
509
513
  );
510
514
  expect(lines).toContain(
package/src/new-paper.ts CHANGED
@@ -8,8 +8,7 @@
8
8
  * Every rule below is taken from a tool that already solved it (docs/prior-art/paper-folder-scaffolding.md):
9
9
  * - the template is a FILE with a lookup order — the project's `<papers>/.template/<file>` first,
10
10
  * the package's `templates/paper/<file>` second (Hugo's archetypes);
11
- * - it NEVER overwrites: on an existing folder it adds only the missing required files, which is
12
- * also the migration for an old folder that has only `paper.md` (`cargo init`);
11
+ * - it NEVER overwrites: on an existing folder it adds only the missing required files (`cargo init`);
13
12
  * - the name is validated, because it becomes a path and, through the hooks, a shell argument.
14
13
  *
15
14
  * The only substitution is `{{name}}` → the folder name. Nothing else in a template is touched.
@@ -33,10 +32,6 @@ import {
33
32
  type NamedVenue,
34
33
  } from "./domain/venue-name.ts";
35
34
 
36
- export type PaperFormat = "tex" | "md";
37
- export const FORMATS: readonly PaperFormat[] = ["tex", "md"];
38
- export const DEFAULT_FORMAT: PaperFormat = "tex";
39
-
40
35
  /** The package's default templates. Read from disk, so they ship in the tarball (`files`). */
41
36
  export const PACKAGE_TEMPLATES = fileURLToPath(
42
37
  new URL("../templates/paper/", import.meta.url),
@@ -50,17 +45,12 @@ export const PACKAGE_TEMPLATES = fileURLToPath(
50
45
  export const OVERRIDE_DIR = ".template";
51
46
 
52
47
  export const STATUS_FILE = "PIPELINE-STATUS.md";
53
- export const SOURCE_FILE: Readonly<Record<PaperFormat, string>> = {
54
- tex: "paper.tex",
55
- md: "paper.md",
56
- };
48
+ /** A paper's source: LaTeX, one file at the paper's root. */
49
+ export const SOURCE_FILE = "paper.tex";
57
50
 
58
51
  /** The flag that lets `paperlint new` create a folder whose name names a venue. */
59
52
  export const ALLOW_VENUE_NAME = "--allow-venue-name";
60
53
 
61
- export const isFormat = (v: unknown): v is PaperFormat =>
62
- FORMATS.some((f) => f === v);
63
-
64
54
  /**
65
55
  * Why a name is refused, or null. `[a-z0-9._-]+` is the charset; a LEADING dot is refused on top
66
56
  * of it, because discovery skips dot-directories — a paper named `.x` would be created and then
@@ -109,20 +99,12 @@ export type NewPaperResult =
109
99
  }
110
100
  | { readonly ok: false; readonly reason: string };
111
101
 
112
- /**
113
- * The files a paper folder must end up with: the scorecard, a source in `format` — unless it
114
- * already has a source in EITHER format, in which case that one stands — and `paperlint.json`.
115
- */
116
- export function wantedFiles(dir: string, format: PaperFormat): string[] {
117
- return [...scorecardAndSource(dir, format), CONFIG_FILE];
118
- }
119
-
120
- function scorecardAndSource(dir: string, format: PaperFormat): string[] {
121
- const hasSource = Object.values(SOURCE_FILE).some((f) =>
122
- existsSync(join(dir, f)),
123
- );
124
- return hasSource ? [STATUS_FILE] : [STATUS_FILE, SOURCE_FILE[format]];
125
- }
102
+ /** The files a paper folder must end up with: the scorecard, the source and `paperlint.json`. */
103
+ export const WANTED_FILES: readonly string[] = [
104
+ STATUS_FILE,
105
+ SOURCE_FILE,
106
+ CONFIG_FILE,
107
+ ];
126
108
 
127
109
  /** The venue `paperlint new --venue` chose: what goes into the new paper's `paperlint.json`. */
128
110
  export interface VenueSetting {
@@ -195,7 +177,7 @@ function fromTemplate(
195
177
  return { ok: false, reason: `no template for ${file}: ${src} is missing` };
196
178
  const text = readFileSync(src, "utf8").split("{{name}}").join(name);
197
179
  const setClass = venue?.setClass ?? null;
198
- if (file === SOURCE_FILE.tex && setClass !== null)
180
+ if (file === SOURCE_FILE && setClass !== null)
199
181
  return { ok: true, value: { file, text: setClass(text), from } };
200
182
  if (file !== CONFIG_FILE || venue === null)
201
183
  return { ok: true, value: { file, text, from } };
@@ -217,7 +199,6 @@ export interface NewPaperOptions {
217
199
  export function newPaper(
218
200
  papersRoot: string,
219
201
  name: string,
220
- format: PaperFormat,
221
202
  opts: NewPaperOptions,
222
203
  ): NewPaperResult {
223
204
  const { packageTemplates = PACKAGE_TEMPLATES, venue = null } = opts;
@@ -230,7 +211,7 @@ export function newPaper(
230
211
 
231
212
  const files: FileOutcome[] = [];
232
213
  const toWrite: Planned[] = [];
233
- for (const file of wantedFiles(dir, format)) {
214
+ for (const file of WANTED_FILES) {
234
215
  if (existsSync(join(dir, file))) {
235
216
  files.push({ file, status: "kept" });
236
217
  continue;
@@ -6,8 +6,9 @@
6
6
  * presets/ the venue presets (`paperlint:<name>`) and their JSON Schema
7
7
  * presets/tex/ the LaTeX inputs `paperlint build` puts on TEXINPUTS
8
8
  *
9
- * `texSearchPath` is where an `\input` is looked for, in order. The build hands it to TeX and the
10
- * lint rules read includes through it, so the two cannot disagree about which file a paper means.
9
+ * `texSearchPath` is where an `\input` is looked for, in order. The build hands it to TeX; lint does
10
+ * not search for an include at all — it reads the files TeX read, from the build's record
11
+ * (docs/design/paper-sources.md §1).
11
12
  */
12
13
  import { fileURLToPath } from "node:url";
13
14
 
@@ -20,6 +20,7 @@ import { tmpdir } from "node:os";
20
20
  import { join } from "node:path";
21
21
  import { afterEach, describe, expect, it } from "vitest";
22
22
  import { buildConfig, eslintConfig, run } from "./cli.ts";
23
+ import { texLanguage } from "#eslint-rules/latex-language";
23
24
  import {
24
25
  narrowToOwners,
25
26
  ownedPatterns,
@@ -247,7 +248,7 @@ describe("`rules` blocks — where the rule lands", () => {
247
248
  },
248
249
  ],
249
250
  },
250
- null,
251
+ texLanguage,
251
252
  ),
252
253
  ),
253
254
  });
@@ -257,9 +258,9 @@ describe("`rules` blocks — where the rule lands", () => {
257
258
  const status = await on("papers/a/PIPELINE-STATUS.md");
258
259
  expect(status["paper/source"]).toEqual([1]);
259
260
  expect(status["paper/section-word"]).toBeUndefined();
260
- const draft = await on("papers/a/paper.md");
261
- expect(draft["paper/section-word"]).toEqual([0]);
262
- expect(draft["paper/source"]).toBeUndefined();
261
+ const source = await on("papers/a/paper.tex");
262
+ expect(source["paper/section-word"]).toEqual([0]);
263
+ expect(source["paper/source"]).toBeUndefined();
263
264
  });
264
265
  });
265
266
 
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * THE FILES PAPERLINT OWNS — read off its own config, and made the only thing ESLint may reach.
3
3
  *
4
- * paperlint's blocks name the files they lint (`PIPELINE-STATUS.md`, `paper.md`, `paper.tex`,
5
- * `reviews/*.md`, …). That list is the whole scope of `paperlint lint`, and this module turns it
4
+ * paperlint's blocks name the files they lint (`PIPELINE-STATUS.md`, `paper.tex`, `reviews/*.md`,
5
+ * …). That list is the whole scope of `paperlint lint`, and this module turns it
6
6
  * into two things ESLint understands:
7
7
  *
8
8
  * 1. **A global ignore of everything else** (`scopeToOwned`). Handing ESLint a directory is not
@@ -17,7 +17,7 @@
17
17
  * 2. **Each rule scoped to the files it was written for** (`ruleOwners` + `narrowToOwners`).
18
18
  * One plugin NAME is bound to different plugin objects in different blocks: `paper` holds
19
19
  * `stages`/`source`/`author-list` beside `PIPELINE-STATUS.md` and `typography` beside
20
- * `paper.md`/`paper.tex`. So a consumer block that named `paper/source`
20
+ * `paper.tex`. So a consumer block that named `paper/source`
21
21
  * for every file under the papers directory reached `paper.tex`, where `paper` has no `source`
22
22
  * — and ESLint threw `Could not find "source" in plugin "paper"` from inside `lintFiles`,
23
23
  * after paperlint's own validation had (correctly) accepted the id. A block is therefore split
@@ -36,7 +36,7 @@
36
36
  * - a global-ignore pattern that matches a DIRECTORY hides everything inside it, so directories
37
37
  * must be un-ignored (`!` + a pattern ending in a slash) before owned files can be;
38
38
  * - that un-ignores `node_modules/` and `.git/` too — ESLint's own defaults — so they are
39
- * re-ignored after, or a `paper.md` inside a dependency would be linted;
39
+ * re-ignored after, or a `paper.tex` inside a dependency would be linted;
40
40
  * - later global-ignore patterns win, across blocks, in config order;
41
41
  * - `files: [["a", "b"]]` matches a file only when BOTH patterns match.
42
42
  */
@@ -1,9 +1,11 @@
1
1
  /**
2
2
  * `paperlint lint` over a paper whose body is in other files: the rules over ESLint's LaTeX text read
3
- * every file `paper.tex` includes from its document body, report at that file's own path and line,
4
- * and fix that file. What a rule decides for the whole paper — the majority reference form and
5
- * whether the build is in review mode — it decides over all of the paper's files, not over the one
6
- * it is reporting in.
3
+ * every body file the last build's record lists (`_build/sources.json`: the files TeX read after
4
+ * `\\begin{document}`), report at that file's own path and line, and fix that file. A paper with no
5
+ * record, or one it has outgrown, is `paper.tex` alone and `paper/sources-fresh` says the rest went
6
+ * unlinted. What a rule decides for the whole paper — the majority reference form and whether the
7
+ * build is in review mode — it decides over all of the paper's files, not over the one it is
8
+ * reporting in.
7
9
  */
8
10
  import {
9
11
  mkdirSync,
@@ -16,8 +18,13 @@ import {
16
18
  import { tmpdir } from "node:os";
17
19
  import { join, relative } from "node:path";
18
20
  import { afterEach, describe, expect, it } from "vitest";
21
+ import { memoryFiles } from "./adapters/memory/index.ts";
22
+ import { absolutePath } from "./domain/paths.ts";
23
+ import { sourcesCodec } from "./adapters/sources-record/index.ts";
19
24
  import { run } from "./cli.ts";
25
+ import { includeBlocks, paperBodies } from "./paper-includes.ts";
20
26
  import { lintReport } from "../test/lint-report.ts";
27
+ import { builtFixture, recordedTree } from "../test/recorded-fixture.ts";
21
28
 
22
29
  const dirs: string[] = [];
23
30
  afterEach(() => {
@@ -29,7 +36,21 @@ const doc = (cls: string, preamble: string, body: string): string =>
29
36
 
30
37
  const INTRO = "See §3 and p < .05.\nAs Fig.~\\ref{f3} shows.\n";
31
38
 
32
- function project(files: Record<string, string>): string {
39
+ /** What TeX read of the paper `project` makes, in the order it read it. */
40
+ const READ = [
41
+ { path: "paper.tex", role: "body" },
42
+ { path: "macros.tex", role: "preamble" },
43
+ { path: "sections/intro.tex", role: "body" },
44
+ ] as const;
45
+
46
+ /**
47
+ * A project with one paper, `papers/a`, as a build left it: its files, and a record listing `inputs`.
48
+ * `inputs: null` is a paper nothing has built.
49
+ */
50
+ function project(
51
+ files: Record<string, string>,
52
+ inputs: readonly { path: string; role: "preamble" | "body" }[] | null = READ,
53
+ ): string {
33
54
  const root = realpathSync(mkdtempSync(join(tmpdir(), "paperlint-inc-")));
34
55
  dirs.push(root);
35
56
  const all: Record<string, string> = {
@@ -51,7 +72,8 @@ function project(files: Record<string, string>): string {
51
72
  ),
52
73
  ...files,
53
74
  };
54
- for (const [p, text] of Object.entries(all)) {
75
+ const written = inputs === null ? all : recordedTree(all, "papers/a", inputs);
76
+ for (const [p, text] of Object.entries(written)) {
55
77
  mkdirSync(join(root, p, ".."), { recursive: true });
56
78
  writeFileSync(join(root, p), text);
57
79
  }
@@ -62,7 +84,11 @@ function project(files: Record<string, string>): string {
62
84
  async function lint(
63
85
  root: string,
64
86
  args: string[] = [],
65
- ): Promise<{ byFile: Record<string, string[]>; err: string }> {
87
+ ): Promise<{
88
+ byFile: Record<string, string[]>;
89
+ said: Record<string, string[]>;
90
+ err: string;
91
+ }> {
66
92
  const out: string[] = [];
67
93
  const err: string[] = [];
68
94
  await run(["lint", "--json", ...args], {
@@ -70,13 +96,20 @@ async function lint(
70
96
  log: (s: string) => out.push(s),
71
97
  err: (s: string) => err.push(s),
72
98
  });
99
+ const report = lintReport(out.join("\n"));
73
100
  const byFile = Object.fromEntries(
74
- lintReport(out.join("\n")).map((r) => [
101
+ report.map((r) => [
75
102
  relative(root, r.filePath),
76
103
  r.messages.map((m) => m.ruleId ?? "(fatal)").sort(),
77
104
  ]),
78
105
  );
79
- return { byFile, err: err.join("\n") };
106
+ const said = Object.fromEntries(
107
+ report.map((r) => [
108
+ relative(root, r.filePath),
109
+ r.messages.map((m) => m.message),
110
+ ]),
111
+ );
112
+ return { byFile, said, err: err.join("\n") };
80
113
  }
81
114
 
82
115
  describe("lint reads the files paper.tex includes from its body", () => {
@@ -104,18 +137,24 @@ describe("lint reads the files paper.tex includes from its body", () => {
104
137
 
105
138
  it("🔴 an included file that is not `.tex` — a `.bbl` — is not handed to ESLint, which parsed it as JavaScript (#153)", async () => {
106
139
  const { byFile, err } = await lint(
107
- project({
108
- "papers/a/paper.tex": doc(
109
- "{article}",
110
- "",
111
- "Text, as Figure~\\ref{f1} shows.\n\\input{paper.bbl}",
112
- ),
113
- // What bibtex writes: a comment first, which ESLint's JavaScript parser stops at.
114
- "papers/a/paper.bbl":
115
- "% Generated by IEEEtran.bst\n\\begin{thebibliography}{1}\n" +
116
- "\\bibitem{a} A. Author, ``A result with p < .05,'' 2024.\n" +
117
- "\\end{thebibliography}\n",
118
- }),
140
+ project(
141
+ {
142
+ "papers/a/paper.tex": doc(
143
+ "{article}",
144
+ "",
145
+ "Text, as Figure~\\ref{f1} shows.\n\\input{paper.bbl}",
146
+ ),
147
+ // What bibtex writes: a comment first, which ESLint's JavaScript parser stops at.
148
+ "papers/a/paper.bbl":
149
+ "% Generated by IEEEtran.bst\n\\begin{thebibliography}{1}\n" +
150
+ "\\bibitem{a} A. Author, ``A result with p < .05,'' 2024.\n" +
151
+ "\\end{thebibliography}\n",
152
+ },
153
+ [
154
+ { path: "paper.tex", role: "body" },
155
+ { path: "paper.bbl", role: "body" },
156
+ ],
157
+ ),
119
158
  );
120
159
  // Guards: the `.bbl` was the one file of the run with a parse error («Unexpected token %»).
121
160
  // It is bibtex's output, not prose the author edits: no fragment rule reports in it, and
@@ -124,7 +163,6 @@ describe("lint reads the files paper.tex includes from its body", () => {
124
163
  "papers/a/PIPELINE-STATUS.md": [],
125
164
  "papers/a/paper.tex": [],
126
165
  });
127
- // Read, not missing: no «lint did not read» note names it.
128
166
  expect(err).not.toMatch(/paper\.bbl/);
129
167
  });
130
168
 
@@ -162,26 +200,107 @@ describe("what lint decides for the whole paper, it decides over every file of i
162
200
  );
163
201
  expect(review.byFile["papers/a/sections/intro.tex"]).toEqual([]);
164
202
  });
203
+ });
165
204
 
166
- it("an include that resolves nowhere is named as a file lint did not read", async () => {
167
- const { err } = await lint(
168
- project({
169
- "papers/a/paper.tex": doc("{article}", "", "\\input{sections/gone}"),
170
- }),
171
- );
172
- expect(err).toMatch(
173
- /papers\/a\/paper\.tex: lint did not read `sections\/gone`/,
174
- );
205
+ describe("a built paper whose files are clean", () => {
206
+ it("is reported as the files that were checked — the paper's, and the section TeX read", async () => {
207
+ const root = project({
208
+ "papers/a/sections/intro.tex": "Plain words, nothing to flag.\n",
209
+ });
210
+ const out: string[] = [];
211
+ const code = await run(["lint"], {
212
+ cwd: root,
213
+ log: (s: string) => out.push(s),
214
+ err: () => undefined,
215
+ });
216
+ expect({ code, out }).toEqual({
217
+ code: 0,
218
+ out: ["✓ 3 file(s) checked, no findings"],
219
+ });
175
220
  });
221
+ });
176
222
 
177
- it("the note is about the papers this run lints, not another paper of the project", async () => {
178
- const root = project({
179
- "papers/b/PIPELINE-STATUS.md": "---\nstages: []\n---\n",
180
- "papers/b/paper.tex": doc("{article}", "", "\\input{gone}"),
223
+ describe("a paper whose record is missing, stale or silent about a file", () => {
224
+ it("🔴 a paper no build has recorded is paper.tex alone: its includes are not linted, and paper/sources-fresh says so", async () => {
225
+ const { byFile } = await lint(project({}, null));
226
+ expect(byFile).toEqual({
227
+ "papers/a/PIPELINE-STATUS.md": [],
228
+ "papers/a/paper.tex": ["paper/sources-fresh"],
181
229
  });
182
- expect((await lint(root, ["papers/a"])).err).not.toMatch(/did not read/);
183
- expect((await lint(root)).err).toMatch(
184
- /papers\/b\/paper\.tex: lint did not read `gone`/,
230
+ });
231
+
232
+ it("🔴 a paper changed since its build is paper.tex alone, and paper/sources-fresh names the file", async () => {
233
+ const root = project({});
234
+ writeFileSync(
235
+ join(root, "papers/a/sections/intro.tex"),
236
+ "Edited after the build, p < .05.\n",
237
+ );
238
+ const { byFile, said } = await lint(root);
239
+ expect(Object.keys(byFile)).not.toContain("papers/a/sections/intro.tex");
240
+ expect(said["papers/a/paper.tex"]).toEqual([
241
+ expect.stringMatching(/sections\/intro\.tex edited/),
242
+ ]);
243
+ });
244
+ });
245
+
246
+ describe("paperBodies — the body files of the record TeX's own build left", () => {
247
+ const bodies = (files: Record<string, string>, dir = "/papers/p") =>
248
+ paperBodies([dir], { files: memoryFiles(files), codec: sourcesCodec });
249
+
250
+ it("p1: the one file TeX read after \\begin{document} — not the `.bib`, not the files its text names behind a comment or \\iffalse", () => {
251
+ expect(bodies(builtFixture("p1", "/papers/p"))).toEqual([
252
+ {
253
+ dir: "/papers/p",
254
+ main: "/papers/p/paper.tex",
255
+ files: ["/papers/p/sections/intro.tex"],
256
+ },
257
+ ]);
258
+ });
259
+
260
+ it("a body file that is a `.tex` listed after paper.tex is handed over; a preamble one is not (v16: a block written from a preamble include)", () => {
261
+ expect(
262
+ bodies(builtFixture("v16-included-block", "/papers/p"))[0]?.files,
263
+ ).toEqual([]);
264
+ expect(
265
+ bodies(builtFixture("v17-include-order", "/papers/p"))[0]?.files,
266
+ ).toEqual(["/papers/p/bibsetup.tex"]);
267
+ });
268
+
269
+ it("🔴 a body .tex TeX wrote from a filecontents block is not handed to ESLint: its text is the block's, and a fix there is overwritten or splits it from the block", () => {
270
+ const files = builtFixture("v29-generated-input", "/papers/p");
271
+ expect(files["/papers/p/body.tex"]).toBeDefined();
272
+ expect(bodies(files)[0]?.files).toEqual([]);
273
+ });
274
+
275
+ it("a directory without paper.tex is no paper", () => {
276
+ expect(bodies({}, "/papers/none")).toEqual([]);
277
+ });
278
+ });
279
+
280
+ describe("includeBlocks — what ESLint is told of each paper", () => {
281
+ it("un-ignores each body file by name, and hands the paper to every file of it", () => {
282
+ const main = absolutePath("/papers/p/paper.tex");
283
+ const intro = absolutePath("/papers/p/sections/intro.tex");
284
+ expect(includeBlocks([{ dir: "/papers/p", main, files: [intro] }])).toEqual(
285
+ [
286
+ { basePath: "/papers/p", ignores: ["!sections/intro.tex"] },
287
+ {
288
+ basePath: "/papers/p",
289
+ files: ["paper.tex", "sections/intro.tex"],
290
+ settings: { paperlint: { paper: { main, files: [main, intro] } } },
291
+ },
292
+ ],
185
293
  );
186
294
  });
295
+
296
+ it("a paper of one file un-ignores nothing", () => {
297
+ const main = absolutePath("/papers/p/paper.tex");
298
+ expect(includeBlocks([{ dir: "/papers/p", main, files: [] }])).toEqual([
299
+ {
300
+ basePath: "/papers/p",
301
+ files: ["paper.tex"],
302
+ settings: { paperlint: { paper: { main, files: [main] } } },
303
+ },
304
+ ]);
305
+ });
187
306
  });