paperlint 7.2.0 → 9.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (474) hide show
  1. package/CLAUDE.md +9 -25
  2. package/CONTRIBUTING.md +5 -4
  3. package/README.md +4 -3
  4. package/dist/adapters/bibtex/index.d.ts +3 -0
  5. package/dist/adapters/bibtex/index.d.ts.map +1 -0
  6. package/dist/adapters/bibtex/index.js +103 -0
  7. package/dist/adapters/bibtex/index.js.map +1 -0
  8. package/dist/adapters/latex/filecontents.d.ts +33 -0
  9. package/dist/adapters/latex/filecontents.d.ts.map +1 -0
  10. package/dist/adapters/latex/filecontents.js +65 -0
  11. package/dist/adapters/latex/filecontents.js.map +1 -0
  12. package/dist/adapters/latex/includes.d.ts +3 -3
  13. package/dist/adapters/latex/includes.d.ts.map +1 -1
  14. package/dist/adapters/latex/includes.js +18 -10
  15. package/dist/adapters/latex/includes.js.map +1 -1
  16. package/dist/adapters/latex/index.d.ts.map +1 -1
  17. package/dist/adapters/latex/index.js +2 -0
  18. package/dist/adapters/latex/index.js.map +1 -1
  19. package/dist/adapters/latex/nodes.d.ts +12 -0
  20. package/dist/adapters/latex/nodes.d.ts.map +1 -1
  21. package/dist/adapters/latex/nodes.js +39 -0
  22. package/dist/adapters/latex/nodes.js.map +1 -1
  23. package/dist/adapters/latex/parse.d.ts +7 -1
  24. package/dist/adapters/latex/parse.d.ts.map +1 -1
  25. package/dist/adapters/latex/parse.js +55 -6
  26. package/dist/adapters/latex/parse.js.map +1 -1
  27. package/dist/adapters/memory/index.d.ts +3 -0
  28. package/dist/adapters/memory/index.d.ts.map +1 -1
  29. package/dist/adapters/memory/index.js +9 -0
  30. package/dist/adapters/memory/index.js.map +1 -1
  31. package/dist/adapters/references/index.d.ts.map +1 -1
  32. package/dist/adapters/references/index.js +27 -24
  33. package/dist/adapters/references/index.js.map +1 -1
  34. package/dist/adapters/sources-record/index.d.ts +9 -0
  35. package/dist/adapters/sources-record/index.d.ts.map +1 -0
  36. package/dist/adapters/sources-record/index.js +68 -0
  37. package/dist/adapters/sources-record/index.js.map +1 -0
  38. package/dist/adapters/tex-output/blg.d.ts +9 -0
  39. package/dist/adapters/tex-output/blg.d.ts.map +1 -0
  40. package/dist/adapters/tex-output/blg.js +20 -0
  41. package/dist/adapters/tex-output/blg.js.map +1 -0
  42. package/dist/adapters/tex-output/fls.d.ts +8 -0
  43. package/dist/adapters/tex-output/fls.d.ts.map +1 -0
  44. package/dist/adapters/tex-output/fls.js +16 -0
  45. package/dist/adapters/tex-output/fls.js.map +1 -0
  46. package/dist/adapters/tex-output/index.d.ts +7 -0
  47. package/dist/adapters/tex-output/index.d.ts.map +1 -0
  48. package/dist/adapters/tex-output/index.js +7 -0
  49. package/dist/adapters/tex-output/index.js.map +1 -0
  50. package/dist/bib-rules.d.ts +47 -0
  51. package/dist/bib-rules.d.ts.map +1 -0
  52. package/dist/bib-rules.js +127 -0
  53. package/dist/bib-rules.js.map +1 -0
  54. package/dist/build.d.ts +29 -1
  55. package/dist/build.d.ts.map +1 -1
  56. package/dist/build.js +41 -30
  57. package/dist/build.js.map +1 -1
  58. package/dist/claim-provenance.d.ts +2 -2
  59. package/dist/cli.d.ts +9 -5
  60. package/dist/cli.d.ts.map +1 -1
  61. package/dist/cli.js +43 -68
  62. package/dist/cli.js.map +1 -1
  63. package/dist/domain/paper-source.d.ts +1 -10
  64. package/dist/domain/paper-source.d.ts.map +1 -1
  65. package/dist/domain/paper-source.js +34 -24
  66. package/dist/domain/paper-source.js.map +1 -1
  67. package/dist/domain/paper-sources.d.ts +72 -0
  68. package/dist/domain/paper-sources.d.ts.map +1 -0
  69. package/dist/domain/paper-sources.js +18 -0
  70. package/dist/domain/paper-sources.js.map +1 -0
  71. package/dist/domain/sources-record.d.ts +102 -0
  72. package/dist/domain/sources-record.d.ts.map +1 -0
  73. package/dist/domain/sources-record.js +38 -0
  74. package/dist/domain/sources-record.js.map +1 -0
  75. package/dist/domain/tex-run.d.ts +25 -0
  76. package/dist/domain/tex-run.d.ts.map +1 -0
  77. package/dist/domain/tex-run.js +79 -0
  78. package/dist/domain/tex-run.js.map +1 -0
  79. package/dist/eslint-rules/latex-language.d.ts.map +1 -1
  80. package/dist/eslint-rules/latex-language.js +10 -13
  81. package/dist/eslint-rules/latex-language.js.map +1 -1
  82. package/dist/eslint-rules/paper-context.d.ts.map +1 -1
  83. package/dist/eslint-rules/paper-context.js +6 -5
  84. package/dist/eslint-rules/paper-context.js.map +1 -1
  85. package/dist/eslint-rules/paper-typography.d.ts +0 -9
  86. package/dist/eslint-rules/paper-typography.d.ts.map +1 -1
  87. package/dist/eslint-rules/paper-typography.js +34 -96
  88. package/dist/eslint-rules/paper-typography.js.map +1 -1
  89. package/dist/eslint-rules/papers.d.ts +1 -3
  90. package/dist/eslint-rules/papers.d.ts.map +1 -1
  91. package/dist/eslint-rules/papers.js +2 -4
  92. package/dist/eslint-rules/papers.js.map +1 -1
  93. package/dist/eslint-rules/tex-build.d.ts.map +1 -1
  94. package/dist/eslint-rules/tex-build.js +8 -25
  95. package/dist/eslint-rules/tex-build.js.map +1 -1
  96. package/dist/init.d.ts +1 -4
  97. package/dist/init.d.ts.map +1 -1
  98. package/dist/init.js +3 -9
  99. package/dist/init.js.map +1 -1
  100. package/dist/latex-log.d.ts +10 -0
  101. package/dist/latex-log.d.ts.map +1 -1
  102. package/dist/latex-log.js +10 -0
  103. package/dist/latex-log.js.map +1 -1
  104. package/dist/latex-loop.d.ts +2 -0
  105. package/dist/latex-loop.d.ts.map +1 -1
  106. package/dist/latex-loop.js.map +1 -1
  107. package/dist/lib/markdown.d.ts +5 -81
  108. package/dist/lib/markdown.d.ts.map +1 -1
  109. package/dist/lib/markdown.js +5 -116
  110. package/dist/lib/markdown.js.map +1 -1
  111. package/dist/lib/paper-config.d.ts.map +1 -1
  112. package/dist/lib/paper-config.js +0 -2
  113. package/dist/lib/paper-config.js.map +1 -1
  114. package/dist/lib/skill-eval-fixture.d.ts +6 -4
  115. package/dist/lib/skill-eval-fixture.d.ts.map +1 -1
  116. package/dist/lib/skill-eval-fixture.js +25 -18
  117. package/dist/lib/skill-eval-fixture.js.map +1 -1
  118. package/dist/new-paper.d.ts +5 -11
  119. package/dist/new-paper.d.ts.map +1 -1
  120. package/dist/new-paper.js +12 -23
  121. package/dist/new-paper.js.map +1 -1
  122. package/dist/package-dirs.d.ts.map +1 -1
  123. package/dist/package-dirs.js +3 -2
  124. package/dist/package-dirs.js.map +1 -1
  125. package/dist/paper-files.d.ts +4 -4
  126. package/dist/paper-includes.d.ts +4 -7
  127. package/dist/paper-includes.d.ts.map +1 -1
  128. package/dist/paper-includes.js +29 -21
  129. package/dist/paper-includes.js.map +1 -1
  130. package/dist/paper-prose.d.ts +17 -0
  131. package/dist/paper-prose.d.ts.map +1 -0
  132. package/dist/paper-prose.js +46 -0
  133. package/dist/paper-prose.js.map +1 -0
  134. package/dist/paper-record.d.ts +25 -0
  135. package/dist/paper-record.d.ts.map +1 -0
  136. package/dist/paper-record.js +39 -0
  137. package/dist/paper-record.js.map +1 -0
  138. package/dist/paper-sources.d.ts +66 -0
  139. package/dist/paper-sources.d.ts.map +1 -0
  140. package/dist/paper-sources.js +92 -0
  141. package/dist/paper-sources.js.map +1 -0
  142. package/dist/ports/bib-reader.d.ts +18 -0
  143. package/dist/ports/bib-reader.d.ts.map +1 -0
  144. package/dist/ports/bib-reader.js +2 -0
  145. package/dist/ports/bib-reader.js.map +1 -0
  146. package/dist/ports/check-references.d.ts +9 -4
  147. package/dist/ports/check-references.d.ts.map +1 -1
  148. package/dist/ports/latex.d.ts +15 -0
  149. package/dist/ports/latex.d.ts.map +1 -1
  150. package/dist/ports/sources-codec.d.ts +11 -0
  151. package/dist/ports/sources-codec.d.ts.map +1 -0
  152. package/dist/ports/sources-codec.js +2 -0
  153. package/dist/ports/sources-codec.js.map +1 -0
  154. package/dist/ports/tex-output.d.ts +17 -0
  155. package/dist/ports/tex-output.d.ts.map +1 -0
  156. package/dist/ports/tex-output.js +2 -0
  157. package/dist/ports/tex-output.js.map +1 -0
  158. package/dist/recorded-bibliography.d.ts +89 -0
  159. package/dist/recorded-bibliography.d.ts.map +1 -0
  160. package/dist/recorded-bibliography.js +136 -0
  161. package/dist/recorded-bibliography.js.map +1 -0
  162. package/dist/reference-rules.d.ts +5 -5
  163. package/dist/reference-rules.d.ts.map +1 -1
  164. package/dist/reference-rules.js +150 -77
  165. package/dist/reference-rules.js.map +1 -1
  166. package/dist/references.d.ts +71 -16
  167. package/dist/references.d.ts.map +1 -1
  168. package/dist/references.js +110 -26
  169. package/dist/references.js.map +1 -1
  170. package/dist/register-bands.d.ts +3 -3
  171. package/dist/register-bands.d.ts.map +1 -1
  172. package/dist/register-bands.js.map +1 -1
  173. package/dist/register.d.ts +2 -2
  174. package/dist/sources-record.d.ts +38 -0
  175. package/dist/sources-record.d.ts.map +1 -0
  176. package/dist/sources-record.js +123 -0
  177. package/dist/sources-record.js.map +1 -0
  178. package/dist/sources-rules.d.ts +44 -0
  179. package/dist/sources-rules.d.ts.map +1 -0
  180. package/dist/sources-rules.js +76 -0
  181. package/dist/sources-rules.js.map +1 -0
  182. package/dist/structure.d.ts.map +1 -1
  183. package/dist/structure.js +4 -3
  184. package/dist/structure.js.map +1 -1
  185. package/dist/tex-paper.d.ts +10 -15
  186. package/dist/tex-paper.d.ts.map +1 -1
  187. package/dist/tex-paper.js +24 -40
  188. package/dist/tex-paper.js.map +1 -1
  189. package/dist/tex-venue-rules.d.ts +3 -3
  190. package/dist/tex-venue-rules.d.ts.map +1 -1
  191. package/dist/tex-venue-rules.js.map +1 -1
  192. package/dist/types.d.ts +0 -2
  193. package/dist/types.d.ts.map +1 -1
  194. package/docs/configuration.md +18 -12
  195. package/docs/design/paper-sources.md +197 -0
  196. package/docs/e2e.md +4 -6
  197. package/docs/package-shape-options.md +1 -1
  198. package/docs/prior-art/paper-folder-scaffolding.md +24 -29
  199. package/docs/references.md +34 -8
  200. package/docs/rules/bib/reachable-entry.md +64 -0
  201. package/docs/rules/paper/sources-fresh.md +81 -0
  202. package/docs/rules/tex/claim-provenance.md +0 -1
  203. package/docs/rules.md +62 -61
  204. package/eslint-rules/latex-language.harness.mjs +3 -3
  205. package/eslint-rules/latex-language.ts +10 -13
  206. package/eslint-rules/paper-context.ts +6 -5
  207. package/eslint-rules/paper-typography.test.mjs +29 -157
  208. package/eslint-rules/paper-typography.ts +35 -104
  209. package/eslint-rules/papers.harness.mjs +1 -1
  210. package/eslint-rules/papers.ts +2 -4
  211. package/eslint-rules/tex-build.harness.mjs +2 -2
  212. package/eslint-rules/tex-build.ts +8 -25
  213. package/eslint.config.mjs +22 -1
  214. package/fixtures/build-e2e/aidc/paper.tex +1 -1
  215. package/fixtures/build-e2e/anon-ack/paper.tex +1 -1
  216. package/fixtures/build-e2e/anon-author/paper.tex +1 -1
  217. package/fixtures/build-e2e/anon-bib/paper.tex +1 -1
  218. package/fixtures/build-e2e/anon-href/paper.tex +1 -1
  219. package/fixtures/build-e2e/anon-macro/paper.tex +1 -1
  220. package/fixtures/build-e2e/anon-pdfauthor/paper.tex +1 -1
  221. package/fixtures/paper-sources/p1/paper.tex +30 -0
  222. package/fixtures/paper-sources/p1/refs.bib +6 -0
  223. package/fixtures/paper-sources/p1/sections/dead.tex +6 -0
  224. package/fixtures/paper-sources/p1/sections/intro.tex +7 -0
  225. package/fixtures/paper-sources/p1/sections/parked.tex +6 -0
  226. package/fixtures/paper-sources/p1/tex-truth.json +37 -0
  227. package/fixtures/paper-sources/p1/versions/2026-01-01-review.tex +4 -0
  228. package/fixtures/paper-sources/v1-stale/paper.tex +14 -0
  229. package/fixtures/paper-sources/v1-stale/refs.bib +6 -0
  230. package/fixtures/paper-sources/v1-stale/tex-truth.json +32 -0
  231. package/fixtures/paper-sources/v10-ifthenelse/anon.bib +1 -0
  232. package/fixtures/paper-sources/v10-ifthenelse/paper.tex +9 -0
  233. package/fixtures/paper-sources/v10-ifthenelse/real.bib +1 -0
  234. package/fixtures/paper-sources/v10-ifthenelse/tex-truth.json +28 -0
  235. package/fixtures/paper-sources/v11-two-blocks/paper.tex +12 -0
  236. package/fixtures/paper-sources/v11-two-blocks/tex-truth.json +30 -0
  237. package/fixtures/paper-sources/v13-percent-text/paper.tex +6 -0
  238. package/fixtures/paper-sources/v13-percent-text/refs.bib +3 -0
  239. package/fixtures/paper-sources/v13-percent-text/tex-truth.json +30 -0
  240. package/fixtures/paper-sources/v15-after-end/paper.tex +6 -0
  241. package/fixtures/paper-sources/v15-after-end/refs.bib +1 -0
  242. package/fixtures/paper-sources/v15-after-end/tex-truth.json +17 -0
  243. package/fixtures/paper-sources/v16-included-block/bibblock.tex +3 -0
  244. package/fixtures/paper-sources/v16-included-block/paper.tex +7 -0
  245. package/fixtures/paper-sources/v16-included-block/refs.bib +1 -0
  246. package/fixtures/paper-sources/v16-included-block/tex-truth.json +35 -0
  247. package/fixtures/paper-sources/v17-include-order/bibsetup.tex +1 -0
  248. package/fixtures/paper-sources/v17-include-order/first.bib +1 -0
  249. package/fixtures/paper-sources/v17-include-order/later.bib +1 -0
  250. package/fixtures/paper-sources/v17-include-order/paper.tex +7 -0
  251. package/fixtures/paper-sources/v17-include-order/tex-truth.json +40 -0
  252. package/fixtures/paper-sources/v2-overwrite/paper.tex +14 -0
  253. package/fixtures/paper-sources/v2-overwrite/refs.bib +6 -0
  254. package/fixtures/paper-sources/v2-overwrite/tex-truth.json +30 -0
  255. package/fixtures/paper-sources/v22-parked-thebibliography/paper.tex +7 -0
  256. package/fixtures/paper-sources/v22-parked-thebibliography/tex-truth.json +17 -0
  257. package/fixtures/paper-sources/v23-parked-include/old.bib +1 -0
  258. package/fixtures/paper-sources/v23-parked-include/paper.tex +7 -0
  259. package/fixtures/paper-sources/v23-parked-include/parked.tex +1 -0
  260. package/fixtures/paper-sources/v23-parked-include/refs.bib +1 -0
  261. package/fixtures/paper-sources/v23-parked-include/tex-truth.json +28 -0
  262. package/fixtures/paper-sources/v24-quoted-fields/paper.tex +6 -0
  263. package/fixtures/paper-sources/v24-quoted-fields/refs.bib +5 -0
  264. package/fixtures/paper-sources/v24-quoted-fields/tex-truth.json +32 -0
  265. package/fixtures/paper-sources/v25-string-differs/paper.tex +10 -0
  266. package/fixtures/paper-sources/v25-string-differs/refs.bib +2 -0
  267. package/fixtures/paper-sources/v25-string-differs/tex-truth.json +32 -0
  268. package/fixtures/paper-sources/v26-comment-in-names/one.bib +1 -0
  269. package/fixtures/paper-sources/v26-comment-in-names/paper.tex +7 -0
  270. package/fixtures/paper-sources/v26-comment-in-names/tex-truth.json +32 -0
  271. package/fixtures/paper-sources/v26-comment-in-names/two.bib +1 -0
  272. package/fixtures/paper-sources/v27-shared-string/abbreviations.bib +2 -0
  273. package/fixtures/paper-sources/v27-shared-string/paper.tex +6 -0
  274. package/fixtures/paper-sources/v27-shared-string/references.bib +6 -0
  275. package/fixtures/paper-sources/v27-shared-string/tex-truth.json +31 -0
  276. package/fixtures/paper-sources/v28-shared-string-block/abbreviations.bib +2 -0
  277. package/fixtures/paper-sources/v28-shared-string-block/paper.tex +14 -0
  278. package/fixtures/paper-sources/v28-shared-string-block/tex-truth.json +33 -0
  279. package/fixtures/paper-sources/v29-generated-input/paper.tex +9 -0
  280. package/fixtures/paper-sources/v29-generated-input/tex-truth.json +24 -0
  281. package/fixtures/paper-sources/v3-declared/paper.bib +6 -0
  282. package/fixtures/paper-sources/v3-declared/paper.tex +6 -0
  283. package/fixtures/paper-sources/v3-declared/tex-truth.json +28 -0
  284. package/fixtures/paper-sources/v30-name-next-line/paper.tex +14 -0
  285. package/fixtures/paper-sources/v30-name-next-line/tex-truth.json +30 -0
  286. package/fixtures/paper-sources/v4-commented/paper.tex +14 -0
  287. package/fixtures/paper-sources/v4-commented/refs.bib +6 -0
  288. package/fixtures/paper-sources/v4-commented/tex-truth.json +28 -0
  289. package/fixtures/paper-sources/v5-percent-entry/paper.tex +20 -0
  290. package/fixtures/paper-sources/v5-percent-entry/tex-truth.json +37 -0
  291. package/fixtures/paper-sources/v6-unclosed/paper.tex +6 -0
  292. package/fixtures/paper-sources/v6-unclosed/refs.bib +5 -0
  293. package/fixtures/paper-sources/v6-unclosed/tex-truth.json +37 -0
  294. package/fixtures/paper-sources/v8-jobname/paper.tex +9 -0
  295. package/fixtures/paper-sources/v8-jobname/tex-truth.json +30 -0
  296. package/fixtures/paper-sources/v9-two-bibliographies/one.bib +1 -0
  297. package/fixtures/paper-sources/v9-two-bibliographies/paper.tex +7 -0
  298. package/fixtures/paper-sources/v9-two-bibliographies/tex-truth.json +35 -0
  299. package/fixtures/paper-sources/v9-two-bibliographies/two.bib +1 -0
  300. package/fixtures/paper-typography/clean-paper/paper.tex +1 -1
  301. package/fixtures/paper-typography/messy-paper/paper.tex +1 -1
  302. package/hooks/hooks.harness.ts +15 -15
  303. package/hooks/paper-edit-guard.hook.mjs +5 -8
  304. package/hooks/paper-edit-guard.test.ts +12 -3
  305. package/hooks/paper-skills-nudge.hook.mjs +2 -3
  306. package/hooks/paper-skills-nudge.test.ts +13 -3
  307. package/hooks/paper-status-gates.hook.mjs +2 -2
  308. package/hooks/paper-status-gates.test.ts +14 -2
  309. package/lib/markdown.test.mjs +5 -60
  310. package/lib/markdown.ts +9 -136
  311. package/lib/paper-config.ts +0 -2
  312. package/lib/skill-checks.test.mjs +4 -1
  313. package/lib/skill-eval-fixture.ts +25 -18
  314. package/package.json +2 -12
  315. package/presets/realm.jsonc +1 -1
  316. package/scripts/check.test.ts +23 -1
  317. package/scripts/check.ts +7 -3
  318. package/scripts/coverage-config.test.ts +19 -10
  319. package/scripts/harness-api.frozen.json +0 -5
  320. package/scripts/layer-legacy-frozen.test.ts +41 -2
  321. package/scripts/layer-legacy-frozen.ts +31 -2
  322. package/skills/cold-read-diff/SKILL.md +3 -3
  323. package/skills/cold-read-diff/SKILL.md.spec.ts +2 -2
  324. package/skills/grade-paper-writing/SKILL.md +15 -22
  325. package/skills/grade-paper-writing/SKILL.md.spec.ts +14 -21
  326. package/skills/grade-paper-writing/__snapshots__/prose-lint.test.ts.snap +40 -26
  327. package/skills/grade-paper-writing/prose-lint.mjs +117 -278
  328. package/skills/grade-paper-writing/prose-lint.test.ts +230 -79
  329. package/skills/paper-adversarial-review/SKILL.md +2 -2
  330. package/skills/paper-adversarial-review/SKILL.md.spec.ts +1 -1
  331. package/skills/paper-pipeline/SKILL.md +9 -12
  332. package/skills/paper-pipeline/SKILL.md.spec.ts +8 -11
  333. package/skills/paper-pipeline/description-language.eval.mjs +25 -15
  334. package/skills/paper-pipeline/framing-vs-vocabulary.eval.mjs +25 -15
  335. package/skills/paper-pipeline/grade-paper-writing-ablation.eval.mjs +25 -15
  336. package/skills/paper-pipeline/pipeline-firing.eval.mjs +27 -16
  337. package/skills/paper-pipeline/pipeline-language.eval.mjs +25 -15
  338. package/skills/paper-pipeline/references/acceptance-gate.md +16 -20
  339. package/skills/paper-pipeline/references/artifact-checklist.md +4 -5
  340. package/skills/paper-pipeline/references/pipeline-status-template.md +1 -1
  341. package/skills/paper-pipeline/scripts/README.md +0 -4
  342. package/skills/paper-pipeline/scripts/extract-ref-facts.harness.mjs +91 -105
  343. package/skills/paper-pipeline/scripts/extract-ref-facts.mjs +85 -241
  344. package/skills/paper-pipeline/scripts/extract-ref-facts.test.mjs +166 -153
  345. package/skills/paper-pipeline/scripts/generated-code.harness.mjs +3 -3
  346. package/skills/paper-status/SKILL.md +2 -2
  347. package/skills/paper-status/SKILL.md.spec.ts +1 -1
  348. package/skills/render-paper/SKILL.md +2 -2
  349. package/skills/render-paper/SKILL.md.spec.ts +1 -1
  350. package/skills/render-paper/extract-pdf-facts.mjs +2 -2
  351. package/skills/tighten-paper/SKILL.md +29 -42
  352. package/skills/tighten-paper/SKILL.md.spec.ts +28 -41
  353. package/skills/verify-citations/SKILL.md +7 -4
  354. package/skills/verify-citations/SKILL.md.spec.ts +6 -3
  355. package/skills/verify-citations/scripts/bib-authors.check.test.mjs +17 -32
  356. package/skills/verify-citations/scripts/bib-authors.cli.test.ts +138 -43
  357. package/skills/verify-citations/scripts/bib-authors.d.mts +22 -2
  358. package/skills/verify-citations/scripts/bib-authors.mjs +54 -193
  359. package/skills/verify-citations/scripts/bib-authors.test.mjs +1 -44
  360. package/skills/verify-citations/scripts/verify-cites.cli.test.ts +16 -0
  361. package/skills/verify-citations/scripts/verify-cites.d.mts +19 -3
  362. package/skills/verify-citations/scripts/verify-cites.mjs +42 -108
  363. package/skills/verify-citations/scripts/verify-cites.test.mjs +23 -36
  364. package/src/adapters/bibtex/index.test.ts +163 -0
  365. package/src/adapters/bibtex/index.ts +155 -0
  366. package/src/adapters/latex/filecontents.ts +114 -0
  367. package/src/adapters/latex/includes.ts +18 -10
  368. package/src/adapters/latex/index.test.ts +73 -0
  369. package/src/adapters/latex/index.ts +2 -0
  370. package/src/adapters/latex/nodes.ts +64 -0
  371. package/src/adapters/latex/parse.test.ts +38 -0
  372. package/src/adapters/latex/parse.ts +67 -6
  373. package/src/adapters/memory/index.ts +17 -0
  374. package/src/adapters/references/index.test.ts +75 -21
  375. package/src/adapters/references/index.ts +37 -32
  376. package/src/adapters/sources-record/index.test.ts +87 -0
  377. package/src/adapters/sources-record/index.ts +82 -0
  378. package/src/adapters/tex-output/blg.ts +27 -0
  379. package/src/adapters/tex-output/fls.ts +24 -0
  380. package/src/adapters/tex-output/index.test.ts +171 -0
  381. package/src/adapters/tex-output/index.ts +12 -0
  382. package/src/bib-rules.test.ts +356 -0
  383. package/src/bib-rules.ts +228 -0
  384. package/src/bibliography-at.test.ts +286 -0
  385. package/src/build.harness.mjs +2 -2
  386. package/src/build.test.ts +211 -1
  387. package/src/build.ts +101 -41
  388. package/src/claim-provenance.test.ts +33 -7
  389. package/src/claim-provenance.ts +2 -2
  390. package/src/cli.harness.mjs +46 -56
  391. package/src/cli.test.ts +44 -48
  392. package/src/cli.ts +51 -82
  393. package/src/domain/paper-source.test.ts +58 -20
  394. package/src/domain/paper-source.ts +38 -42
  395. package/src/domain/paper-sources.ts +94 -0
  396. package/src/domain/sources-record.test.ts +71 -0
  397. package/src/domain/sources-record.ts +136 -0
  398. package/src/domain/tex-run.test.ts +228 -0
  399. package/src/domain/tex-run.ts +139 -0
  400. package/src/folder-venue-rule.test.ts +1 -1
  401. package/src/heading-case.test.ts +24 -10
  402. package/src/init.test.ts +6 -7
  403. package/src/init.ts +3 -22
  404. package/src/latex-log.ts +20 -0
  405. package/src/latex-loop.harness.ts +1 -0
  406. package/src/latex-loop.ts +2 -0
  407. package/src/lint-fix.test.ts +10 -4
  408. package/src/linted-set.test.ts +9 -1
  409. package/src/new-paper.harness.mjs +12 -30
  410. package/src/new-paper.test.ts +15 -11
  411. package/src/new-paper.ts +11 -30
  412. package/src/package-dirs.ts +3 -2
  413. package/src/paper-files.test.ts +5 -4
  414. package/src/paper-files.ts +4 -4
  415. package/src/paper-includes.test.ts +157 -38
  416. package/src/paper-includes.ts +39 -35
  417. package/src/paper-prose.test.ts +78 -0
  418. package/src/paper-prose.ts +67 -0
  419. package/src/paper-record.test.ts +173 -0
  420. package/src/paper-record.ts +66 -0
  421. package/src/paper-settings-commands.test.ts +5 -2
  422. package/src/paper-sources.ts +151 -0
  423. package/src/parse-args.test.ts +0 -1
  424. package/src/ports/bib-reader.ts +27 -0
  425. package/src/ports/check-references.ts +7 -3
  426. package/src/ports/latex.ts +19 -0
  427. package/src/ports/sources-codec.ts +11 -0
  428. package/src/ports/tex-output.ts +17 -0
  429. package/src/recorded-bibliography.ts +297 -0
  430. package/src/reference-rules.ts +217 -104
  431. package/src/references.test.ts +520 -177
  432. package/src/references.ts +185 -37
  433. package/src/register-bands.test.ts +5 -1
  434. package/src/register-bands.ts +3 -5
  435. package/src/register.test.ts +6 -1
  436. package/src/register.ts +2 -2
  437. package/src/rule-cases.test.ts +68 -12
  438. package/src/rule-docs.test.ts +0 -1
  439. package/src/sources-record.test.ts +308 -0
  440. package/src/sources-record.ts +231 -0
  441. package/src/sources-rules.test.ts +152 -0
  442. package/src/sources-rules.ts +117 -0
  443. package/src/structure.harness.ts +5 -11
  444. package/src/structure.ts +4 -3
  445. package/src/tex-paper.test.ts +148 -78
  446. package/src/tex-paper.ts +31 -61
  447. package/src/tex-venue-rules.test.ts +36 -8
  448. package/src/tex-venue-rules.ts +3 -5
  449. package/src/types.ts +0 -2
  450. package/dist/eslint-rules/bib-reachable-entry.d.ts +0 -22
  451. package/dist/eslint-rules/bib-reachable-entry.d.ts.map +0 -1
  452. package/dist/eslint-rules/bib-reachable-entry.js +0 -75
  453. package/dist/eslint-rules/bib-reachable-entry.js.map +0 -1
  454. package/eslint-rules/bib-reachable-entry.ts +0 -76
  455. package/fixtures/real-markdown-paper/baseline.json +0 -22
  456. package/fixtures/real-markdown-paper/baseline.ts +0 -70
  457. package/skills/paper-pipeline/PIPELINE-MAP.md +0 -381
  458. package/skills/paper-pipeline/pipeline-firing.baseline.json +0 -270
  459. package/skills/paper-pipeline/references/sweep-2026-08-09-ideal-pipeline.md +0 -585
  460. package/skills/paper-pipeline/scripts/artifact-coverage.harness.mjs +0 -492
  461. package/skills/paper-pipeline/scripts/artifact-coverage.mjs +0 -397
  462. package/skills/paper-pipeline/scripts/artifact-coverage.test.ts +0 -86
  463. package/skills/paper-pipeline/scripts/check-provenance.mjs +0 -184
  464. package/skills/paper-pipeline/scripts/population-map.harness.mjs +0 -135
  465. package/skills/paper-pipeline/scripts/population-map.mjs +0 -188
  466. package/skills/paper-pipeline/scripts/population-map.test.mjs +0 -154
  467. package/skills/paper-pipeline/scripts/provenance.harness.mjs +0 -266
  468. package/skills/paper-pipeline/scripts/round-diff.harness.ts +0 -907
  469. package/skills/paper-pipeline/scripts/round-diff.mjs +0 -583
  470. package/skills/paper-pipeline/scripts/round-diff.test.mjs +0 -159
  471. package/skills/tighten-paper/__snapshots__/structure.test.ts.snap +0 -133
  472. package/skills/tighten-paper/structure.mjs +0 -364
  473. package/skills/tighten-paper/structure.test.ts +0 -117
  474. package/templates/paper/paper.md +0 -4
@@ -0,0 +1,286 @@
1
+ /**
2
+ * `bibliographyAt` — a path a person gave, and the bibliography to read for it. A `.bib` is read alone,
3
+ * as named; a `.tex`, or a directory holding `paper.tex`, is the bibliography the last build's bibtex
4
+ * opened (`_build/sources.json`, docs/design/paper-sources.md §1) — read from the text the author
5
+ * edits: the `filecontents` block that wrote a database, or the file itself. A paper with no current
6
+ * record is refused, in one vocabulary, naming the command that records it.
7
+ */
8
+ import { describe, expect, it } from "vitest";
9
+ import { bibReader } from "./adapters/bibtex/index.ts";
10
+ import { latexReader } from "./adapters/latex/index.ts";
11
+ import { memoryFiles } from "./adapters/memory/index.ts";
12
+ import { sourcesCodec } from "./adapters/sources-record/index.ts";
13
+ import type { BibText } from "./domain/paper-sources.ts";
14
+ import { absolutePath } from "./domain/paths.ts";
15
+ import { bibliographyAt, bibliographyUnreadWhy } from "./paper-sources.ts";
16
+ import {
17
+ leftByTeX,
18
+ planted,
19
+ recordText,
20
+ type Shape,
21
+ } from "../test/recorded-fixture.ts";
22
+
23
+ const doc = (body: string): string =>
24
+ `\\documentclass{article}\n\\begin{document}\n${body}\n\\end{document}\n`;
25
+ const A = "@misc{a, title = {A}}\n";
26
+ const B = "@misc{b, title = {B}}\n";
27
+
28
+ /** A directory as TeX and the build leave it: the files, and the record over them (none: not built). */
29
+ function built(
30
+ dir: string,
31
+ files: Readonly<Record<string, string>>,
32
+ shape: Shape | string | null,
33
+ ): Record<string, string> {
34
+ const record =
35
+ shape === null
36
+ ? {}
37
+ : {
38
+ [`${dir}/_build/sources.json`]:
39
+ typeof shape === "string" ? shape : recordText(files, shape),
40
+ };
41
+ return {
42
+ ...Object.fromEntries(
43
+ Object.entries(files).map(([f, t]) => [`${dir}/${f}`, t]),
44
+ ),
45
+ ...record,
46
+ };
47
+ }
48
+
49
+ const PAPER = doc("\\bibliography{refs}");
50
+ const SHAPE: Shape = {
51
+ inputs: [
52
+ ["paper.tex", "body"],
53
+ ["refs.bib", "preamble"],
54
+ ],
55
+ databases: ["refs.bib"],
56
+ };
57
+ const OWN = { "paper.tex": PAPER, "refs.bib": A, "other.bib": B };
58
+
59
+ /** Each text read, with the keys of the entries it holds. */
60
+ const keysIn = (
61
+ r: ReturnType<typeof bibliographyAt>,
62
+ ): readonly (readonly [string, readonly string[]])[] =>
63
+ r.ok ? r.value.texts.map((t) => [t.path, entryKeys(t)] as const) : [];
64
+ const entryKeys = (t: BibText): readonly string[] =>
65
+ t.entries.map((e) => e.key);
66
+
67
+ const depsOf = (disk: Readonly<Record<string, string>>) => ({
68
+ files: memoryFiles({ "/p/notes.txt": "notes", ...disk }),
69
+ codec: sourcesCodec,
70
+ latex: latexReader,
71
+ bib: bibReader,
72
+ });
73
+
74
+ /** What a path comes to: the paper's directory and the files read, or the one sentence why not. */
75
+ const at = (disk: Readonly<Record<string, string>>, path: string) => {
76
+ const r = bibliographyAt(absolutePath(path), depsOf(disk));
77
+ return r.ok
78
+ ? [r.value.paperDir, r.value.texts.map((t) => t.path)]
79
+ : bibliographyUnreadWhy(r.error);
80
+ };
81
+
82
+ describe("bibliographyAt — the paths it reads", () => {
83
+ const disk = built("/p", OWN, SHAPE);
84
+
85
+ it("a .bib is read alone, as named — with no record of any build", () => {
86
+ expect([
87
+ at(built("/p", OWN, null), "/p/other.bib"),
88
+ at(disk, "/p/other.bib"),
89
+ ]).toEqual([
90
+ ["/p", ["/p/other.bib"]],
91
+ ["/p", ["/p/other.bib"]],
92
+ ]);
93
+ });
94
+
95
+ it("a .tex, or the directory of a paper.tex, is the bibliography the last build's bibtex opened — not every .bib in the folder", () => {
96
+ expect([at(disk, "/p/paper.tex"), at(disk, "/p")]).toEqual([
97
+ ["/p", ["/p/refs.bib"]],
98
+ ["/p", ["/p/refs.bib"]],
99
+ ]);
100
+ });
101
+
102
+ it("🔴 a .bib TeX wrote from a block is read from the block of the .tex the author edits, where its entries stand", () => {
103
+ const tex =
104
+ "\\documentclass{article}\n\\begin{filecontents*}[overwrite]{refs.bib}\n" +
105
+ A +
106
+ "\\end{filecontents*}\n\\begin{document}\nx\\bibliography{refs}\n\\end{document}\n";
107
+ const files = { "paper.tex": tex, "refs.bib": A };
108
+ const d = built("/p", files, {
109
+ inputs: [["paper.tex", "body"]],
110
+ written: ["refs.bib"],
111
+ databases: ["refs.bib"],
112
+ });
113
+ expect(keysIn(bibliographyAt(absolutePath("/p"), depsOf(d)))).toEqual([
114
+ ["/p/paper.tex", ["a"]],
115
+ ]);
116
+ });
117
+ });
118
+
119
+ describe("bibliographyAt — where a block is", () => {
120
+ it("a block that is in an included file is read from that file", () => {
121
+ const files = {
122
+ "paper.tex": doc("\\input{bibblock}\\bibliography{refs}"),
123
+ "bibblock.tex": `\\begin{filecontents*}{refs.bib}\n${A}\\end{filecontents*}\n`,
124
+ "refs.bib": A,
125
+ };
126
+ const d = built("/p", files, {
127
+ inputs: [
128
+ ["paper.tex", "body"],
129
+ ["bibblock.tex", "body"],
130
+ ],
131
+ written: ["refs.bib"],
132
+ databases: ["refs.bib"],
133
+ });
134
+ expect(at(d, "/p")).toEqual(["/p", ["/p/bibblock.tex"]]);
135
+ });
136
+
137
+ it("when no text holds what bibtex read, the file TeX left is read", () => {
138
+ const files = {
139
+ "paper.tex": doc("\\bibliography{refs}"),
140
+ "refs.bib": A,
141
+ };
142
+ const d = built("/p", files, {
143
+ inputs: [["paper.tex", "body"]],
144
+ written: ["refs.bib"],
145
+ databases: ["refs.bib"],
146
+ });
147
+ expect(at(d, "/p")).toEqual(["/p", ["/p/refs.bib"]]);
148
+ });
149
+ });
150
+
151
+ describe("bibliographyAt — every refusal in one vocabulary, naming the path and what to give instead", () => {
152
+ it("a path that is not a paper, a .bib or a .tex", () => {
153
+ const disk = built("/p", OWN, SHAPE);
154
+ expect([
155
+ at(disk, "/p/gone.bib"),
156
+ at(disk, "/nowhere"),
157
+ at(disk, "/p/notes.txt"),
158
+ at(disk, "/p/gone.tex"),
159
+ ]).toEqual([
160
+ "/p/gone.bib does not exist — nowhere to take a bibliography from",
161
+ "no paper.tex in /nowhere — name the paper's .tex, or a .bib to read it alone",
162
+ "/p/notes.txt is neither a .bib nor a .tex — name the paper's directory, its .tex, or a .bib",
163
+ "/p/gone.tex does not exist — nowhere to take a bibliography from",
164
+ ]);
165
+ });
166
+
167
+ it("🔴 a paper with no record of a build is not read from its source — run `npx paperlint build` first", () => {
168
+ const disk = built("/p", OWN, null);
169
+ expect([at(disk, "/p"), at(disk, "/p/paper.tex")]).toEqual([
170
+ "/p has not been built — run `npx paperlint build` first, which records the databases bibtex reads",
171
+ "/p has not been built — run `npx paperlint build` first, which records the databases bibtex reads",
172
+ ]);
173
+ });
174
+
175
+ it("a record the paper has changed since, and one that cannot be read, name the command too", () => {
176
+ const stale = built("/p", OWN, SHAPE);
177
+ stale["/p/refs.bib"] = `${A}${B}`;
178
+ expect(at(stale, "/p")).toBe(
179
+ "/p changed since the last build (refs.bib edited) — run `npx paperlint build` first",
180
+ );
181
+ const broken = built("/p", OWN, "{ not json");
182
+ expect(at(broken, "/p")).toMatch(
183
+ /^the last build's record of \/p cannot be used \(.+\) — run `npx paperlint build` first$/,
184
+ );
185
+ });
186
+
187
+ it("a build whose bibtex read no database, and one whose databases are gone, have nothing to read", () => {
188
+ const none = built(
189
+ "/q",
190
+ { "paper.tex": doc("no bibliography") },
191
+ {
192
+ inputs: [["paper.tex", "body"]],
193
+ },
194
+ );
195
+ expect(at(none, "/q")).toBe(
196
+ "bibtex read no database in the last build of /q — there is nothing to read",
197
+ );
198
+ const gone = built(
199
+ "/s",
200
+ { "paper.tex": doc("\\bibliography{gone}") },
201
+ {
202
+ inputs: [["paper.tex", "body"]],
203
+ databases: ["gone.bib"],
204
+ },
205
+ );
206
+ expect(at(gone, "/s")).toBe(
207
+ "bibtex opened gone.bib in the last build of /s, and none of them is on disk now",
208
+ );
209
+ });
210
+ });
211
+
212
+ describe("bibliographyAt — a bibliography it can read only in part", () => {
213
+ it("🔴 a build whose bibtex also opened a database outside the paper directory (BIBINPUTS) is refused, not read in part", () => {
214
+ // Measured: with BIBINPUTS=../shared, bibtex's .blg names `Database file #2: shared.bib` and the
215
+ // .bbl typesets its entry; the paper directory holds refs.bib only.
216
+ const partly = built(
217
+ "/r",
218
+ { "paper.tex": doc("\\bibliography{refs,shared}"), "refs.bib": A },
219
+ {
220
+ inputs: [["paper.tex", "body"]],
221
+ databases: ["refs.bib", "shared.bib"],
222
+ },
223
+ );
224
+ expect(at(partly, "/r")).toBe(
225
+ "bibtex opened shared.bib in the last build of /r from outside it (its search path, BIBINPUTS), and paperlint reads only the paper's own databases — copy it into /r, or name each .bib to read it alone",
226
+ );
227
+ });
228
+ });
229
+
230
+ describe("bibliographyAt on the planted papers, with the record TeX wrote for each", () => {
231
+ it.each([
232
+ ["v3-declared", ["/p/paper.bib"]],
233
+ ["v26-comment-in-names", ["/p/one.bib", "/p/two.bib"]],
234
+ ["v16-included-block", ["/p/refs.bib"]],
235
+ ["v25-string-differs", ["/p/refs.bib"]],
236
+ // A block that TeX wrote: read from the block where the author edits it.
237
+ ["v8-jobname", ["/p/paper.tex"]],
238
+ ["v5-percent-entry", ["/p/paper.tex"]],
239
+ // The block names its file on the line after \\begin: still read from the block.
240
+ ["v30-name-next-line", ["/p/paper.tex"]],
241
+ ])("%s", (name, files) => {
242
+ const p = planted(name);
243
+ const disk = built("/p", leftByTeX(p), p.record);
244
+ expect(at(disk, "/p")).toEqual(["/p", files]);
245
+ });
246
+ });
247
+
248
+ describe("bibliographyAt — an @string one database defines and a later one uses", () => {
249
+ /** Each text read, with each entry's author and booktitle as the reader read them. */
250
+ const fieldsAt = (name: string) => {
251
+ const p = planted(name);
252
+ const r = bibliographyAt(
253
+ absolutePath("/p"),
254
+ depsOf(built("/p", leftByTeX(p), p.record)),
255
+ );
256
+ return r.ok
257
+ ? r.value.texts.map((t) => [
258
+ t.path,
259
+ t.entries.map((e) => [
260
+ e.key,
261
+ e.names["author"],
262
+ e.fields["booktitle"],
263
+ ]),
264
+ ])
265
+ : r.error;
266
+ };
267
+ const LATE = [
268
+ "late",
269
+ [{ lastName: "Lovelace", firstName: "Ada" }],
270
+ "Proceedings of the Shared Venue",
271
+ ];
272
+
273
+ it("🔴 v27-shared-string: references.bib's entry is read with the @strings abbreviations.bib defined, as bibtex read it", () => {
274
+ expect(fieldsAt("v27-shared-string")).toEqual([
275
+ ["/p/abbreviations.bib", []],
276
+ ["/p/references.bib", [LATE]],
277
+ ]);
278
+ });
279
+
280
+ it("🔴 v28-shared-string-block: a block that uses them is still the database TeX wrote, read where the author edits it", () => {
281
+ expect(fieldsAt("v28-shared-string-block")).toEqual([
282
+ ["/p/abbreviations.bib", []],
283
+ ["/p/paper.tex", [LATE]],
284
+ ]);
285
+ });
286
+ });
@@ -482,7 +482,7 @@ try {
482
482
  );
483
483
 
484
484
  // ── no paper.tex: a REFUSAL, not a skip ───────────────────────────────────────────────
485
- const bare = paper("bare", { "paper.md": "# x" });
485
+ const bare = paper("bare", { "PIPELINE-STATUS.md": "# x" });
486
486
  const ntex = fakeTex();
487
487
  const nr = await buildPaper(bare, {
488
488
  cwd: root,
@@ -546,7 +546,7 @@ try {
546
546
 
547
547
  // (c) no paper.tex: refused, and a PDF left from some earlier build goes too.
548
548
  const bareStale = paper("bare-stale", {
549
- "paper.md": "# x",
549
+ "PIPELINE-STATUS.md": "# x",
550
550
  "paper.pdf": "%PDF-stale-from-yesterday",
551
551
  });
552
552
  const bsTex = fakeTex();
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
  }
@@ -162,7 +172,7 @@ test("documentclass: an empty name is no class; a macro inside the name contribu
162
172
  });
163
173
 
164
174
  test("facts of a paper with no paper.tex: no class", () => {
165
- assert.deepEqual(readFacts(paper({ "paper.md": "# P\n" })), {
175
+ assert.deepEqual(readFacts(paper({ "PIPELINE-STATUS.md": "# P\n" })), {
166
176
  main: null,
167
177
  documentclass: null,
168
178
  venue: null,
@@ -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
+ });