paperlint 8.0.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 (356) 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 +40 -24
  57. package/dist/build.js.map +1 -1
  58. package/dist/claim-provenance.d.ts +2 -2
  59. package/dist/cli.d.ts +6 -1
  60. package/dist/cli.d.ts.map +1 -1
  61. package/dist/cli.js +30 -26
  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/paper-context.d.ts.map +1 -1
  80. package/dist/eslint-rules/paper-context.js +4 -3
  81. package/dist/eslint-rules/paper-context.js.map +1 -1
  82. package/dist/eslint-rules/paper-typography.d.ts +0 -9
  83. package/dist/eslint-rules/paper-typography.d.ts.map +1 -1
  84. package/dist/eslint-rules/paper-typography.js +12 -5
  85. package/dist/eslint-rules/paper-typography.js.map +1 -1
  86. package/dist/latex-log.d.ts +10 -0
  87. package/dist/latex-log.d.ts.map +1 -1
  88. package/dist/latex-log.js +10 -0
  89. package/dist/latex-log.js.map +1 -1
  90. package/dist/latex-loop.d.ts +2 -0
  91. package/dist/latex-loop.d.ts.map +1 -1
  92. package/dist/latex-loop.js.map +1 -1
  93. package/dist/package-dirs.d.ts.map +1 -1
  94. package/dist/package-dirs.js +3 -2
  95. package/dist/package-dirs.js.map +1 -1
  96. package/dist/paper-includes.d.ts +4 -7
  97. package/dist/paper-includes.d.ts.map +1 -1
  98. package/dist/paper-includes.js +29 -21
  99. package/dist/paper-includes.js.map +1 -1
  100. package/dist/paper-prose.d.ts +2 -2
  101. package/dist/paper-record.d.ts +25 -0
  102. package/dist/paper-record.d.ts.map +1 -0
  103. package/dist/paper-record.js +39 -0
  104. package/dist/paper-record.js.map +1 -0
  105. package/dist/paper-sources.d.ts +66 -0
  106. package/dist/paper-sources.d.ts.map +1 -0
  107. package/dist/paper-sources.js +92 -0
  108. package/dist/paper-sources.js.map +1 -0
  109. package/dist/ports/bib-reader.d.ts +18 -0
  110. package/dist/ports/bib-reader.d.ts.map +1 -0
  111. package/dist/ports/bib-reader.js +2 -0
  112. package/dist/ports/bib-reader.js.map +1 -0
  113. package/dist/ports/check-references.d.ts +9 -4
  114. package/dist/ports/check-references.d.ts.map +1 -1
  115. package/dist/ports/latex.d.ts +15 -0
  116. package/dist/ports/latex.d.ts.map +1 -1
  117. package/dist/ports/sources-codec.d.ts +11 -0
  118. package/dist/ports/sources-codec.d.ts.map +1 -0
  119. package/dist/ports/sources-codec.js +2 -0
  120. package/dist/ports/sources-codec.js.map +1 -0
  121. package/dist/ports/tex-output.d.ts +17 -0
  122. package/dist/ports/tex-output.d.ts.map +1 -0
  123. package/dist/ports/tex-output.js +2 -0
  124. package/dist/ports/tex-output.js.map +1 -0
  125. package/dist/recorded-bibliography.d.ts +89 -0
  126. package/dist/recorded-bibliography.d.ts.map +1 -0
  127. package/dist/recorded-bibliography.js +136 -0
  128. package/dist/recorded-bibliography.js.map +1 -0
  129. package/dist/reference-rules.d.ts +5 -5
  130. package/dist/reference-rules.d.ts.map +1 -1
  131. package/dist/reference-rules.js +150 -77
  132. package/dist/reference-rules.js.map +1 -1
  133. package/dist/references.d.ts +71 -16
  134. package/dist/references.d.ts.map +1 -1
  135. package/dist/references.js +110 -26
  136. package/dist/references.js.map +1 -1
  137. package/dist/register-bands.d.ts +3 -3
  138. package/dist/register-bands.d.ts.map +1 -1
  139. package/dist/register-bands.js.map +1 -1
  140. package/dist/register.d.ts +2 -2
  141. package/dist/sources-record.d.ts +38 -0
  142. package/dist/sources-record.d.ts.map +1 -0
  143. package/dist/sources-record.js +123 -0
  144. package/dist/sources-record.js.map +1 -0
  145. package/dist/sources-rules.d.ts +44 -0
  146. package/dist/sources-rules.d.ts.map +1 -0
  147. package/dist/sources-rules.js +76 -0
  148. package/dist/sources-rules.js.map +1 -0
  149. package/dist/tex-paper.d.ts +10 -15
  150. package/dist/tex-paper.d.ts.map +1 -1
  151. package/dist/tex-paper.js +24 -40
  152. package/dist/tex-paper.js.map +1 -1
  153. package/dist/tex-venue-rules.d.ts +3 -3
  154. package/dist/tex-venue-rules.d.ts.map +1 -1
  155. package/dist/tex-venue-rules.js.map +1 -1
  156. package/docs/configuration.md +12 -4
  157. package/docs/design/paper-sources.md +197 -0
  158. package/docs/package-shape-options.md +1 -1
  159. package/docs/references.md +34 -8
  160. package/docs/rules/bib/reachable-entry.md +64 -0
  161. package/docs/rules/paper/sources-fresh.md +81 -0
  162. package/docs/rules.md +62 -57
  163. package/eslint-rules/paper-context.ts +4 -3
  164. package/eslint-rules/paper-typography.test.mjs +12 -72
  165. package/eslint-rules/paper-typography.ts +13 -6
  166. package/eslint.config.mjs +22 -0
  167. package/fixtures/build-e2e/aidc/paper.tex +1 -1
  168. package/fixtures/build-e2e/anon-ack/paper.tex +1 -1
  169. package/fixtures/build-e2e/anon-author/paper.tex +1 -1
  170. package/fixtures/build-e2e/anon-bib/paper.tex +1 -1
  171. package/fixtures/build-e2e/anon-href/paper.tex +1 -1
  172. package/fixtures/build-e2e/anon-macro/paper.tex +1 -1
  173. package/fixtures/build-e2e/anon-pdfauthor/paper.tex +1 -1
  174. package/fixtures/paper-sources/p1/paper.tex +30 -0
  175. package/fixtures/paper-sources/p1/refs.bib +6 -0
  176. package/fixtures/paper-sources/p1/sections/dead.tex +6 -0
  177. package/fixtures/paper-sources/p1/sections/intro.tex +7 -0
  178. package/fixtures/paper-sources/p1/sections/parked.tex +6 -0
  179. package/fixtures/paper-sources/p1/tex-truth.json +37 -0
  180. package/fixtures/paper-sources/p1/versions/2026-01-01-review.tex +4 -0
  181. package/fixtures/paper-sources/v1-stale/paper.tex +14 -0
  182. package/fixtures/paper-sources/v1-stale/refs.bib +6 -0
  183. package/fixtures/paper-sources/v1-stale/tex-truth.json +32 -0
  184. package/fixtures/paper-sources/v10-ifthenelse/anon.bib +1 -0
  185. package/fixtures/paper-sources/v10-ifthenelse/paper.tex +9 -0
  186. package/fixtures/paper-sources/v10-ifthenelse/real.bib +1 -0
  187. package/fixtures/paper-sources/v10-ifthenelse/tex-truth.json +28 -0
  188. package/fixtures/paper-sources/v11-two-blocks/paper.tex +12 -0
  189. package/fixtures/paper-sources/v11-two-blocks/tex-truth.json +30 -0
  190. package/fixtures/paper-sources/v13-percent-text/paper.tex +6 -0
  191. package/fixtures/paper-sources/v13-percent-text/refs.bib +3 -0
  192. package/fixtures/paper-sources/v13-percent-text/tex-truth.json +30 -0
  193. package/fixtures/paper-sources/v15-after-end/paper.tex +6 -0
  194. package/fixtures/paper-sources/v15-after-end/refs.bib +1 -0
  195. package/fixtures/paper-sources/v15-after-end/tex-truth.json +17 -0
  196. package/fixtures/paper-sources/v16-included-block/bibblock.tex +3 -0
  197. package/fixtures/paper-sources/v16-included-block/paper.tex +7 -0
  198. package/fixtures/paper-sources/v16-included-block/refs.bib +1 -0
  199. package/fixtures/paper-sources/v16-included-block/tex-truth.json +35 -0
  200. package/fixtures/paper-sources/v17-include-order/bibsetup.tex +1 -0
  201. package/fixtures/paper-sources/v17-include-order/first.bib +1 -0
  202. package/fixtures/paper-sources/v17-include-order/later.bib +1 -0
  203. package/fixtures/paper-sources/v17-include-order/paper.tex +7 -0
  204. package/fixtures/paper-sources/v17-include-order/tex-truth.json +40 -0
  205. package/fixtures/paper-sources/v2-overwrite/paper.tex +14 -0
  206. package/fixtures/paper-sources/v2-overwrite/refs.bib +6 -0
  207. package/fixtures/paper-sources/v2-overwrite/tex-truth.json +30 -0
  208. package/fixtures/paper-sources/v22-parked-thebibliography/paper.tex +7 -0
  209. package/fixtures/paper-sources/v22-parked-thebibliography/tex-truth.json +17 -0
  210. package/fixtures/paper-sources/v23-parked-include/old.bib +1 -0
  211. package/fixtures/paper-sources/v23-parked-include/paper.tex +7 -0
  212. package/fixtures/paper-sources/v23-parked-include/parked.tex +1 -0
  213. package/fixtures/paper-sources/v23-parked-include/refs.bib +1 -0
  214. package/fixtures/paper-sources/v23-parked-include/tex-truth.json +28 -0
  215. package/fixtures/paper-sources/v24-quoted-fields/paper.tex +6 -0
  216. package/fixtures/paper-sources/v24-quoted-fields/refs.bib +5 -0
  217. package/fixtures/paper-sources/v24-quoted-fields/tex-truth.json +32 -0
  218. package/fixtures/paper-sources/v25-string-differs/paper.tex +10 -0
  219. package/fixtures/paper-sources/v25-string-differs/refs.bib +2 -0
  220. package/fixtures/paper-sources/v25-string-differs/tex-truth.json +32 -0
  221. package/fixtures/paper-sources/v26-comment-in-names/one.bib +1 -0
  222. package/fixtures/paper-sources/v26-comment-in-names/paper.tex +7 -0
  223. package/fixtures/paper-sources/v26-comment-in-names/tex-truth.json +32 -0
  224. package/fixtures/paper-sources/v26-comment-in-names/two.bib +1 -0
  225. package/fixtures/paper-sources/v27-shared-string/abbreviations.bib +2 -0
  226. package/fixtures/paper-sources/v27-shared-string/paper.tex +6 -0
  227. package/fixtures/paper-sources/v27-shared-string/references.bib +6 -0
  228. package/fixtures/paper-sources/v27-shared-string/tex-truth.json +31 -0
  229. package/fixtures/paper-sources/v28-shared-string-block/abbreviations.bib +2 -0
  230. package/fixtures/paper-sources/v28-shared-string-block/paper.tex +14 -0
  231. package/fixtures/paper-sources/v28-shared-string-block/tex-truth.json +33 -0
  232. package/fixtures/paper-sources/v29-generated-input/paper.tex +9 -0
  233. package/fixtures/paper-sources/v29-generated-input/tex-truth.json +24 -0
  234. package/fixtures/paper-sources/v3-declared/paper.bib +6 -0
  235. package/fixtures/paper-sources/v3-declared/paper.tex +6 -0
  236. package/fixtures/paper-sources/v3-declared/tex-truth.json +28 -0
  237. package/fixtures/paper-sources/v30-name-next-line/paper.tex +14 -0
  238. package/fixtures/paper-sources/v30-name-next-line/tex-truth.json +30 -0
  239. package/fixtures/paper-sources/v4-commented/paper.tex +14 -0
  240. package/fixtures/paper-sources/v4-commented/refs.bib +6 -0
  241. package/fixtures/paper-sources/v4-commented/tex-truth.json +28 -0
  242. package/fixtures/paper-sources/v5-percent-entry/paper.tex +20 -0
  243. package/fixtures/paper-sources/v5-percent-entry/tex-truth.json +37 -0
  244. package/fixtures/paper-sources/v6-unclosed/paper.tex +6 -0
  245. package/fixtures/paper-sources/v6-unclosed/refs.bib +5 -0
  246. package/fixtures/paper-sources/v6-unclosed/tex-truth.json +37 -0
  247. package/fixtures/paper-sources/v8-jobname/paper.tex +9 -0
  248. package/fixtures/paper-sources/v8-jobname/tex-truth.json +30 -0
  249. package/fixtures/paper-sources/v9-two-bibliographies/one.bib +1 -0
  250. package/fixtures/paper-sources/v9-two-bibliographies/paper.tex +7 -0
  251. package/fixtures/paper-sources/v9-two-bibliographies/tex-truth.json +35 -0
  252. package/fixtures/paper-sources/v9-two-bibliographies/two.bib +1 -0
  253. package/fixtures/paper-typography/clean-paper/paper.tex +1 -1
  254. package/fixtures/paper-typography/messy-paper/paper.tex +1 -1
  255. package/package.json +2 -10
  256. package/scripts/check.test.ts +23 -1
  257. package/scripts/check.ts +7 -3
  258. package/scripts/coverage-config.test.ts +19 -10
  259. package/scripts/layer-legacy-frozen.test.ts +41 -2
  260. package/scripts/layer-legacy-frozen.ts +31 -2
  261. package/skills/grade-paper-writing/SKILL.md +4 -3
  262. package/skills/grade-paper-writing/SKILL.md.spec.ts +3 -2
  263. package/skills/grade-paper-writing/prose-lint.mjs +11 -2
  264. package/skills/grade-paper-writing/prose-lint.test.ts +52 -4
  265. package/skills/paper-pipeline/scripts/extract-ref-facts.harness.mjs +83 -33
  266. package/skills/paper-pipeline/scripts/extract-ref-facts.mjs +67 -115
  267. package/skills/paper-pipeline/scripts/extract-ref-facts.test.mjs +128 -121
  268. package/skills/verify-citations/SKILL.md +7 -4
  269. package/skills/verify-citations/SKILL.md.spec.ts +6 -3
  270. package/skills/verify-citations/scripts/bib-authors.check.test.mjs +17 -19
  271. package/skills/verify-citations/scripts/bib-authors.cli.test.ts +129 -46
  272. package/skills/verify-citations/scripts/bib-authors.d.mts +22 -2
  273. package/skills/verify-citations/scripts/bib-authors.mjs +57 -111
  274. package/skills/verify-citations/scripts/verify-cites.cli.test.ts +16 -0
  275. package/skills/verify-citations/scripts/verify-cites.d.mts +19 -3
  276. package/skills/verify-citations/scripts/verify-cites.mjs +42 -108
  277. package/skills/verify-citations/scripts/verify-cites.test.mjs +23 -36
  278. package/src/adapters/bibtex/index.test.ts +163 -0
  279. package/src/adapters/bibtex/index.ts +155 -0
  280. package/src/adapters/latex/filecontents.ts +114 -0
  281. package/src/adapters/latex/includes.ts +18 -10
  282. package/src/adapters/latex/index.test.ts +73 -0
  283. package/src/adapters/latex/index.ts +2 -0
  284. package/src/adapters/latex/nodes.ts +64 -0
  285. package/src/adapters/latex/parse.test.ts +38 -0
  286. package/src/adapters/latex/parse.ts +67 -6
  287. package/src/adapters/memory/index.ts +17 -0
  288. package/src/adapters/references/index.test.ts +75 -21
  289. package/src/adapters/references/index.ts +37 -32
  290. package/src/adapters/sources-record/index.test.ts +87 -0
  291. package/src/adapters/sources-record/index.ts +82 -0
  292. package/src/adapters/tex-output/blg.ts +27 -0
  293. package/src/adapters/tex-output/fls.ts +24 -0
  294. package/src/adapters/tex-output/index.test.ts +171 -0
  295. package/src/adapters/tex-output/index.ts +12 -0
  296. package/src/bib-rules.test.ts +356 -0
  297. package/src/bib-rules.ts +228 -0
  298. package/src/bibliography-at.test.ts +286 -0
  299. package/src/build.test.ts +210 -0
  300. package/src/build.ts +100 -35
  301. package/src/claim-provenance.test.ts +33 -7
  302. package/src/claim-provenance.ts +2 -2
  303. package/src/cli.harness.mjs +20 -10
  304. package/src/cli.test.ts +14 -10
  305. package/src/cli.ts +37 -28
  306. package/src/domain/paper-source.test.ts +58 -20
  307. package/src/domain/paper-source.ts +38 -42
  308. package/src/domain/paper-sources.ts +94 -0
  309. package/src/domain/sources-record.test.ts +71 -0
  310. package/src/domain/sources-record.ts +136 -0
  311. package/src/domain/tex-run.test.ts +228 -0
  312. package/src/domain/tex-run.ts +139 -0
  313. package/src/heading-case.test.ts +24 -10
  314. package/src/latex-log.ts +20 -0
  315. package/src/latex-loop.harness.ts +1 -0
  316. package/src/latex-loop.ts +2 -0
  317. package/src/linted-set.test.ts +9 -1
  318. package/src/new-paper.harness.mjs +3 -2
  319. package/src/new-paper.test.ts +9 -5
  320. package/src/package-dirs.ts +3 -2
  321. package/src/paper-includes.test.ts +157 -38
  322. package/src/paper-includes.ts +39 -35
  323. package/src/paper-prose.test.ts +34 -5
  324. package/src/paper-prose.ts +2 -2
  325. package/src/paper-record.test.ts +173 -0
  326. package/src/paper-record.ts +66 -0
  327. package/src/paper-settings-commands.test.ts +5 -2
  328. package/src/paper-sources.ts +151 -0
  329. package/src/ports/bib-reader.ts +27 -0
  330. package/src/ports/check-references.ts +7 -3
  331. package/src/ports/latex.ts +19 -0
  332. package/src/ports/sources-codec.ts +11 -0
  333. package/src/ports/tex-output.ts +17 -0
  334. package/src/recorded-bibliography.ts +297 -0
  335. package/src/reference-rules.ts +217 -104
  336. package/src/references.test.ts +520 -177
  337. package/src/references.ts +185 -37
  338. package/src/register-bands.test.ts +5 -1
  339. package/src/register-bands.ts +3 -5
  340. package/src/register.test.ts +6 -1
  341. package/src/register.ts +2 -2
  342. package/src/rule-cases.test.ts +68 -12
  343. package/src/rule-docs.test.ts +0 -1
  344. package/src/sources-record.test.ts +308 -0
  345. package/src/sources-record.ts +231 -0
  346. package/src/sources-rules.test.ts +152 -0
  347. package/src/sources-rules.ts +117 -0
  348. package/src/tex-paper.test.ts +148 -78
  349. package/src/tex-paper.ts +31 -61
  350. package/src/tex-venue-rules.test.ts +36 -8
  351. package/src/tex-venue-rules.ts +3 -5
  352. package/dist/eslint-rules/bib-reachable-entry.d.ts +0 -22
  353. package/dist/eslint-rules/bib-reachable-entry.d.ts.map +0 -1
  354. package/dist/eslint-rules/bib-reachable-entry.js +0 -75
  355. package/dist/eslint-rules/bib-reachable-entry.js.map +0 -1
  356. package/eslint-rules/bib-reachable-entry.ts +0 -76
package/src/build.test.ts CHANGED
@@ -22,7 +22,14 @@ import {
22
22
  type PaperFacts,
23
23
  } from "./build.ts";
24
24
  import type { Geometry } from "./domain/geometry.ts";
25
+ import { bibReader } from "./adapters/bibtex/index.ts";
26
+ import { nodeFiles } from "./adapters/node/index.ts";
27
+ import { sourcesCodec } from "./adapters/sources-record/index.ts";
28
+ import { sha256Hex } from "./domain/sha256.ts";
29
+ import { serializeSourcesRecord } from "./domain/sources-record.ts";
30
+ import { bibliographyReader } from "./references.ts";
25
31
  import type { PdfReader } from "./pdf-facts.ts";
32
+ import type { Recorded, TexRun } from "./sources-record.ts";
26
33
 
27
34
  const root = useTempDir("paperlint-build-test-");
28
35
  const TEX = "\\documentclass{article}\\begin{document}x\\end{document}";
@@ -44,6 +51,8 @@ interface Fake {
44
51
  noLog?: boolean;
45
52
  /** Each pass writes a different .aux, so the loop never settles. */
46
53
  churn?: boolean;
54
+ /** What pdflatex's `-recorder` file holds after pass `n`; without it, pdflatex records nothing. */
55
+ fls?: (pass: number) => string;
47
56
  }
48
57
  /** A pdflatex pass: writes the .aux, the log unless it dies first, and a PDF when it exits 0. */
49
58
  function latexPass(f: Fake, cwd: string, pass: number) {
@@ -51,6 +60,7 @@ function latexPass(f: Fake, cwd: string, pass: number) {
51
60
  const aux = f.churn ? `\\relax % ${String(pass)}\n` : (f.aux ?? "\\relax\n");
52
61
  writeFileSync(join(cwd, "paper.aux"), aux);
53
62
  if (!f.noLog) writeFileSync(join(cwd, "paper.log"), f.log ?? "");
63
+ if (f.fls) writeFileSync(join(cwd, "paper.fls"), f.fls(pass));
54
64
  if (status === 0) writeFileSync(join(cwd, "paper.pdf"), "%PDF-fake");
55
65
  return f.noLog ? { status } : { status, stdout: "", stderr: "" };
56
66
  }
@@ -263,6 +273,55 @@ test("compile: warnings the final log still reports are named in the note", asyn
263
273
  assert.match(r.notes?.[0] ?? "", /— ⚠️ the final log still reports /);
264
274
  });
265
275
 
276
+ test("references: without a reader the step says it was not wired; with one and no checker, the record says not checked", async () => {
277
+ const text = {
278
+ "paper.tex":
279
+ "\\documentclass{article}\\begin{document}x\\bibliography{refs}\\end{document}",
280
+ "refs.bib": "@misc{a, url = {https://example.org}}\n",
281
+ };
282
+ // What a build of this paper records: TeX read paper.tex, bibtex opened refs.bib and typeset `a`.
283
+ const digest = (s: string) => sha256Hex(new TextEncoder().encode(s));
284
+ const bib = {
285
+ ...text,
286
+ "_build/sources.json": serializeSourcesRecord({
287
+ schema: 1,
288
+ inputs: [{ path: "paper.tex", role: "body" }],
289
+ written: [],
290
+ bibdata: ["refs"],
291
+ bibtex: {
292
+ ran: true,
293
+ databases: ["refs.bib"],
294
+ keys: ["a"],
295
+ exit: 0,
296
+ errors: [],
297
+ },
298
+ sha256: {
299
+ "paper.tex": digest(text["paper.tex"]),
300
+ "refs.bib": digest(text["refs.bib"]),
301
+ },
302
+ }),
303
+ };
304
+ const unwired = await build(paper(bib));
305
+ assert.ok(
306
+ unwired.r.notes?.includes(
307
+ "references NOT checked — no bibliography reader was wired into this build; lint will say so",
308
+ ),
309
+ JSON.stringify(unwired.r.notes),
310
+ );
311
+ const readBibliography = bibliographyReader({
312
+ files: nodeFiles,
313
+ codec: sourcesCodec,
314
+ bib: bibReader,
315
+ });
316
+ const wired = await build(paper(bib), {}, { readBibliography });
317
+ assert.ok(
318
+ wired.r.notes?.includes(
319
+ "references NOT checked — no reference checker was wired into this build; lint will say so",
320
+ ),
321
+ JSON.stringify(wired.r.notes),
322
+ );
323
+ });
324
+
266
325
  test("measure: a review build and a stub last page are named; a measured geometry adds nothing", async () => {
267
326
  const review = await build(paper(), {}, { readPdf: reader(page(20, 60)) });
268
327
  assert.match(
@@ -382,3 +441,154 @@ test("a failed result without its lines still says what failed", () => {
382
441
  ].join("\n"),
383
442
  );
384
443
  });
444
+
445
+ // ── the record of the build's sources ────────────────────────────────────────────────────────
446
+
447
+ /** What a recorder was handed, for each paper it was asked about. */
448
+ function spyRecorder(answer: Recorded) {
449
+ const asked: { dir: string; run: TexRun }[] = [];
450
+ return {
451
+ asked,
452
+ recordSources: (dir: string, run: TexRun): Recorded => {
453
+ asked.push({ dir, run });
454
+ return answer;
455
+ },
456
+ };
457
+ }
458
+ const RECORDED: Recorded = {
459
+ kind: "recorded",
460
+ path: "/p/_build/sources.json",
461
+ record: {
462
+ schema: 1,
463
+ inputs: [{ path: "paper.tex", role: "body" }],
464
+ written: [],
465
+ bibdata: [],
466
+ bibtex: { ran: false },
467
+ sha256: {},
468
+ },
469
+ };
470
+
471
+ test("sources: every pdflatex pass runs with -recorder, so TeX itself says which files it read", async () => {
472
+ const args: (readonly string[])[] = [];
473
+ const run = fakeRun({});
474
+ await build(
475
+ paper(),
476
+ {},
477
+ {
478
+ run: (bin, a, opts) => {
479
+ if (bin === "pdflatex") args.push(a);
480
+ return run(bin, a, opts);
481
+ },
482
+ },
483
+ );
484
+ assert.ok(args.length >= 2);
485
+ assert.ok(
486
+ args.every((a) => a.includes("-recorder")),
487
+ JSON.stringify(args),
488
+ );
489
+ });
490
+
491
+ test("sources: the recorder is handed the .fls of every pass, in order, and bibtex's exit when it ran", async () => {
492
+ const spy = spyRecorder(RECORDED);
493
+ const { r } = await build(
494
+ paper({ "paper.tex": TEX, "refs.bib": "@misc{k}\n" }),
495
+ {
496
+ aux: "\\citation{k}\n\\bibstyle{plain}\n\\bibdata{refs}\n",
497
+ fls: (pass) => `pass ${String(pass)}`,
498
+ },
499
+ { recordSources: spy.recordSources },
500
+ );
501
+ assert.equal(r.status, "built");
502
+ const [only] = spy.asked;
503
+ assert.equal(spy.asked.length, 1);
504
+ assert.match(only?.dir ?? "", /\/p\d+$/);
505
+ const passes = only?.run.fls ?? [];
506
+ assert.ok(passes.length >= 2);
507
+ assert.deepEqual(
508
+ passes,
509
+ passes.map((_, i) => `pass ${String(i + 1)}`),
510
+ );
511
+ assert.equal(only?.run.bibtexExit, 0);
512
+ assert.match(
513
+ r.notes?.[0] ?? "",
514
+ /bibtex runs?; sources: 1 file read → _build\/sources\.json$/,
515
+ );
516
+ });
517
+
518
+ test("sources: a build that ran no bibtex says so to the recorder", async () => {
519
+ const spy = spyRecorder(RECORDED);
520
+ await build(
521
+ paper(),
522
+ { fls: () => "x" },
523
+ { recordSources: spy.recordSources },
524
+ );
525
+ assert.equal(spy.asked[0]?.run.bibtexExit, null);
526
+ });
527
+
528
+ test("sources: a paper.fls an earlier build left is not this build's: pdflatex that wrote none hands the recorder none", async () => {
529
+ const spy = spyRecorder(RECORDED);
530
+ const dir = paper({ "paper.tex": TEX, "paper.fls": "INPUT stale.tex\n" });
531
+ await build(dir, {}, { recordSources: spy.recordSources });
532
+ assert.deepEqual(spy.asked[0]?.run.fls, []);
533
+ });
534
+
535
+ test("sources: without a recorder, or when it records nothing, the build still builds and the note says it did not record", async () => {
536
+ const unwired = await build(paper());
537
+ assert.equal(unwired.r.status, "built");
538
+ assert.ok(
539
+ unwired.r.notes?.[0]?.endsWith(
540
+ "; sources NOT recorded — no recorder was wired into this build; lint will say so",
541
+ ),
542
+ JSON.stringify(unwired.r.notes),
543
+ );
544
+ const empty = await build(
545
+ paper(),
546
+ {},
547
+ {
548
+ recordSources: spyRecorder({
549
+ kind: "not-recorded",
550
+ why: "pdflatex wrote no paper.fls — nothing to record",
551
+ }).recordSources,
552
+ },
553
+ );
554
+ assert.equal(empty.r.status, "built");
555
+ assert.ok(
556
+ empty.r.notes?.[0]?.endsWith(
557
+ "; sources NOT recorded — pdflatex wrote no paper.fls — nothing to record; lint will say so",
558
+ ),
559
+ JSON.stringify(empty.r.notes),
560
+ );
561
+ });
562
+
563
+ test("sources: the note counts the files the record lists", async () => {
564
+ const two: Recorded = {
565
+ ...RECORDED,
566
+ record: {
567
+ ...RECORDED.record,
568
+ inputs: [
569
+ { path: "paper.tex", role: "body" },
570
+ { path: "sections/intro.tex", role: "body" },
571
+ ],
572
+ },
573
+ };
574
+ const { r } = await build(
575
+ paper(),
576
+ {},
577
+ { recordSources: spyRecorder(two).recordSources },
578
+ );
579
+ assert.ok(
580
+ r.notes?.[0]?.endsWith("; sources: 2 files read → _build/sources.json"),
581
+ JSON.stringify(r.notes),
582
+ );
583
+ });
584
+
585
+ test("sources: a build that fails records nothing", async () => {
586
+ const spy = spyRecorder(RECORDED);
587
+ const { r } = await build(
588
+ paper(),
589
+ { latex: 1 },
590
+ { recordSources: spy.recordSources },
591
+ );
592
+ assert.equal(r.status, "failed");
593
+ assert.deepEqual(spy.asked, []);
594
+ });
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,
@@ -93,7 +105,7 @@ export const PAPER_MARKERS = ["PIPELINE-STATUS.md", "paper.tex", CONFIG_FILE];
93
105
 
94
106
  /** The source paperlint compiles, and the job name every output file carries. */
95
107
  export const MAIN = "paper.tex";
96
- export const JOB = "paper";
108
+ export const JOB = JOBNAME;
97
109
 
98
110
  /**
99
111
  * Scripts that `paperlint build` USED to run. Their presence is reported and nothing more: running a
@@ -101,11 +113,16 @@ export const JOB = "paper";
101
113
  */
102
114
  export const IGNORED_SCRIPTS = ["build.sh", "repro/build-submission.sh"];
103
115
 
104
- /** 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
+ */
105
121
  export const PDFLATEX_FLAGS = [
106
122
  "-interaction=nonstopmode",
107
123
  "-halt-on-error",
108
124
  "-file-line-error",
125
+ "-recorder",
109
126
  ];
110
127
 
111
128
  /** Everything the steps may know about a paper — parsed from it, not configured. */
@@ -138,6 +155,16 @@ export interface BuildContext {
138
155
  readonly files: Files;
139
156
  /** The online reference checks; the CLI wires the real ones, a test passes a function. */
140
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;
141
168
  }
142
169
 
143
170
  export type StepOutcome =
@@ -265,7 +292,6 @@ export const inputsStep: BuildStep = {
265
292
  yes: true,
266
293
  why: `TEXINPUTS += the paper's directory, then ${texInputsDir()}`,
267
294
  }),
268
- // The same search path the lint rules read includes through: one answer to which file is meant.
269
295
  // A pass runs IN the paper's directory, so that directory is `.` here. TeX names a file it finds
270
296
  // through an absolute entry by that absolute path: with the directory itself on TEXINPUTS, the
271
297
  // `broken` build fixture's log said `/tmp/…/paper.tex:6:` instead of `./paper.tex:6:` (measured).
@@ -374,6 +400,7 @@ function latexPass(
374
400
  after: hashes(ctx.paperDir),
375
401
  markers: logMarkers(log),
376
402
  bib: bibInput(ctx.paperDir),
403
+ fls: readOr(join(ctx.paperDir, `${JOB}.fls`), "utf8"),
377
404
  errorLines:
378
405
  exitCode === 0
379
406
  ? []
@@ -420,28 +447,47 @@ function bibtexPass(ctx: BuildContext, opts: SpawnOptions): Observation | null {
420
447
  };
421
448
  }
422
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
+
423
463
  /** Run the loop until `nextStep` says done or fail. */
424
464
  export function compile(ctx: BuildContext): {
425
465
  end: Terminal;
426
466
  latex: number;
427
467
  bibtex: number;
468
+ run: TexRun;
428
469
  } {
429
470
  const history: Observation[] = [];
430
471
  const opts = spawnOptions(ctx);
431
- // Every log this loop reads is one its own pdflatex wrote. A pdflatex that dies before opening
432
- // paper.log would otherwise have an earlier build's error quoted as this one's.
433
- 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");
434
480
  const runs = { latex: 0, bibtex: 0 };
481
+ const finish = (end: Terminal) => ({ end, ...runs, run: runOf(history) });
435
482
  for (;;) {
436
483
  const step = nextStep(summarize(history));
437
- if (step.kind === "done" || step.kind === "fail")
438
- return { end: step, ...runs };
484
+ if (step.kind === "done" || step.kind === "fail") return finish(step);
439
485
  runs[step.kind]++;
440
486
  const seen =
441
487
  step.kind === "latex"
442
488
  ? latexPass(ctx, step.final, opts)
443
489
  : bibtexPass(ctx, opts);
444
- if (seen === null) return { end: cannotStart(step.kind), ...runs };
490
+ if (seen === null) return finish(cannotStart(step.kind));
445
491
  history.push(seen);
446
492
  }
447
493
  }
@@ -468,7 +514,7 @@ export const compileStep: BuildStep = {
468
514
  return { yes: true, why: `${facts.main} (${cls}${venue})` };
469
515
  },
470
516
  run: (ctx) => {
471
- const { end, latex, bibtex } = compile(ctx);
517
+ const { end, latex, bibtex, run } = compile(ctx);
472
518
  if (end.kind === "fail") {
473
519
  const bin = end.step === "latex" ? "pdflatex" : "bibtex";
474
520
  const where =
@@ -504,7 +550,7 @@ export const compileStep: BuildStep = {
504
550
  : "";
505
551
  return {
506
552
  ok: true,
507
- 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))}`,
508
554
  };
509
555
  },
510
556
  };
@@ -585,7 +631,12 @@ export const referencesStep: BuildStep = {
585
631
  : { yes: false, why: "nothing is compiled" },
586
632
  run: async (ctx) => ({
587
633
  ok: true,
588
- 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
+ ),
589
640
  }),
590
641
  };
591
642
 
@@ -648,6 +699,17 @@ export interface BuildOptions {
648
699
  * warns. The CLI passes the real ones (`adapters/references`).
649
700
  */
650
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;
651
713
  }
652
714
 
653
715
  /**
@@ -669,7 +731,14 @@ function baseDefaults({
669
731
  readPdf = pdfjsReader,
670
732
  projectRoot = env["CLAUDE_PROJECT_DIR"] || cwd,
671
733
  }: BuildOptions): Required<
672
- Omit<BuildOptions, "measure" | "files" | "checkReferences">
734
+ Omit<
735
+ BuildOptions,
736
+ | "measure"
737
+ | "files"
738
+ | "checkReferences"
739
+ | "readBibliography"
740
+ | "recordSources"
741
+ >
673
742
  > {
674
743
  return { run, cwd, env, steps, log, dryRun, readPdf, projectRoot };
675
744
  }
@@ -686,7 +755,16 @@ const notWired: CheckReferences = (_bib, cache) =>
686
755
 
687
756
  /** banal as the measurer, wired from the build's environment: the one piece of root work left here (#76). */
688
757
  function defaultMeasurer(
689
- 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
+ >,
690
768
  ): MeasureGeometry {
691
769
  const dirs = hostDirs({ cwd: b.cwd });
692
770
  return banalMeasurer(
@@ -705,6 +783,8 @@ function withDefaults(o: BuildOptions): Required<BuildOptions> {
705
783
  measure,
706
784
  files: o.files ?? nodeFiles,
707
785
  checkReferences: o.checkReferences ?? notWired,
786
+ readBibliography: o.readBibliography ?? notWiredBibliography,
787
+ recordSources: o.recordSources ?? notWiredRecord,
708
788
  };
709
789
  }
710
790
 
@@ -713,29 +793,14 @@ async function runSteps(
713
793
  paperDir: string,
714
794
  dir: string,
715
795
  plan: PlanLine[],
716
- {
717
- run,
718
- env,
719
- steps,
720
- readPdf,
721
- measure,
722
- files,
723
- checkReferences,
724
- }: Required<BuildOptions>,
796
+ o: Required<BuildOptions>,
725
797
  ): Promise<BuildResult> {
726
- let stepEnv = env;
798
+ let stepEnv = o.env;
727
799
  const notes: string[] = [];
728
- for (const [i, step] of steps.entries()) {
800
+ for (const [i, step] of o.steps.entries()) {
729
801
  if (!plan[i]?.applies) continue;
730
- const out = await step.run({
731
- paperDir,
732
- env: stepEnv,
733
- run,
734
- readPdf,
735
- measure,
736
- files,
737
- checkReferences,
738
- });
802
+ // Every port the options hold, the paper, and the environment so far.
803
+ const out = await step.run({ ...o, paperDir, env: stepEnv });
739
804
  if (!out.ok) {
740
805
  // The PDF THIS run wrote and the step then rejected (a partial pass).
741
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 {
@@ -46,6 +46,15 @@ const check = createChecker();
46
46
  const tex = (body) =>
47
47
  `\\documentclass{article}\n\\begin{document}\n${body}\n\\end{document}\n`;
48
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
+
49
58
  /** Runs the utility with output captured — quieter and faster than spawning a process. */
50
59
  async function cli(args, cwd) {
51
60
  // 🔴 THE STREAMS ARE SPLIT, AND THAT IS LOAD-BEARING. As long as the harness dumped log and
@@ -196,7 +205,7 @@ check(
196
205
  const bare = await cli(["lint"], root);
197
206
  check(
198
207
  "🔴 NO paperlint.json IS A VALID PROJECT — the default papers directory is linted",
199
- bare.code === 0 && /no findings/.test(bare.out),
208
+ cleanButUnbuilt(bare),
200
209
  );
201
210
  check(
202
211
  "and no config line is printed when there is no file to name",
@@ -208,7 +217,7 @@ check(
208
217
  const empty = await cli(["lint"], root);
209
218
  check(
210
219
  `an empty paperlint.json — no \`${PAPERS_DIR_FIELD}\`, so the default`,
211
- empty.code === 0 && /no findings/.test(empty.out),
220
+ cleanButUnbuilt(empty),
212
221
  );
213
222
 
214
223
  // 🔴 THE DECLARED DIRECTORY IS NOT THE DEFAULT: with `papers` declared, a CLI that ignored
@@ -221,7 +230,7 @@ check(
221
230
  const r = await cli(["lint"], root);
222
231
  check(
223
232
  "🔴 THE UTILITY READS THE DECLARATION FROM paperlint.json — the file `paperlint init` writes and the hooks read",
224
- r.code === 0 && /no findings/.test(r.out),
233
+ cleanButUnbuilt(r),
225
234
  );
226
235
  check(
227
236
  "and NAMES the carrier out loud — swapping settings is never silent",
@@ -713,7 +722,7 @@ check(
713
722
  const clean = await cli(["lint", "papers"], root);
714
723
  check(
715
724
  "on a clean corpus — zero and a legible report",
716
- clean.code === 0 && /no findings/.test(clean.out),
725
+ cleanButUnbuilt(clean),
717
726
  );
718
727
 
719
728
  writeFileSync(join(paper, "PIPELINE-STATUS.md"), status(999));
@@ -822,7 +831,7 @@ check(
822
831
  const found = await cli(["lint"], root);
823
832
  check(
824
833
  "the config is FOUND on its own: `lint` with no argument at all runs",
825
- found.code === 0 && /no findings/.test(found.out),
834
+ cleanButUnbuilt(found),
826
835
  );
827
836
  check(
828
837
  "and the found file is NAMED out loud — swapping settings silently is not acceptable",
@@ -836,7 +845,7 @@ check(
836
845
  const fromSub = await cli(["lint"], paper);
837
846
  check(
838
847
  "the path from the config resolves relative to the CONFIG'S DIRECTORY, not the current one",
839
- fromSub.code === 0 && /no findings/.test(fromSub.out),
848
+ cleanButUnbuilt(fromSub),
840
849
  );
841
850
 
842
851
  // Consumer data from the found config actually reaches the rules, not just gets read.
@@ -865,7 +874,7 @@ check(
865
874
  const noPapers = await cli(["lint"], root);
866
875
  check(
867
876
  "a config WITHOUT the papers-directory field lints the default `papers/`",
868
- noPapers.code === 0 && /no findings/.test(noPapers.out),
877
+ cleanButUnbuilt(noPapers),
869
878
  );
870
879
  check(
871
880
  'an empty string as the papers directory is refused, not read as the directory ""',
@@ -925,7 +934,7 @@ check(
925
934
  );
926
935
  check(
927
936
  'and the failure names the NUMBER and the THRESHOLD, not just "too many"',
928
- /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),
929
938
  );
930
939
  const generous = await cli(["lint", "--max-warnings", "5"], root);
931
940
  check(
@@ -1597,13 +1606,14 @@ console.log(
1597
1606
  );
1598
1607
  const r = await cli(["new", "demo"], root);
1599
1608
  check(
1600
- "🔴 `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",
1601
1610
  r.code === 0 &&
1602
1611
  existsSync(join(root, "writing", "demo", "PIPELINE-STATUS.md")) &&
1603
1612
  existsSync(join(root, "writing", "demo", "paper.tex")) &&
1604
1613
  existsSync(join(root, "writing", "demo", "paperlint.json")) &&
1605
1614
  /names no venue preset yet/.test(r.out) &&
1606
- /\(0 errors, 1 warning\)/.test(r.out),
1615
+ /paper\/sources-fresh/.test(r.out) &&
1616
+ /\(0 errors, 2 warnings\)/.test(r.out),
1607
1617
  );
1608
1618
  const again = await cli(["new", "demo"], root);
1609
1619
  check(