paperlint 8.0.0 → 9.0.1

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 (393) hide show
  1. package/.github/CLAUDE.md +22 -0
  2. package/.github/CLAUDE.md.spec.ts +20 -0
  3. package/CLAUDE.md +59 -531
  4. package/CONTRIBUTING.md +7 -6
  5. package/README.md +4 -3
  6. package/dist/CLAUDE.md.spec.d.ts +3 -0
  7. package/dist/CLAUDE.md.spec.d.ts.map +1 -0
  8. package/dist/CLAUDE.md.spec.js +62 -0
  9. package/dist/CLAUDE.md.spec.js.map +1 -0
  10. package/dist/adapters/bibtex/index.d.ts +3 -0
  11. package/dist/adapters/bibtex/index.d.ts.map +1 -0
  12. package/dist/adapters/bibtex/index.js +103 -0
  13. package/dist/adapters/bibtex/index.js.map +1 -0
  14. package/dist/adapters/latex/filecontents.d.ts +33 -0
  15. package/dist/adapters/latex/filecontents.d.ts.map +1 -0
  16. package/dist/adapters/latex/filecontents.js +65 -0
  17. package/dist/adapters/latex/filecontents.js.map +1 -0
  18. package/dist/adapters/latex/includes.d.ts +3 -3
  19. package/dist/adapters/latex/includes.d.ts.map +1 -1
  20. package/dist/adapters/latex/includes.js +18 -10
  21. package/dist/adapters/latex/includes.js.map +1 -1
  22. package/dist/adapters/latex/index.d.ts.map +1 -1
  23. package/dist/adapters/latex/index.js +2 -0
  24. package/dist/adapters/latex/index.js.map +1 -1
  25. package/dist/adapters/latex/nodes.d.ts +12 -0
  26. package/dist/adapters/latex/nodes.d.ts.map +1 -1
  27. package/dist/adapters/latex/nodes.js +39 -0
  28. package/dist/adapters/latex/nodes.js.map +1 -1
  29. package/dist/adapters/latex/parse.d.ts +7 -1
  30. package/dist/adapters/latex/parse.d.ts.map +1 -1
  31. package/dist/adapters/latex/parse.js +55 -6
  32. package/dist/adapters/latex/parse.js.map +1 -1
  33. package/dist/adapters/memory/index.d.ts +3 -0
  34. package/dist/adapters/memory/index.d.ts.map +1 -1
  35. package/dist/adapters/memory/index.js +9 -0
  36. package/dist/adapters/memory/index.js.map +1 -1
  37. package/dist/adapters/references/index.d.ts.map +1 -1
  38. package/dist/adapters/references/index.js +27 -24
  39. package/dist/adapters/references/index.js.map +1 -1
  40. package/dist/adapters/sources-record/index.d.ts +9 -0
  41. package/dist/adapters/sources-record/index.d.ts.map +1 -0
  42. package/dist/adapters/sources-record/index.js +68 -0
  43. package/dist/adapters/sources-record/index.js.map +1 -0
  44. package/dist/adapters/tex-output/blg.d.ts +9 -0
  45. package/dist/adapters/tex-output/blg.d.ts.map +1 -0
  46. package/dist/adapters/tex-output/blg.js +20 -0
  47. package/dist/adapters/tex-output/blg.js.map +1 -0
  48. package/dist/adapters/tex-output/fls.d.ts +8 -0
  49. package/dist/adapters/tex-output/fls.d.ts.map +1 -0
  50. package/dist/adapters/tex-output/fls.js +16 -0
  51. package/dist/adapters/tex-output/fls.js.map +1 -0
  52. package/dist/adapters/tex-output/index.d.ts +7 -0
  53. package/dist/adapters/tex-output/index.d.ts.map +1 -0
  54. package/dist/adapters/tex-output/index.js +7 -0
  55. package/dist/adapters/tex-output/index.js.map +1 -0
  56. package/dist/bib-rules.d.ts +47 -0
  57. package/dist/bib-rules.d.ts.map +1 -0
  58. package/dist/bib-rules.js +127 -0
  59. package/dist/bib-rules.js.map +1 -0
  60. package/dist/build.d.ts +29 -1
  61. package/dist/build.d.ts.map +1 -1
  62. package/dist/build.js +41 -25
  63. package/dist/build.js.map +1 -1
  64. package/dist/claim-provenance.d.ts +2 -2
  65. package/dist/cli.d.ts +6 -1
  66. package/dist/cli.d.ts.map +1 -1
  67. package/dist/cli.js +30 -26
  68. package/dist/cli.js.map +1 -1
  69. package/dist/domain/paper-source.d.ts +1 -10
  70. package/dist/domain/paper-source.d.ts.map +1 -1
  71. package/dist/domain/paper-source.js +34 -24
  72. package/dist/domain/paper-source.js.map +1 -1
  73. package/dist/domain/paper-sources.d.ts +72 -0
  74. package/dist/domain/paper-sources.d.ts.map +1 -0
  75. package/dist/domain/paper-sources.js +18 -0
  76. package/dist/domain/paper-sources.js.map +1 -0
  77. package/dist/domain/sources-record.d.ts +102 -0
  78. package/dist/domain/sources-record.d.ts.map +1 -0
  79. package/dist/domain/sources-record.js +38 -0
  80. package/dist/domain/sources-record.js.map +1 -0
  81. package/dist/domain/tex-run.d.ts +25 -0
  82. package/dist/domain/tex-run.d.ts.map +1 -0
  83. package/dist/domain/tex-run.js +79 -0
  84. package/dist/domain/tex-run.js.map +1 -0
  85. package/dist/eslint-rules/CLAUDE.md.spec.d.ts +3 -0
  86. package/dist/eslint-rules/CLAUDE.md.spec.d.ts.map +1 -0
  87. package/dist/eslint-rules/CLAUDE.md.spec.js +39 -0
  88. package/dist/eslint-rules/CLAUDE.md.spec.js.map +1 -0
  89. package/dist/eslint-rules/install-path-literals.d.ts +1 -1
  90. package/dist/eslint-rules/install-path-literals.js +4 -4
  91. package/dist/eslint-rules/install-path-literals.js.map +1 -1
  92. package/dist/eslint-rules/paper-context.d.ts.map +1 -1
  93. package/dist/eslint-rules/paper-context.js +4 -3
  94. package/dist/eslint-rules/paper-context.js.map +1 -1
  95. package/dist/eslint-rules/paper-typography.d.ts +0 -9
  96. package/dist/eslint-rules/paper-typography.d.ts.map +1 -1
  97. package/dist/eslint-rules/paper-typography.js +12 -5
  98. package/dist/eslint-rules/paper-typography.js.map +1 -1
  99. package/dist/eslint-rules/pdf-last-page-balance.js +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/agent-cli-version.js +1 -1
  108. package/dist/lib/agent-cli-version.js.map +1 -1
  109. package/dist/package-dirs.d.ts.map +1 -1
  110. package/dist/package-dirs.js +3 -2
  111. package/dist/package-dirs.js.map +1 -1
  112. package/dist/paper-includes.d.ts +4 -7
  113. package/dist/paper-includes.d.ts.map +1 -1
  114. package/dist/paper-includes.js +29 -21
  115. package/dist/paper-includes.js.map +1 -1
  116. package/dist/paper-prose.d.ts +2 -2
  117. package/dist/paper-record.d.ts +25 -0
  118. package/dist/paper-record.d.ts.map +1 -0
  119. package/dist/paper-record.js +39 -0
  120. package/dist/paper-record.js.map +1 -0
  121. package/dist/paper-sources.d.ts +66 -0
  122. package/dist/paper-sources.d.ts.map +1 -0
  123. package/dist/paper-sources.js +92 -0
  124. package/dist/paper-sources.js.map +1 -0
  125. package/dist/ports/bib-reader.d.ts +18 -0
  126. package/dist/ports/bib-reader.d.ts.map +1 -0
  127. package/dist/ports/bib-reader.js +2 -0
  128. package/dist/ports/bib-reader.js.map +1 -0
  129. package/dist/ports/check-references.d.ts +9 -4
  130. package/dist/ports/check-references.d.ts.map +1 -1
  131. package/dist/ports/latex.d.ts +15 -0
  132. package/dist/ports/latex.d.ts.map +1 -1
  133. package/dist/ports/sources-codec.d.ts +11 -0
  134. package/dist/ports/sources-codec.d.ts.map +1 -0
  135. package/dist/ports/sources-codec.js +2 -0
  136. package/dist/ports/sources-codec.js.map +1 -0
  137. package/dist/ports/tex-output.d.ts +17 -0
  138. package/dist/ports/tex-output.d.ts.map +1 -0
  139. package/dist/ports/tex-output.js +2 -0
  140. package/dist/ports/tex-output.js.map +1 -0
  141. package/dist/recorded-bibliography.d.ts +89 -0
  142. package/dist/recorded-bibliography.d.ts.map +1 -0
  143. package/dist/recorded-bibliography.js +136 -0
  144. package/dist/recorded-bibliography.js.map +1 -0
  145. package/dist/reference-rules.d.ts +5 -5
  146. package/dist/reference-rules.d.ts.map +1 -1
  147. package/dist/reference-rules.js +150 -77
  148. package/dist/reference-rules.js.map +1 -1
  149. package/dist/references.d.ts +71 -16
  150. package/dist/references.d.ts.map +1 -1
  151. package/dist/references.js +110 -26
  152. package/dist/references.js.map +1 -1
  153. package/dist/register-bands.d.ts +3 -3
  154. package/dist/register-bands.d.ts.map +1 -1
  155. package/dist/register-bands.js.map +1 -1
  156. package/dist/register.d.ts +2 -2
  157. package/dist/sources-record.d.ts +38 -0
  158. package/dist/sources-record.d.ts.map +1 -0
  159. package/dist/sources-record.js +123 -0
  160. package/dist/sources-record.js.map +1 -0
  161. package/dist/sources-rules.d.ts +44 -0
  162. package/dist/sources-rules.d.ts.map +1 -0
  163. package/dist/sources-rules.js +76 -0
  164. package/dist/sources-rules.js.map +1 -0
  165. package/dist/tex-paper.d.ts +10 -15
  166. package/dist/tex-paper.d.ts.map +1 -1
  167. package/dist/tex-paper.js +24 -40
  168. package/dist/tex-paper.js.map +1 -1
  169. package/dist/tex-requirements.js +1 -1
  170. package/dist/tex-venue-rules.d.ts +3 -3
  171. package/dist/tex-venue-rules.d.ts.map +1 -1
  172. package/dist/tex-venue-rules.js.map +1 -1
  173. package/dist/toolchain.js +1 -1
  174. package/docs/configuration.md +12 -4
  175. package/docs/design/paper-sources.md +197 -0
  176. package/docs/e2e.md +1 -1
  177. package/docs/incidents.md +5 -5
  178. package/docs/package-shape-options.md +1 -1
  179. package/docs/references.md +34 -8
  180. package/docs/rules/bib/reachable-entry.md +64 -0
  181. package/docs/rules/paper/sources-fresh.md +81 -0
  182. package/docs/rules.md +62 -57
  183. package/eslint-rules/CLAUDE.md +44 -0
  184. package/eslint-rules/CLAUDE.md.spec.ts +43 -0
  185. package/eslint-rules/install-path-literals.harness.mjs +1 -1
  186. package/eslint-rules/install-path-literals.ts +4 -4
  187. package/eslint-rules/paper-context.ts +4 -3
  188. package/eslint-rules/paper-typography.test.mjs +12 -72
  189. package/eslint-rules/paper-typography.ts +13 -6
  190. package/eslint-rules/pdf-last-page-balance.ts +1 -1
  191. package/eslint.config.mjs +28 -3
  192. package/fixtures/build-e2e/aidc/paper.tex +1 -1
  193. package/fixtures/build-e2e/anon-ack/paper.tex +1 -1
  194. package/fixtures/build-e2e/anon-author/paper.tex +1 -1
  195. package/fixtures/build-e2e/anon-bib/paper.tex +1 -1
  196. package/fixtures/build-e2e/anon-href/paper.tex +1 -1
  197. package/fixtures/build-e2e/anon-macro/paper.tex +1 -1
  198. package/fixtures/build-e2e/anon-pdfauthor/paper.tex +1 -1
  199. package/fixtures/paper-sources/p1/paper.tex +30 -0
  200. package/fixtures/paper-sources/p1/refs.bib +6 -0
  201. package/fixtures/paper-sources/p1/sections/dead.tex +6 -0
  202. package/fixtures/paper-sources/p1/sections/intro.tex +7 -0
  203. package/fixtures/paper-sources/p1/sections/parked.tex +6 -0
  204. package/fixtures/paper-sources/p1/tex-truth.json +37 -0
  205. package/fixtures/paper-sources/p1/versions/2026-01-01-review.tex +4 -0
  206. package/fixtures/paper-sources/v1-stale/paper.tex +14 -0
  207. package/fixtures/paper-sources/v1-stale/refs.bib +6 -0
  208. package/fixtures/paper-sources/v1-stale/tex-truth.json +32 -0
  209. package/fixtures/paper-sources/v10-ifthenelse/anon.bib +1 -0
  210. package/fixtures/paper-sources/v10-ifthenelse/paper.tex +9 -0
  211. package/fixtures/paper-sources/v10-ifthenelse/real.bib +1 -0
  212. package/fixtures/paper-sources/v10-ifthenelse/tex-truth.json +28 -0
  213. package/fixtures/paper-sources/v11-two-blocks/paper.tex +12 -0
  214. package/fixtures/paper-sources/v11-two-blocks/tex-truth.json +30 -0
  215. package/fixtures/paper-sources/v13-percent-text/paper.tex +6 -0
  216. package/fixtures/paper-sources/v13-percent-text/refs.bib +3 -0
  217. package/fixtures/paper-sources/v13-percent-text/tex-truth.json +30 -0
  218. package/fixtures/paper-sources/v15-after-end/paper.tex +6 -0
  219. package/fixtures/paper-sources/v15-after-end/refs.bib +1 -0
  220. package/fixtures/paper-sources/v15-after-end/tex-truth.json +17 -0
  221. package/fixtures/paper-sources/v16-included-block/bibblock.tex +3 -0
  222. package/fixtures/paper-sources/v16-included-block/paper.tex +7 -0
  223. package/fixtures/paper-sources/v16-included-block/refs.bib +1 -0
  224. package/fixtures/paper-sources/v16-included-block/tex-truth.json +35 -0
  225. package/fixtures/paper-sources/v17-include-order/bibsetup.tex +1 -0
  226. package/fixtures/paper-sources/v17-include-order/first.bib +1 -0
  227. package/fixtures/paper-sources/v17-include-order/later.bib +1 -0
  228. package/fixtures/paper-sources/v17-include-order/paper.tex +7 -0
  229. package/fixtures/paper-sources/v17-include-order/tex-truth.json +40 -0
  230. package/fixtures/paper-sources/v2-overwrite/paper.tex +14 -0
  231. package/fixtures/paper-sources/v2-overwrite/refs.bib +6 -0
  232. package/fixtures/paper-sources/v2-overwrite/tex-truth.json +30 -0
  233. package/fixtures/paper-sources/v22-parked-thebibliography/paper.tex +7 -0
  234. package/fixtures/paper-sources/v22-parked-thebibliography/tex-truth.json +17 -0
  235. package/fixtures/paper-sources/v23-parked-include/old.bib +1 -0
  236. package/fixtures/paper-sources/v23-parked-include/paper.tex +7 -0
  237. package/fixtures/paper-sources/v23-parked-include/parked.tex +1 -0
  238. package/fixtures/paper-sources/v23-parked-include/refs.bib +1 -0
  239. package/fixtures/paper-sources/v23-parked-include/tex-truth.json +28 -0
  240. package/fixtures/paper-sources/v24-quoted-fields/paper.tex +6 -0
  241. package/fixtures/paper-sources/v24-quoted-fields/refs.bib +5 -0
  242. package/fixtures/paper-sources/v24-quoted-fields/tex-truth.json +32 -0
  243. package/fixtures/paper-sources/v25-string-differs/paper.tex +10 -0
  244. package/fixtures/paper-sources/v25-string-differs/refs.bib +2 -0
  245. package/fixtures/paper-sources/v25-string-differs/tex-truth.json +32 -0
  246. package/fixtures/paper-sources/v26-comment-in-names/one.bib +1 -0
  247. package/fixtures/paper-sources/v26-comment-in-names/paper.tex +7 -0
  248. package/fixtures/paper-sources/v26-comment-in-names/tex-truth.json +32 -0
  249. package/fixtures/paper-sources/v26-comment-in-names/two.bib +1 -0
  250. package/fixtures/paper-sources/v27-shared-string/abbreviations.bib +2 -0
  251. package/fixtures/paper-sources/v27-shared-string/paper.tex +6 -0
  252. package/fixtures/paper-sources/v27-shared-string/references.bib +6 -0
  253. package/fixtures/paper-sources/v27-shared-string/tex-truth.json +31 -0
  254. package/fixtures/paper-sources/v28-shared-string-block/abbreviations.bib +2 -0
  255. package/fixtures/paper-sources/v28-shared-string-block/paper.tex +14 -0
  256. package/fixtures/paper-sources/v28-shared-string-block/tex-truth.json +33 -0
  257. package/fixtures/paper-sources/v29-generated-input/paper.tex +9 -0
  258. package/fixtures/paper-sources/v29-generated-input/tex-truth.json +24 -0
  259. package/fixtures/paper-sources/v3-declared/paper.bib +6 -0
  260. package/fixtures/paper-sources/v3-declared/paper.tex +6 -0
  261. package/fixtures/paper-sources/v3-declared/tex-truth.json +28 -0
  262. package/fixtures/paper-sources/v30-name-next-line/paper.tex +14 -0
  263. package/fixtures/paper-sources/v30-name-next-line/tex-truth.json +30 -0
  264. package/fixtures/paper-sources/v4-commented/paper.tex +14 -0
  265. package/fixtures/paper-sources/v4-commented/refs.bib +6 -0
  266. package/fixtures/paper-sources/v4-commented/tex-truth.json +28 -0
  267. package/fixtures/paper-sources/v5-percent-entry/paper.tex +20 -0
  268. package/fixtures/paper-sources/v5-percent-entry/tex-truth.json +37 -0
  269. package/fixtures/paper-sources/v6-unclosed/paper.tex +6 -0
  270. package/fixtures/paper-sources/v6-unclosed/refs.bib +5 -0
  271. package/fixtures/paper-sources/v6-unclosed/tex-truth.json +37 -0
  272. package/fixtures/paper-sources/v8-jobname/paper.tex +9 -0
  273. package/fixtures/paper-sources/v8-jobname/tex-truth.json +30 -0
  274. package/fixtures/paper-sources/v9-two-bibliographies/one.bib +1 -0
  275. package/fixtures/paper-sources/v9-two-bibliographies/paper.tex +7 -0
  276. package/fixtures/paper-sources/v9-two-bibliographies/tex-truth.json +35 -0
  277. package/fixtures/paper-sources/v9-two-bibliographies/two.bib +1 -0
  278. package/fixtures/paper-typography/clean-paper/paper.tex +1 -1
  279. package/fixtures/paper-typography/messy-paper/paper.tex +1 -1
  280. package/hooks/CLAUDE.md +34 -0
  281. package/hooks/CLAUDE.md.spec.ts +30 -0
  282. package/lib/agent-cli-version.ts +1 -1
  283. package/package.json +3 -11
  284. package/plugin/CLAUDE.md +22 -0
  285. package/plugin/CLAUDE.md.spec.ts +22 -0
  286. package/scripts/check.test.ts +23 -1
  287. package/scripts/check.ts +7 -3
  288. package/scripts/coverage-config.test.ts +21 -11
  289. package/scripts/layer-legacy-frozen.test.ts +41 -2
  290. package/scripts/layer-legacy-frozen.ts +31 -2
  291. package/skills/CLAUDE.md +19 -0
  292. package/skills/CLAUDE.md.spec.ts +18 -0
  293. package/skills/grade-paper-writing/SKILL.md +4 -3
  294. package/skills/grade-paper-writing/SKILL.md.spec.ts +3 -2
  295. package/skills/grade-paper-writing/prose-lint.mjs +11 -2
  296. package/skills/grade-paper-writing/prose-lint.test.ts +52 -4
  297. package/skills/paper-pipeline/scripts/extract-ref-facts.harness.mjs +83 -33
  298. package/skills/paper-pipeline/scripts/extract-ref-facts.mjs +67 -115
  299. package/skills/paper-pipeline/scripts/extract-ref-facts.test.mjs +128 -121
  300. package/skills/plan-paper-timeline/plan-paper-timeline.effects.harness.mjs +21 -102
  301. package/skills/verify-citations/SKILL.md +7 -4
  302. package/skills/verify-citations/SKILL.md.spec.ts +6 -3
  303. package/skills/verify-citations/scripts/bib-authors.check.test.mjs +17 -19
  304. package/skills/verify-citations/scripts/bib-authors.cli.test.ts +129 -46
  305. package/skills/verify-citations/scripts/bib-authors.d.mts +22 -2
  306. package/skills/verify-citations/scripts/bib-authors.mjs +57 -111
  307. package/skills/verify-citations/scripts/verify-cites.cli.test.ts +16 -0
  308. package/skills/verify-citations/scripts/verify-cites.d.mts +19 -3
  309. package/skills/verify-citations/scripts/verify-cites.mjs +42 -108
  310. package/skills/verify-citations/scripts/verify-cites.test.mjs +23 -36
  311. package/src/CLAUDE.md +23 -1
  312. package/src/CLAUDE.md.spec.ts +72 -0
  313. package/src/adapters/bibtex/index.test.ts +163 -0
  314. package/src/adapters/bibtex/index.ts +155 -0
  315. package/src/adapters/latex/filecontents.ts +114 -0
  316. package/src/adapters/latex/includes.ts +18 -10
  317. package/src/adapters/latex/index.test.ts +73 -0
  318. package/src/adapters/latex/index.ts +2 -0
  319. package/src/adapters/latex/nodes.ts +64 -0
  320. package/src/adapters/latex/parse.test.ts +38 -0
  321. package/src/adapters/latex/parse.ts +67 -6
  322. package/src/adapters/memory/index.ts +17 -0
  323. package/src/adapters/references/index.test.ts +75 -21
  324. package/src/adapters/references/index.ts +37 -32
  325. package/src/adapters/sources-record/index.test.ts +87 -0
  326. package/src/adapters/sources-record/index.ts +82 -0
  327. package/src/adapters/tex-output/blg.ts +27 -0
  328. package/src/adapters/tex-output/fls.ts +24 -0
  329. package/src/adapters/tex-output/index.test.ts +171 -0
  330. package/src/adapters/tex-output/index.ts +12 -0
  331. package/src/bib-rules.test.ts +356 -0
  332. package/src/bib-rules.ts +228 -0
  333. package/src/bibliography-at.test.ts +286 -0
  334. package/src/build.test.ts +210 -0
  335. package/src/build.ts +101 -36
  336. package/src/claim-provenance.test.ts +33 -7
  337. package/src/claim-provenance.ts +2 -2
  338. package/src/cli.harness.mjs +20 -10
  339. package/src/cli.test.ts +14 -10
  340. package/src/cli.ts +37 -28
  341. package/src/domain/paper-source.test.ts +58 -20
  342. package/src/domain/paper-source.ts +38 -42
  343. package/src/domain/paper-sources.ts +94 -0
  344. package/src/domain/sources-record.test.ts +71 -0
  345. package/src/domain/sources-record.ts +136 -0
  346. package/src/domain/tex-run.test.ts +228 -0
  347. package/src/domain/tex-run.ts +139 -0
  348. package/src/heading-case.test.ts +24 -10
  349. package/src/latex-log.ts +20 -0
  350. package/src/latex-loop.harness.ts +1 -0
  351. package/src/latex-loop.ts +2 -0
  352. package/src/linted-set.test.ts +9 -1
  353. package/src/new-paper.harness.mjs +3 -2
  354. package/src/new-paper.test.ts +9 -5
  355. package/src/package-dirs.ts +3 -2
  356. package/src/paper-includes.test.ts +157 -38
  357. package/src/paper-includes.ts +39 -35
  358. package/src/paper-prose.test.ts +34 -5
  359. package/src/paper-prose.ts +2 -2
  360. package/src/paper-record.test.ts +173 -0
  361. package/src/paper-record.ts +66 -0
  362. package/src/paper-settings-commands.test.ts +5 -2
  363. package/src/paper-sources.ts +151 -0
  364. package/src/ports/bib-reader.ts +27 -0
  365. package/src/ports/check-references.ts +7 -3
  366. package/src/ports/latex.ts +19 -0
  367. package/src/ports/sources-codec.ts +11 -0
  368. package/src/ports/tex-output.ts +17 -0
  369. package/src/recorded-bibliography.ts +297 -0
  370. package/src/reference-rules.ts +217 -104
  371. package/src/references.test.ts +520 -177
  372. package/src/references.ts +185 -37
  373. package/src/register-bands.test.ts +5 -1
  374. package/src/register-bands.ts +3 -5
  375. package/src/register.test.ts +6 -1
  376. package/src/register.ts +2 -2
  377. package/src/rule-cases.test.ts +68 -12
  378. package/src/rule-docs.test.ts +0 -1
  379. package/src/sources-record.test.ts +308 -0
  380. package/src/sources-record.ts +231 -0
  381. package/src/sources-rules.test.ts +152 -0
  382. package/src/sources-rules.ts +117 -0
  383. package/src/tex-paper.test.ts +148 -78
  384. package/src/tex-paper.ts +31 -61
  385. package/src/tex-requirements.ts +1 -1
  386. package/src/tex-venue-rules.test.ts +36 -8
  387. package/src/tex-venue-rules.ts +3 -5
  388. package/src/toolchain.ts +1 -1
  389. package/dist/eslint-rules/bib-reachable-entry.d.ts +0 -22
  390. package/dist/eslint-rules/bib-reachable-entry.d.ts.map +0 -1
  391. package/dist/eslint-rules/bib-reachable-entry.js +0 -75
  392. package/dist/eslint-rules/bib-reachable-entry.js.map +0 -1
  393. package/eslint-rules/bib-reachable-entry.ts +0 -76
package/src/references.ts CHANGED
@@ -8,14 +8,16 @@
8
8
  * "bib-authors" in a scorecard table, or a command the project configured. Now the build runs
9
9
  * them and records the result as data, beside the facts it already writes about the PDF:
10
10
  *
11
- * { "schema": 1,
12
- * "bib": { "source": "paper.tex", "sha256": "…" }, the bibliography that was checked
11
+ * { "schema": 3,
12
+ * "bib": { "sources": ["refs.bib"], "sha256": "…" }, the databases that were checked, as
13
+ * bibtex names them, and a hash of their text
13
14
  * "status": "checked" | "not-checked", "why": "…", not-checked: nothing could be asked
14
15
  * "entries": [ { "key", "exists", "authors", "why" } ] }
16
+ * one per entry checked, in its order
15
17
  *
16
18
  * A sibling file, not a new field of `paper.facts.json`: those facts are about ONE PDF and are
17
- * stale when it changes (`pdf_sha256`); these are about the bibliography and are stale when IT
18
- * changes (`bib.sha256`). One staleness key per file, the way `pdf/fresh` already works.
19
+ * stale when it changes (`pdf_sha256`); these are about the databases and are stale when THEY
20
+ * change (`bib.sha256`). One staleness key per file, the way `pdf/fresh` already works.
19
21
  *
20
22
  * 🔴 WHAT THE CHECKERS ANSWERED IS KEPT, COMMITTED, IN `<paper>/repro/references-cache.json`
21
23
  * (`domain/lookup-cache.ts`). Loaded before the check and saved after it — only when something new
@@ -23,6 +25,18 @@
23
25
  * MAX_AGE_DAYS, not once per build (#107: 217 s for 27 references, every build). The verdicts above are NOT cached: they are derived from the cached
24
26
  * answers on every build, so a fix to the checkers reaches every paper at once.
25
27
  *
28
+ * ── WHICH DATABASES, AND WHAT BIBTEX TYPESET ───────────────────────────────────────────
29
+ * The databases checked are the ones the build's bibtex OPENED (`_build/sources.json`, written by
30
+ * `sources-record.ts` from TeX's own files; docs/design/paper-sources.md §1) — nothing here reads
31
+ * TeX source to decide which. Their entries are read by the one bibtex reader. bibtex reads
32
+ * differently from it in one place: it has no comment syntax, so an entry behind `%` or inside
33
+ * `@comment{…}` is one it typesets and the reader does not see. The record holds the keys bibtex
34
+ * typeset, so `paper/refs-checked` names the ones no entry of those databases has (`unseenKeys`).
35
+ * A malformed `.bib` never gets here: bibtex fails on it, and the build fails with bibtex's own lines.
36
+ *
37
+ * Without a record of the build, or with one the paper has changed since, there is no answer to
38
+ * "which databases": the step checks nothing, and the rules are silent — `paper/sources-fresh` speaks.
39
+ *
26
40
  * 🔴 THE STEP NEVER FAILS THE BUILD. A build without network still builds the PDF; the reference
27
41
  * step records `not-checked` and the lint rule says so, as a warning. Recording "not checked" as
28
42
  * a pass would be the counter that counts what it never looked at.
@@ -44,10 +58,22 @@ import type {
44
58
  ReferencesCheck,
45
59
  ReferencesRun,
46
60
  } from "./ports/check-references.ts";
47
- import { bibRange } from "#eslint-rules/paper-typography";
48
61
  import { messageOf } from "./domain/text.ts";
62
+ import {
63
+ describeChanges,
64
+ type SourcesRecord,
65
+ } from "./domain/sources-record.ts";
66
+ import type { PaperRecord } from "./paper-record.ts";
67
+ import {
68
+ entriesOfDatabases,
69
+ recordedBibliography,
70
+ type ReadDatabase,
71
+ type RecordedEntry,
72
+ type RecordedReadDeps,
73
+ } from "./recorded-bibliography.ts";
49
74
 
50
- export const REFERENCES_SCHEMA = 1;
75
+ /** 3: the record of what bibtex read lives in `_build/sources.json`; an older one reads as none. */
76
+ export const REFERENCES_SCHEMA = 3;
51
77
  export const REFERENCES_FILE = "references.json";
52
78
 
53
79
  /** The lookup cache's place in a paper: committed, beside the paper's other reproduction files. */
@@ -65,44 +91,127 @@ const text = (files: Files, p: string): string | null => {
65
91
  return b === null ? null : new TextDecoder().decode(b);
66
92
  };
67
93
 
68
- /** The bibliography a paper carries: inside `paper.tex` (`filecontents`), else `refs.bib`. */
69
- export interface Bibliography {
70
- readonly source: "paper.tex" | "refs.bib";
71
- readonly text: string;
72
- /** Where `text` starts in the source file — so a finding can point at an entry. */
73
- readonly offset: number;
94
+ /** `xs` without the elements whose `key` an earlier one has, in order. */
95
+ const distinctBy = <T>(xs: readonly T[], key: (x: T) => string): readonly T[] =>
96
+ xs.filter((x, i) => xs.findIndex((o) => key(o) === key(x)) === i);
97
+
98
+ /**
99
+ * The bibliography the build checks: the databases bibtex opened, in the order it opened them, and
100
+ * their entries as the bibtex reader read them. The verdicts are recorded one per entry, in this
101
+ * order: a verdict is about an entry, not about its key.
102
+ */
103
+ export interface CheckedBibliography {
104
+ /** The build's record these databases were read from. */
105
+ readonly record: SourcesRecord;
106
+ readonly databases: readonly [ReadDatabase, ...ReadDatabase[]];
107
+ readonly entries: readonly RecordedEntry[];
108
+ /** The keys bibtex typeset, in the order it did: the `.bbl`'s `\bibitem`s. */
109
+ readonly typeset: readonly string[];
74
110
  }
75
111
 
76
- export function bibliographyOf(
77
- files: Files,
78
- paperDir: string,
79
- ): Bibliography | null {
80
- const tex = text(files, join(paperDir, "paper.tex"));
81
- const inline = tex === null ? null : bibRange(tex);
82
- if (inline)
83
- return { source: "paper.tex", text: inline.body, offset: inline.bodyStart };
84
- const bib = text(files, join(paperDir, "refs.bib"));
85
- return bib === null ? null : { source: "refs.bib", text: bib, offset: 0 };
112
+ /** What reading the bibliography takes: the record's readers, and the bibtex reader. */
113
+ export type BibliographyDeps = RecordedReadDeps;
114
+
115
+ /** What a paper's record comes to for the references. */
116
+ export type BibliographyRead =
117
+ | { readonly kind: "read"; readonly bibliography: CheckedBibliography }
118
+ /** No record of the build, an unreadable one, or one the paper has changed since. */
119
+ | {
120
+ readonly kind: "no-record";
121
+ readonly record: Exclude<PaperRecord, { readonly kind: "fresh" }>;
122
+ }
123
+ /** bibtex opened no database: the build ran none, or the paper names none. */
124
+ | { readonly kind: "nothing" }
125
+ /** Databases bibtex opened that are not in the paper directory: it found them on its search path. */
126
+ | { readonly kind: "unread"; readonly databases: readonly string[] }
127
+ /** The build was not given a reader. */
128
+ | { readonly kind: "not-wired" };
129
+
130
+ /**
131
+ * The bibliography the build recorded for the paper in `dir`: the databases bibtex opened, read by
132
+ * the bibtex reader. Only a fresh record answers (`paperRecord`).
133
+ */
134
+ export function readBibliography(
135
+ dir: string,
136
+ deps: BibliographyDeps,
137
+ ): BibliographyRead {
138
+ const rec = recordedBibliography(dir, deps);
139
+ if (rec.kind === "unrecorded")
140
+ return { kind: "no-record", record: rec.record };
141
+ if (rec.unread.length > 0) return { kind: "unread", databases: rec.unread };
142
+ const [first, ...rest] = rec.databases;
143
+ const bibtex = rec.record.bibtex;
144
+ if (first === undefined || !bibtex.ran) return { kind: "nothing" };
145
+ return {
146
+ kind: "read",
147
+ bibliography: {
148
+ record: rec.record,
149
+ databases: [first, ...rest],
150
+ entries: entriesOfDatabases(rec.databases),
151
+ typeset: bibtex.keys,
152
+ },
153
+ };
154
+ }
155
+
156
+ /** A paper's bibliography by its directory — what the build's steps are handed by the composition root. */
157
+ export type ReadBibliography = (dir: string) => BibliographyRead;
158
+
159
+ /** `readBibliography` over `deps`, as a `ReadBibliography`. */
160
+ export const bibliographyReader =
161
+ (deps: BibliographyDeps): ReadBibliography =>
162
+ (dir) =>
163
+ readBibliography(dir, deps);
164
+
165
+ /** No reader: the build was not given one. Never "no bibliography" — it says so. */
166
+ export const notWiredBibliography: ReadBibliography = () => ({
167
+ kind: "not-wired",
168
+ });
169
+
170
+ /**
171
+ * The keys bibtex typeset that no entry the reader read has — an entry behind `%` or inside
172
+ * `@comment{…}`, which bibtex reads and the reader does not. In the order bibtex typeset them, each
173
+ * once. bibtex's keys are case-insensitive (`\cite{SMITH}` typesets `\bibitem{SMITH}` from an entry
174
+ * `smith`, measured), so they are compared so.
175
+ */
176
+ export function unseenKeys(b: CheckedBibliography): readonly string[] {
177
+ const read = new Set(b.entries.map((e) => e.entry.key.toLowerCase()));
178
+ return distinctBy(
179
+ b.typeset.filter((k) => !read.has(k.toLowerCase())),
180
+ (k) => k.toLowerCase(),
181
+ );
86
182
  }
87
183
 
88
- export const bibHash = (b: Bibliography): string =>
89
- sha256Hex(new TextEncoder().encode(b.text));
184
+ /** The bytes of the databases checked, in order: what a later lint compares to see the verdicts are current. */
185
+ export const bibHash = (b: CheckedBibliography): string =>
186
+ sha256Hex(
187
+ new TextEncoder().encode(
188
+ b.databases
189
+ .map((d) => d.bib.text.slice(d.bib.body.start, d.bib.body.end))
190
+ .join("\n"),
191
+ ),
192
+ );
90
193
 
91
194
  /** What `_build/references.json` holds. */
92
195
  export interface ReferencesDocument {
93
196
  readonly schema: number;
94
- readonly bib: { readonly source: string; readonly sha256: string };
197
+ readonly bib: {
198
+ readonly sources: readonly string[];
199
+ readonly sha256: string;
200
+ };
95
201
  readonly status: "checked" | "not-checked";
96
202
  readonly why?: string;
97
203
  readonly entries: readonly EntryVerdict[];
98
204
  }
99
205
 
100
206
  export const documentOf = (
101
- bib: Bibliography,
207
+ bib: CheckedBibliography,
102
208
  check: ReferencesCheck,
103
209
  ): ReferencesDocument => ({
104
210
  schema: REFERENCES_SCHEMA,
105
- bib: { source: bib.source, sha256: bibHash(bib) },
211
+ bib: {
212
+ sources: bib.databases.map((d) => d.name),
213
+ sha256: bibHash(bib),
214
+ },
106
215
  ...(check.kind === "checked"
107
216
  ? { status: "checked" as const, entries: check.entries }
108
217
  : { status: "not-checked" as const, why: check.why, entries: [] }),
@@ -156,32 +265,66 @@ const newAnswers = (before: LookupCache, after: LookupCache): number => {
156
265
  );
157
266
  };
158
267
 
159
- /** Run the checker; a throw is `not-checked`, with the cache as it was. */
268
+ /** Run the checker over the bibliography's entries; a throw is `not-checked`, with the cache as it was. */
160
269
  async function run(
161
270
  check: CheckReferences,
162
- bib: string,
271
+ bib: CheckedBibliography,
163
272
  cache: LookupCache,
164
273
  ): Promise<ReferencesRun> {
165
274
  try {
166
- return await check(bib, cache);
275
+ return await check(
276
+ bib.entries.map((f) => f.entry),
277
+ cache,
278
+ );
167
279
  } catch (e) {
168
280
  return { check: { kind: "not-checked", why: messageOf(e) }, cache };
169
281
  }
170
282
  }
171
283
 
284
+ /** Why the build's record cannot say which databases were read, in the words a note carries. */
285
+ function noRecord(
286
+ r: Extract<BibliographyRead, { readonly kind: "no-record" }>["record"],
287
+ ): string {
288
+ switch (r.kind) {
289
+ case "none":
290
+ return r.unreadable === null
291
+ ? "the build recorded no sources (_build/sources.json was not written)"
292
+ : `_build/sources.json cannot be used (${r.unreadable})`;
293
+ case "stale":
294
+ return `the paper changed since the build recorded it (${describeChanges(r.changed)})`;
295
+ }
296
+ }
297
+
298
+ /** The note for a paper whose bibliography was not read. */
299
+ function unreadNote(
300
+ read: Exclude<BibliographyRead, { readonly kind: "read" }>,
301
+ ): string {
302
+ switch (read.kind) {
303
+ case "no-record":
304
+ return `references NOT checked — ${noRecord(read.record)}; lint will say so`;
305
+ case "nothing":
306
+ return "no bibliography database — bibtex opened none, nothing to check";
307
+ case "unread":
308
+ return `references NOT checked — ${read.databases.join(", ")}, which bibtex opened, is not in the paper directory; lint will say so`;
309
+ case "not-wired":
310
+ return "references NOT checked — no bibliography reader was wired into this build; lint will say so";
311
+ }
312
+ }
313
+
172
314
  /**
173
- * The build step's work: check the bibliography, serving what it can from the paper's lookup
174
- * cache, and record the verdicts. Returns the one-line note the build prints. A checker that
175
- * throws, or a cache that does not parse, is recorded as `not-checked` — the step reports, it
176
- * never refuses. A cache that does not parse is left as it is, and named.
315
+ * The build step's work: check the bibliography the build recorded, serving what it can from the
316
+ * paper's lookup cache, and record the verdicts. Returns the one-line note the build prints. A
317
+ * checker that throws, or a cache that does not parse, is recorded as `not-checked` — the step
318
+ * reports, it never refuses. A cache that does not parse is left as it is, and named.
177
319
  */
178
320
  export async function recordReferences(
179
321
  files: Files,
180
322
  paperDir: string,
323
+ read: BibliographyRead,
181
324
  check: CheckReferences,
182
325
  ): Promise<string> {
183
- const bib = bibliographyOf(files, paperDir);
184
- if (bib === null) return "no bibliography — nothing to check";
326
+ if (read.kind !== "read") return unreadNote(read);
327
+ const bib = read.bibliography;
185
328
  const record = (c: ReferencesCheck): ReferencesDocument => {
186
329
  const doc = documentOf(bib, c);
187
330
  files.writeAtomic(
@@ -199,7 +342,7 @@ export async function recordReferences(
199
342
  return notChecked(
200
343
  `${LOOKUP_CACHE_FILE} cannot be read (${cache.error}) — fix it, or delete it to ask every question again`,
201
344
  );
202
- const result = await run(check, bib.text, cache.value);
345
+ const result = await run(check, bib, cache.value);
203
346
  if (result.check.kind === "not-checked") return notChecked(result.check.why);
204
347
  const doc = record(result.check);
205
348
  const fetched = newAnswers(cache.value, result.cache);
@@ -208,6 +351,11 @@ export async function recordReferences(
208
351
  callerPath(lookupCachePath(paperDir)),
209
352
  new TextEncoder().encode(serializeLookupCache(result.cache)),
210
353
  );
354
+ return summary(doc, fetched);
355
+ }
356
+
357
+ /** The note a checked run prints: how many entries, how many failing, what was fetched. */
358
+ function summary(doc: ReferencesDocument, fetched: number): string {
211
359
  const bad = doc.entries.filter(
212
360
  (e) => e.exists === "false" || e.authors === "mismatch",
213
361
  ).length;
@@ -17,6 +17,7 @@ import { fileURLToPath } from "node:url";
17
17
  import { describe, expect, it } from "vitest";
18
18
  import { latexReader } from "./adapters/latex/index.ts";
19
19
  import { nodeFiles } from "./adapters/node/index.ts";
20
+ import { sourcesCodec } from "./adapters/sources-record/index.ts";
20
21
  import {
21
22
  bandOf,
22
23
  rateOf,
@@ -36,6 +37,7 @@ import {
36
37
  standingOf,
37
38
  } from "./register-bands.ts";
38
39
  import { readPaper } from "./tex-paper.ts";
40
+ import { withRecord } from "../test/recorded-fixture.ts";
39
41
  import { useTempDir, writeTree } from "../test/support.ts";
40
42
 
41
43
  const ROOT = dirname(dirname(fileURLToPath(import.meta.url)));
@@ -56,8 +58,9 @@ const aidc = (() => {
56
58
  const sourceOf = (paper: string): string => {
57
59
  const file = join(CORPUS, paper, "paper.tex");
58
60
  return readPaper(file, readFileSync(file, "utf8"), {
59
- files: nodeFiles,
61
+ files: withRecord(nodeFiles, join(CORPUS, paper)),
60
62
  latex: latexReader,
63
+ codec: sourcesCodec,
61
64
  }).text;
62
65
  };
63
66
 
@@ -286,6 +289,7 @@ describe("the rules, as ESLint calls them", () => {
286
289
  files: nodeFiles,
287
290
  venuesDir: presetsDir(),
288
291
  latex: latexReader,
292
+ codec: sourcesCodec,
289
293
  };
290
294
  registerBandRules(deps)
291
295
  [rule].create({
@@ -40,7 +40,7 @@ import type { Emphasis, Passage, Span } from "./domain/tex-document.ts";
40
40
  import type { LatexReader } from "./ports/latex.ts";
41
41
  import { paperPreset } from "./presets.ts";
42
42
  import { MIN_WORDS, startOf } from "./register.ts";
43
- import { readPaper, reportInPaper } from "./tex-paper.ts";
43
+ import { readPaper, reportInPaper, type ProseDeps } from "./tex-paper.ts";
44
44
  import {
45
45
  ruleDocsUrl,
46
46
  type Located,
@@ -49,10 +49,8 @@ import {
49
49
  } from "./tex-venue-rules.ts";
50
50
  import type { VenueRuleDeps } from "./venue-rules.ts";
51
51
 
52
- /** What the three rules read: the preset store, and the LaTeX reader. */
53
- export interface RegisterBandDeps extends VenueRuleDeps {
54
- readonly latex: LatexReader;
55
- }
52
+ /** What the three rules read: the preset store, the LaTeX reader, and the paper's files as the build recorded them. */
53
+ export interface RegisterBandDeps extends VenueRuleDeps, ProseDeps {}
56
54
 
57
55
  export type RegisterBandRuleName =
58
56
  "contrast-frames" | "claim-emphasis" | "relation-markers";
@@ -7,6 +7,7 @@
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
  judgeRegister,
12
13
  measureRegister,
@@ -125,7 +126,11 @@ describe("the rule — reported at the start of the body, the option read from t
125
126
  }
126
127
  const run = (src: string, options?: readonly unknown[]): Seen[] => {
127
128
  const seen: Seen[] = [];
128
- registerRule({ files: memoryFiles({}), latex: latexReader })
129
+ registerRule({
130
+ files: memoryFiles({}),
131
+ latex: latexReader,
132
+ codec: sourcesCodec,
133
+ })
129
134
  .create({
130
135
  filename: "/p/paper.tex",
131
136
  ...(options === undefined ? {} : { options }),
package/src/register.ts CHANGED
@@ -17,7 +17,7 @@
17
17
  */
18
18
  import type { Passage, Span } from "./domain/tex-document.ts";
19
19
  import { bodySentences, wordsOf } from "./domain/register.ts";
20
- import { readPaper, reportInPaper, type PaperDeps } from "./tex-paper.ts";
20
+ import { readPaper, reportInPaper, type ProseDeps } from "./tex-paper.ts";
21
21
  import {
22
22
  ruleDocsUrl,
23
23
  type Located,
@@ -125,7 +125,7 @@ const META: TexRuleModule["meta"] = {
125
125
  };
126
126
 
127
127
  /** The rule, over the body's prose of the whole paper, its includes spliced (`readPaper`). */
128
- export const registerRule = (deps: PaperDeps): TexRuleModule => ({
128
+ export const registerRule = (deps: ProseDeps): TexRuleModule => ({
129
129
  meta: META,
130
130
  create(context) {
131
131
  return {
@@ -17,7 +17,7 @@ import { describe, expect, it } from "vitest";
17
17
  import { z } from "zod";
18
18
  import { run, rulePlugins } from "./cli.ts";
19
19
  import { sha256Hex } from "./domain/sha256.ts";
20
- import { bibHash } from "./references.ts";
20
+ import { recordText } from "../test/recorded-fixture.ts";
21
21
  import { useTempDir, venuePreset, writeTree } from "../test/support.ts";
22
22
 
23
23
  const ROOT = dirname(dirname(fileURLToPath(import.meta.url)));
@@ -172,32 +172,84 @@ const built = (
172
172
 
173
173
  const BIB =
174
174
  "@article{a,\n title = {A},\n author = {Doe, J.},\n year = {2026},\n doi = {10.1/x}\n}\n";
175
- const sha = bibHash({ source: "refs.bib", text: BIB, offset: 0 });
176
- /** A paper citing refs.bib, with its references record (`status`, `entries`) or none. */
175
+ /** The hash the build records for a bibliography that is BIB alone (`bibHash` over its one text). */
176
+ const sha: string = sha256Hex(new TextEncoder().encode(BIB));
177
+ /** The paper `cited` builds: it cites refs.bib. */
178
+ const CITING = tex("See~\\cite{a}.\n\\bibliography{refs}");
179
+ const digest = (text: string): string =>
180
+ sha256Hex(new TextEncoder().encode(text));
181
+ /**
182
+ * A paper citing refs.bib that was built — TeX read paper.tex, bibtex opened refs.bib and typeset `a`
183
+ * — with its references record (`status`, `entries`) or none.
184
+ */
177
185
  const cited = (
178
186
  body: Record<string, unknown> | null,
179
187
  bibSha = sha,
180
188
  ): Record<string, string> =>
181
- paper(tex("See~\\cite{a}.\n\\bibliography{refs}"), {
189
+ paper(CITING, {
182
190
  [`${P}/refs.bib`]: BIB,
191
+ [`${P}/_build/sources.json`]: JSON.stringify({
192
+ schema: 1,
193
+ inputs: [{ path: "paper.tex", role: "body" }],
194
+ written: [],
195
+ bibdata: ["refs"],
196
+ bibtex: {
197
+ ran: true,
198
+ databases: ["refs.bib"],
199
+ keys: ["a"],
200
+ exit: 0,
201
+ errors: [],
202
+ },
203
+ sha256: { "paper.tex": digest(CITING), "refs.bib": digest(BIB) },
204
+ }),
183
205
  ...(body === null
184
206
  ? {}
185
207
  : {
186
208
  [`${P}/_build/references.json`]: JSON.stringify({
187
- schema: 1,
188
- bib: { source: "refs.bib", sha256: bibSha },
209
+ schema: 3,
210
+ bib: { sources: ["refs.bib"], sha256: bibSha },
189
211
  ...body,
190
212
  }),
191
213
  }),
192
214
  });
193
- /** A paper whose bibliography is written inline, by `filecontents*`, as `bib/reachable-entry` reads it. */
215
+ /** A paper whose bibliography is written inline, by `filecontents*[overwrite]`. */
194
216
  const inlineBib = (entry: string): string =>
195
- `\\documentclass{article}\n\\begin{filecontents*}{refs.bib}\n${entry}\n\\end{filecontents*}\n\\begin{document}\nSee~\\cite{a}.\n\\bibliography{refs}\n\\end{document}\n`;
217
+ `\\documentclass{article}\n\\begin{filecontents*}[overwrite]{refs.bib}\n${entry}\n\\end{filecontents*}\n\\begin{document}\nSee~\\cite{a}.\n\\bibliography{refs}\n\\end{document}\n`;
218
+ /** That paper as a build leaves it: the `.bib` TeX wrote from the block, and the record of the build. */
219
+ const inlineBuilt = (entry: string): Record<string, string> => {
220
+ const paperTex = inlineBib(entry);
221
+ return paper(paperTex, {
222
+ [`${P}/refs.bib`]: `${entry}\n`,
223
+ [`${P}/_build/sources.json`]: recordText(
224
+ { "paper.tex": paperTex, "refs.bib": `${entry}\n` },
225
+ {
226
+ inputs: [["paper.tex", "body"]],
227
+ written: ["refs.bib"],
228
+ databases: ["refs.bib"],
229
+ },
230
+ ),
231
+ });
232
+ };
196
233
  const checked = (exists: string, authors: string) => ({
197
234
  status: "checked",
198
235
  entries: [{ key: "a", exists, authors }],
199
236
  });
200
237
 
238
+ // ── the build's record of the sources: what `paperlint build` would write after TeX read paper.tex ───
239
+
240
+ /** A paper that was built: the record of its one file, hashed as it stands. */
241
+ const sourced = (paperTex: string): Record<string, string> =>
242
+ paper(paperTex, {
243
+ [`${P}/_build/sources.json`]: JSON.stringify({
244
+ schema: 1,
245
+ inputs: [{ path: "paper.tex", role: "body" }],
246
+ written: [],
247
+ bibdata: [],
248
+ bibtex: { ran: false },
249
+ sha256: { "paper.tex": sha256Hex(new TextEncoder().encode(paperTex)) },
250
+ }),
251
+ });
252
+
201
253
  // ── a BlindConf paper, for the LaTeX venue rules ──────────────────────────────────────────────
202
254
 
203
255
  const BLIND_PAPER = onBlindConf(P);
@@ -517,7 +569,7 @@ const CASES: Readonly<Record<string, RuleCases>> = {
517
569
  tree: cited(checked("true", "mismatch")),
518
570
  file: TEX_FILE,
519
571
  severity: 2,
520
- line: 1,
572
+ line: 1, // refs.bib is a file the author keeps: the top of paper.tex, naming it
521
573
  },
522
574
  silent: cited(checked("true", "match")),
523
575
  },
@@ -526,7 +578,7 @@ const CASES: Readonly<Record<string, RuleCases>> = {
526
578
  tree: cited(checked("false", "match")),
527
579
  file: TEX_FILE,
528
580
  severity: 2,
529
- line: 1,
581
+ line: 1, // refs.bib is a file the author keeps: the top of paper.tex, naming it
530
582
  },
531
583
  silent: cited(checked("true", "match")),
532
584
  },
@@ -543,6 +595,10 @@ const CASES: Readonly<Record<string, RuleCases>> = {
543
595
  },
544
596
  silent: cited(checked("true", "match")),
545
597
  },
598
+ "paper/sources-fresh": {
599
+ reports: { tree: paper(tex("x")), file: TEX_FILE, severity: 1, line: 1 },
600
+ silent: sourced(tex("x")),
601
+ },
546
602
  "review/frontmatter": {
547
603
  reports: {
548
604
  tree: paper(tex("x"), {
@@ -697,12 +753,12 @@ const CASES: Readonly<Record<string, RuleCases>> = {
697
753
  },
698
754
  "bib/reachable-entry": {
699
755
  reports: {
700
- tree: paper(inlineBib("@book{a,\n title = {A},\n year = {2026}\n}")),
756
+ tree: inlineBuilt("@book{a,\n title = {A},\n year = {2026}\n}"),
701
757
  file: TEX_FILE,
702
758
  severity: 1,
703
759
  line: 3,
704
760
  },
705
- silent: paper(inlineBib(BIB)),
761
+ silent: inlineBuilt(BIB.trimEnd()),
706
762
  },
707
763
  "pdf/fresh": {
708
764
  reports: onPdf(built({ pdf_sha256: "0".repeat(64) })),
@@ -19,7 +19,6 @@ const PAGES = "https://github.com/zernie/paperlint/blob/main/";
19
19
 
20
20
  /** Rules shipped before per-rule pages, each waiting for its page in #131. Only ever shrinks. */
21
21
  const AWAITING_PAGE: ReadonlySet<string> = new Set([
22
- "bib/reachable-entry",
23
22
  "paper/author-list",
24
23
  "paper/cite-exists",
25
24
  "paper/figure-ref-style",