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,308 @@
1
+ /**
2
+ * The build's record step (`recordSources`): from the passes' `.fls` and the `.aux`, `.blg` and `.bbl`
3
+ * a run left, the `_build/sources.json` of docs/design/paper-sources.md §1 — on the files of real runs
4
+ * of the planted papers (test/fixtures/tex-output/README.md), over files held in memory. What TeX
5
+ * itself answers for the same papers is `test/e2e/tex/paper-sources.e2e.ts`.
6
+ */
7
+ import { createHash } from "node:crypto";
8
+ import { readFileSync } from "node:fs";
9
+ import { dirname, join } from "node:path";
10
+ import { fileURLToPath } from "node:url";
11
+ import { describe, expect, it } from "vitest";
12
+ import {
13
+ memoryFiles,
14
+ memoryListDir,
15
+ type MemoryFiles,
16
+ } from "./adapters/memory/index.ts";
17
+ import { texOutput } from "./adapters/tex-output/index.ts";
18
+ import { sourcesCodec } from "./adapters/sources-record/index.ts";
19
+ import {
20
+ recordSources,
21
+ sourcesRecorder,
22
+ type TexRun,
23
+ } from "./sources-record.ts";
24
+
25
+ const CAPTURES = join(
26
+ dirname(dirname(fileURLToPath(import.meta.url))),
27
+ "test",
28
+ "fixtures",
29
+ "tex-output",
30
+ );
31
+ const capture = (paper: string, file: string): string =>
32
+ readFileSync(join(CAPTURES, paper, file), "utf8");
33
+
34
+ const DIR = "/work/paper";
35
+ /** The record step's ports over files held in memory. */
36
+ const deps = (files: MemoryFiles) => ({
37
+ files,
38
+ texOutput,
39
+ listDir: memoryListDir(files),
40
+ });
41
+ const sha = (text: string): string =>
42
+ createHash("sha256").update(text).digest("hex");
43
+
44
+ /** The run of a planted paper: both captured passes, and what bibtex and TeX left. */
45
+ const run = (paper: string, bibtexExit: number | null): TexRun => ({
46
+ fls: ["first-pass.fls", "paper.fls"].map((f) => capture(paper, f)),
47
+ bibtexExit,
48
+ });
49
+ /** The paper directory after that run: the sources given, TeX's and bibtex's files captured. */
50
+ const after = (
51
+ paper: string,
52
+ sources: Readonly<Record<string, string>>,
53
+ captured: readonly string[] = ["paper.aux", "paper.blg", "paper.bbl"],
54
+ ) =>
55
+ memoryFiles({
56
+ ...Object.fromEntries(
57
+ Object.entries(sources).map(([rel, text]) => [`${DIR}/${rel}`, text]),
58
+ ),
59
+ ...Object.fromEntries(
60
+ captured.map((f) => [`${DIR}/${f}`, capture(paper, f)]),
61
+ ),
62
+ });
63
+
64
+ const SOURCES = { "paper.tex": "% paper\n", "refs.bib": "@misc{a}\n" };
65
+
66
+ describe("the record of a build", () => {
67
+ it("lists what TeX read, what bibtex did and the hash of every file the paper is made of", () => {
68
+ const files = after("p1", {
69
+ ...SOURCES,
70
+ "sections/intro.tex": "Intro.\n",
71
+ });
72
+ const out = recordSources(deps(files), DIR, run("p1", 0));
73
+ expect(out).toEqual({
74
+ kind: "recorded",
75
+ path: `${DIR}/_build/sources.json`,
76
+ record: {
77
+ schema: 1,
78
+ inputs: [
79
+ { path: "paper.tex", role: "body" },
80
+ { path: "refs.bib", role: "preamble" },
81
+ { path: "sections/intro.tex", role: "body" },
82
+ ],
83
+ written: [],
84
+ bibdata: ["refs"],
85
+ bibtex: {
86
+ ran: true,
87
+ databases: ["refs.bib"],
88
+ keys: ["stale2020"],
89
+ exit: 0,
90
+ errors: [],
91
+ },
92
+ sha256: {
93
+ "paper.tex": sha("% paper\n"),
94
+ "refs.bib": sha("@misc{a}\n"),
95
+ "sections/intro.tex": sha("Intro.\n"),
96
+ },
97
+ },
98
+ });
99
+ });
100
+
101
+ it("writes it to _build/sources.json, and what it wrote parses back to the same record", () => {
102
+ const files = after("p1", { ...SOURCES, "sections/intro.tex": "x" });
103
+ const out = recordSources(deps(files), DIR, run("p1", 0));
104
+ const text = new TextDecoder().decode(
105
+ files.map.get(`${DIR}/_build/sources.json`),
106
+ );
107
+ expect(text.endsWith("}\n")).toBe(true);
108
+ expect(sourcesCodec.parse(text)).toEqual({
109
+ ok: true,
110
+ record: out.kind === "recorded" ? out.record : undefined,
111
+ });
112
+ });
113
+ });
114
+
115
+ describe("what the record leaves out, or marks absent", () => {
116
+ it("a .bib TeX wrote is listed as written and hashed, so an edit to it after the build makes the record stale", () => {
117
+ const files = after("v8-jobname", {
118
+ "paper.tex": "% block\n",
119
+ "paper.bib": "TeX's own copy\n",
120
+ });
121
+ const out = recordSources(deps(files), DIR, run("v8-jobname", 0));
122
+ expect(out.kind === "recorded" ? out.record : out).toEqual({
123
+ schema: 1,
124
+ inputs: [{ path: "paper.tex", role: "body" }],
125
+ written: ["paper.bib"],
126
+ bibdata: ["paper"],
127
+ bibtex: {
128
+ ran: true,
129
+ databases: ["paper.bib"],
130
+ keys: ["jkey"],
131
+ exit: 0,
132
+ errors: [],
133
+ },
134
+ sha256: {
135
+ "paper.tex": sha("% block\n"),
136
+ "paper.bib": sha("TeX's own copy\n"),
137
+ },
138
+ });
139
+ });
140
+
141
+ it("a database bibtex names as ./paper.bib is recorded as paper.bib, the name the .fls gave the file TeX wrote", () => {
142
+ const blg = capture("v8-jobname", "paper.blg").replace(
143
+ "Database file #1: paper.bib",
144
+ "Database file #1: ./paper.bib",
145
+ );
146
+ const files = after(
147
+ "v8-jobname",
148
+ {
149
+ "paper.tex": "% block\n",
150
+ "paper.bib": "TeX's own copy\n",
151
+ "paper.blg": blg,
152
+ },
153
+ ["paper.aux", "paper.bbl"],
154
+ );
155
+ const out = recordSources(deps(files), DIR, run("v8-jobname", 0));
156
+ expect(out.kind === "recorded" ? out.record : out).toMatchObject({
157
+ written: ["paper.bib"],
158
+ bibtex: { ran: true, databases: ["paper.bib"] },
159
+ sha256: { "paper.bib": sha("TeX's own copy\n") },
160
+ });
161
+ });
162
+ });
163
+
164
+ describe("how the record spells a file", () => {
165
+ it("a file TeX logged in the source's spelling is recorded in the disk's, so a file system that ignores case gives the record one that does not", () => {
166
+ const files = memoryFiles({
167
+ [`${DIR}/paper.tex`]: "% p\n",
168
+ [`${DIR}/figures/nexmark.pdf`]: "pdf",
169
+ });
170
+ const out = recordSources(deps(files), DIR, {
171
+ fls: [
172
+ "PWD /work/paper\nINPUT paper.tex\nINPUT Figures/NEXMark.pdf\nINPUT figures/nexmark.pdf\n",
173
+ ],
174
+ bibtexExit: null,
175
+ });
176
+ expect(out.kind === "recorded" ? out.record : out).toMatchObject({
177
+ inputs: [{ path: "paper.tex" }, { path: "figures/nexmark.pdf" }],
178
+ sha256: { "paper.tex": sha("% p\n"), "figures/nexmark.pdf": sha("pdf") },
179
+ });
180
+ });
181
+ });
182
+
183
+ describe("the recorder the build is handed", () => {
184
+ it("sourcesRecorder is recordSources with its ports bound", () => {
185
+ const files = after("p1", { "paper.tex": "% p\n" });
186
+ const bound = sourcesRecorder(deps(files))(DIR, run("p1", 0));
187
+ expect(bound).toEqual(recordSources(deps(files), DIR, run("p1", 0)));
188
+ });
189
+ });
190
+
191
+ describe("what the record hashes", () => {
192
+ it("a database bibtex opened that no TeX input names is hashed all the same", () => {
193
+ const files = after("v17-include-order", {
194
+ "paper.tex": "% p\n",
195
+ "bibsetup.tex": "% s\n",
196
+ "first.bib": "@misc{firstkey}\n",
197
+ "later.bib": "@misc{laterkey}\n",
198
+ });
199
+ const out = recordSources(deps(files), DIR, run("v17-include-order", 2));
200
+ expect(out.kind === "recorded" ? out.record : out).toMatchObject({
201
+ bibdata: ["first", "later"],
202
+ bibtex: { ran: true, databases: ["first.bib"], exit: 2 },
203
+ sha256: {
204
+ "paper.tex": sha("% p\n"),
205
+ "bibsetup.tex": sha("% s\n"),
206
+ "first.bib": sha("@misc{firstkey}\n"),
207
+ },
208
+ });
209
+ });
210
+ });
211
+
212
+ describe("what bibtex reported", () => {
213
+ it("bibtex's errors are recorded as it reported them, with its exit", () => {
214
+ const files = after("v6-unclosed", {
215
+ "paper.tex": "% p\n",
216
+ "refs.bib": "@misc{a",
217
+ });
218
+ const out = recordSources(deps(files), DIR, run("v6-unclosed", 2));
219
+ expect(out.kind === "recorded" ? out.record.bibtex : out).toEqual({
220
+ ran: true,
221
+ databases: ["refs.bib"],
222
+ keys: ["a1", "a4", "a3", "a2unclosed"],
223
+ exit: 2,
224
+ errors: [
225
+ {
226
+ message: "I was expecting a `,' or a `}'",
227
+ file: "refs.bib",
228
+ line: 4,
229
+ },
230
+ ],
231
+ });
232
+ });
233
+ });
234
+
235
+ describe("a build with no bibtex, a missing file, no .fls", () => {
236
+ it("a build that ran no bibtex records none, reads no .blg or .bbl a build before it left, and counts a .bbl TeX read as an input", () => {
237
+ const files = after(
238
+ "v3-declared",
239
+ { "paper.tex": "% p\n", "paper.bbl": "b" },
240
+ ["paper.aux"],
241
+ );
242
+ const out = recordSources(deps(files), DIR, run("v3-declared", null));
243
+ expect(out.kind === "recorded" ? out.record : out).toEqual({
244
+ schema: 1,
245
+ inputs: [
246
+ { path: "paper.tex", role: "body" },
247
+ { path: "paper.bbl", role: "body" },
248
+ ],
249
+ written: [],
250
+ bibdata: ["paper"],
251
+ bibtex: { ran: false },
252
+ sha256: { "paper.tex": sha("% p\n"), "paper.bbl": sha("b") },
253
+ });
254
+ });
255
+ });
256
+
257
+ describe("a file gone, no .aux, no .fls", () => {
258
+ it("a file the record lists that is gone when it is made is recorded as absent, so its return is a change", () => {
259
+ const files = after("p1", {
260
+ "paper.tex": "% p\n",
261
+ "sections/intro.tex": "x",
262
+ });
263
+ const out = recordSources(deps(files), DIR, run("p1", 0));
264
+ expect(out.kind === "recorded" ? out.record.sha256 : out).toEqual({
265
+ "paper.tex": sha("% p\n"),
266
+ "refs.bib": null,
267
+ "sections/intro.tex": sha("x"),
268
+ });
269
+ });
270
+
271
+ it("a paper with no \\bibdata has none, and a missing .aux reads as none", () => {
272
+ const files = after("v3-declared", { "paper.tex": "% p\n" }, []);
273
+ const out = recordSources(deps(files), DIR, run("v3-declared", null));
274
+ expect(out.kind === "recorded" ? out.record.bibdata : out).toEqual([]);
275
+ });
276
+
277
+ it("bibtex that ran and left no .blg or .bbl (it could not start) records what it was given: nothing opened, nothing typeset", () => {
278
+ const files = after("v3-declared", { "paper.tex": "% p\n" }, ["paper.aux"]);
279
+ const out = recordSources(deps(files), DIR, run("v3-declared", 127));
280
+ expect(out.kind === "recorded" ? out.record.bibtex : out).toEqual({
281
+ ran: true,
282
+ databases: [],
283
+ keys: [],
284
+ exit: 127,
285
+ errors: [],
286
+ });
287
+ });
288
+
289
+ it("no .fls, or one that names no file of the paper, records nothing and says why", () => {
290
+ const files = memoryFiles({ [`${DIR}/paper.tex`]: "x" });
291
+ expect(
292
+ recordSources(deps(files), DIR, { fls: [], bibtexExit: null }),
293
+ ).toEqual({
294
+ kind: "not-recorded",
295
+ why: "pdflatex wrote no paper.fls — nothing to record",
296
+ });
297
+ expect(
298
+ recordSources(deps(files), DIR, {
299
+ fls: ["PWD /work/paper\nINPUT /usr/x.cls\n"],
300
+ bibtexExit: null,
301
+ }),
302
+ ).toEqual({
303
+ kind: "not-recorded",
304
+ why: "paper.fls lists no file of the paper directory — nothing to record",
305
+ });
306
+ expect(files.map.has(`${DIR}/_build/sources.json`)).toBe(false);
307
+ });
308
+ });
@@ -0,0 +1,231 @@
1
+ /**
2
+ * THE RECORD STEP OF A BUILD — `recordSources` writes `<paper>/_build/sources.json`
3
+ * (`src/domain/sources-record.ts`, docs/design/paper-sources.md §1) from what TeX and bibtex left in
4
+ * the paper directory after the build's passes: each pass's `.fls`, the `.aux`, the `.blg` and the
5
+ * `.bbl`. The files are read through the `Files` port, the `.fls` and the `.blg` through `TexOutput`;
6
+ * which files the passes read is decided by `filesOfRun` (`src/domain/tex-run.ts`).
7
+ *
8
+ * Nothing here reads TeX source. Every file named in the record is one TeX or bibtex opened, and its
9
+ * bytes are hashed as they are now: what a rule compares later to know whether the paper changed.
10
+ *
11
+ * What the `.aux`, `.blg` and `.bbl` hold about the bibliography is read by `latex-log.ts` — the reader
12
+ * the build loop already uses on them, so one reader answers what bibtex was told and what it read.
13
+ */
14
+ import { join, posix, resolve } from "node:path";
15
+ import { callerPath } from "./caller-path.ts";
16
+ import {
17
+ JOBNAME,
18
+ serializeSourcesRecord,
19
+ SOURCES_DIR,
20
+ SOURCES_FILE,
21
+ SOURCES_SCHEMA,
22
+ spelledIn,
23
+ type RecordedBibtex,
24
+ type RecordedInput,
25
+ type SourcesRecord,
26
+ } from "./domain/sources-record.ts";
27
+ import { sha256Hex } from "./domain/sha256.ts";
28
+ import { filesOfRun, type RunFiles } from "./domain/tex-run.ts";
29
+ import { auxBib, bibtexRead } from "./latex-log.ts";
30
+ import type { Files } from "./ports/files.ts";
31
+ import type { ListDir } from "./ports/talk-media.ts";
32
+ import type { TexOutput } from "./ports/tex-output.ts";
33
+
34
+ /** What recording needs: the disk, its directory listings, and the readers of TeX's own files. */
35
+ export interface RecordDeps {
36
+ readonly files: Files;
37
+ readonly listDir: ListDir;
38
+ readonly texOutput: TexOutput;
39
+ }
40
+
41
+ /** What a build's passes left to read, besides the files on disk. */
42
+ export interface TexRun {
43
+ /** The text of each pdflatex pass's `.fls`, in the order the passes ran. */
44
+ readonly fls: readonly string[];
45
+ /** The exit of the last bibtex run, or null when the build ran none. */
46
+ readonly bibtexExit: number | null;
47
+ }
48
+
49
+ export type Recorded =
50
+ | {
51
+ readonly kind: "recorded";
52
+ readonly record: SourcesRecord;
53
+ /** Where it was written. */
54
+ readonly path: string;
55
+ }
56
+ | { readonly kind: "not-recorded"; readonly why: string };
57
+
58
+ /** The step as the build holds it: wired by the composition root, handed a paper's directory and its run. */
59
+ export type RecordSources = (paperDir: string, run: TexRun) => Recorded;
60
+
61
+ /** No recorder wired: the build says so, and lint reports the paper as not built. */
62
+ export const notWiredRecord: RecordSources = () => ({
63
+ kind: "not-recorded",
64
+ why: "no recorder was wired into this build",
65
+ });
66
+
67
+ /** Where a paper's record lives. */
68
+ export const sourcesPath = (paperDir: string): string =>
69
+ join(paperDir, SOURCES_DIR, SOURCES_FILE);
70
+
71
+ const distinct = <T>(xs: readonly T[]): readonly T[] => [...new Set(xs)];
72
+
73
+ /** A path as the record spells it: relative to the paper directory, `./` and doubled slashes gone. */
74
+ const spelled = (p: string): string => posix.normalize(p);
75
+
76
+ /**
77
+ * A path of the record spelled as the paper directory's entries spell it. TeX logs the name it opened:
78
+ * on a file system that ignores case that is the source's spelling, while TeX on one that does not
79
+ * retries without case and logs the disk's — one paper, two records. A segment no entry matches, or
80
+ * several do, stays as TeX spelled it.
81
+ */
82
+ const spelledOnDisk =
83
+ (listDir: ListDir, paperDir: string) =>
84
+ (p: string): string =>
85
+ p
86
+ .split("/")
87
+ .reduce(
88
+ (at, segment) =>
89
+ posix.join(
90
+ at,
91
+ spelledIn(listDir(callerPath(resolve(paperDir, at))), segment) ??
92
+ segment,
93
+ ),
94
+ "",
95
+ );
96
+
97
+ /** The files a run read and wrote, each named as the disk names it. */
98
+ const respelled = (files: RunFiles, spell: (p: string) => string): RunFiles => {
99
+ const inputs = files.inputs.map((i) => ({ ...i, path: spell(i.path) }));
100
+ return {
101
+ ...files,
102
+ inputs: inputs.filter(
103
+ (i, n) => inputs.findIndex((j) => j.path === i.path) === n,
104
+ ),
105
+ written: distinct(files.written.map(spell)),
106
+ };
107
+ };
108
+
109
+ /** The bytes of a file of the paper by its path in the record, or null when it is not there. */
110
+ const bytesOf =
111
+ (deps: RecordDeps, paperDir: string) =>
112
+ (p: string): Uint8Array | null =>
113
+ deps.files.readBytes(callerPath(resolve(paperDir, p)));
114
+
115
+ const textOf =
116
+ (bytes: (p: string) => Uint8Array | null) =>
117
+ (p: string): string | null => {
118
+ const b = bytes(p);
119
+ return b === null ? null : new TextDecoder().decode(b);
120
+ };
121
+
122
+ /** What the build's bibtex did, from the `.blg` and `.bbl` it left — or that it did not run. */
123
+ function bibtexOf(
124
+ deps: RecordDeps,
125
+ text: (p: string) => string | null,
126
+ spell: (p: string) => string,
127
+ exit: number | null,
128
+ ): RecordedBibtex {
129
+ if (exit === null) return { ran: false };
130
+ const blg = text(`${JOBNAME}.blg`) ?? "";
131
+ const bbl = text(`${JOBNAME}.bbl`) ?? "";
132
+ const read = bibtexRead(blg, bbl);
133
+ return {
134
+ ran: true,
135
+ databases: distinct(read.databases.map((d) => spell(spelled(d)))),
136
+ keys: read.bibitems,
137
+ exit,
138
+ errors: deps.texOutput.blgErrors(blg),
139
+ };
140
+ }
141
+
142
+ /**
143
+ * The digest of every file the paper is made of: each input, and each database bibtex opened — one TeX
144
+ * wrote too, since the rules read it from disk and an edit to it after the build must read as a change.
145
+ * Null for one that is not there.
146
+ */
147
+ function hashesOf(
148
+ inputs: readonly RecordedInput[],
149
+ databases: readonly string[],
150
+ bytes: (p: string) => Uint8Array | null,
151
+ ): SourcesRecord["sha256"] {
152
+ const paths = distinct([...inputs.map((i) => i.path), ...databases]);
153
+ return Object.fromEntries(
154
+ paths.map((p) => {
155
+ const b = bytes(p);
156
+ return [p, b === null ? null : sha256Hex(b)] as const;
157
+ }),
158
+ );
159
+ }
160
+
161
+ /** Why there is nothing to record, or null when the passes read the paper's files. */
162
+ const nothingToRecord = (
163
+ run: TexRun,
164
+ inputs: readonly RecordedInput[],
165
+ ): string | null => {
166
+ if (run.fls.length === 0)
167
+ return `pdflatex wrote no ${JOBNAME}.fls — nothing to record`;
168
+ return inputs.length === 0
169
+ ? `${JOBNAME}.fls lists no file of the paper directory — nothing to record`
170
+ : null;
171
+ };
172
+
173
+ /** The record of a run whose passes read `files`: bibtex's part, the `.aux`'s, every digest. */
174
+ function recordOf(
175
+ deps: RecordDeps,
176
+ paperDir: string,
177
+ run: TexRun,
178
+ files: RunFiles,
179
+ ): SourcesRecord {
180
+ const bytes = bytesOf(deps, paperDir);
181
+ const text = textOf(bytes);
182
+ const bibtex = bibtexOf(
183
+ deps,
184
+ text,
185
+ spelledOnDisk(deps.listDir, paperDir),
186
+ run.bibtexExit,
187
+ );
188
+ return {
189
+ schema: SOURCES_SCHEMA,
190
+ inputs: files.inputs,
191
+ written: files.written,
192
+ bibdata: auxBib(text(`${JOBNAME}.aux`) ?? "", text).databases,
193
+ bibtex,
194
+ sha256: hashesOf(files.inputs, bibtex.ran ? bibtex.databases : [], bytes),
195
+ };
196
+ }
197
+
198
+ export function recordSources(
199
+ deps: RecordDeps,
200
+ paperDir: string,
201
+ run: TexRun,
202
+ ): Recorded {
203
+ const files = respelled(
204
+ filesOfRun(run.fls.map(deps.texOutput.fls), {
205
+ jobname: JOBNAME,
206
+ generated: run.bibtexExit === null ? [] : [`${JOBNAME}.bbl`],
207
+ }),
208
+ spelledOnDisk(deps.listDir, paperDir),
209
+ );
210
+ const why = nothingToRecord(run, files.inputs);
211
+ if (why !== null) return { kind: "not-recorded", why };
212
+ const record = recordOf(deps, paperDir, run, files);
213
+ const path = sourcesPath(paperDir);
214
+ deps.files.writeAtomic(
215
+ callerPath(path),
216
+ new TextEncoder().encode(serializeSourcesRecord(record)),
217
+ );
218
+ return { kind: "recorded", record, path };
219
+ }
220
+
221
+ /** `recordSources` over `deps`, as the `RecordSources` the build is handed. */
222
+ export const sourcesRecorder =
223
+ (deps: RecordDeps): RecordSources =>
224
+ (paperDir, run) =>
225
+ recordSources(deps, paperDir, run);
226
+
227
+ /** The line a build prints for a recording. */
228
+ export const recordingNote = (r: Recorded): string =>
229
+ r.kind === "recorded"
230
+ ? `sources: ${String(r.record.inputs.length)} file${r.record.inputs.length === 1 ? "" : "s"} read → ${SOURCES_DIR}/${SOURCES_FILE}`
231
+ : `sources NOT recorded — ${r.why}; lint will say so`;
@@ -0,0 +1,152 @@
1
+ /**
2
+ * `paper/sources-fresh` (`src/sources-rules.ts`) on papers held in memory, the way ESLint runs the rule:
3
+ * `create`, then `root:exit`. Both halves: a paper with no record, an unreadable one, or one the
4
+ * paper has changed since is reported once, at the top of `paper.tex`, saying what to run; a paper
5
+ * whose record is current is not, and neither is any file but `paper.tex`.
6
+ */
7
+ import { describe, expect, it } from "vitest";
8
+ import { memoryFiles } from "./adapters/memory/index.ts";
9
+ import { sourcesCodec } from "./adapters/sources-record/index.ts";
10
+ import { sha256Hex } from "./domain/sha256.ts";
11
+ import {
12
+ serializeSourcesRecord,
13
+ type SourcesRecord,
14
+ } from "./domain/sources-record.ts";
15
+ import { SOURCES_RULE_LEVELS, sourcesRules } from "./sources-rules.ts";
16
+ import { builtFixture } from "../test/recorded-fixture.ts";
17
+
18
+ const DIR = "/work/papers/p";
19
+ const bytes = (s: string) => new TextEncoder().encode(s);
20
+
21
+ const RECORD: SourcesRecord = {
22
+ schema: 1,
23
+ inputs: [
24
+ { path: "paper.tex", role: "body" },
25
+ { path: "sections/a.tex", role: "body" },
26
+ ],
27
+ written: [],
28
+ bibdata: [],
29
+ bibtex: { ran: false },
30
+ sha256: {
31
+ "paper.tex": sha256Hex(bytes("main")),
32
+ "sections/a.tex": sha256Hex(bytes("a")),
33
+ },
34
+ };
35
+ const FRESH = {
36
+ [`${DIR}/paper.tex`]: "main",
37
+ [`${DIR}/sections/a.tex`]: "a",
38
+ [`${DIR}/_build/sources.json`]: serializeSourcesRecord(RECORD),
39
+ };
40
+
41
+ interface Reported {
42
+ readonly messageId: string;
43
+ readonly message: string;
44
+ readonly line: number;
45
+ }
46
+
47
+ /** The rule over one file of the paper directory, the way ESLint runs it. */
48
+ function lint(
49
+ tree: Record<string, string>,
50
+ file = "paper.tex",
51
+ ): readonly Reported[] {
52
+ const rule = sourcesRules({
53
+ files: memoryFiles(tree),
54
+ codec: sourcesCodec,
55
+ })["sources-fresh"];
56
+ const out: Reported[] = [];
57
+ const visitor = rule.create({
58
+ filename: `${DIR}/${file}`,
59
+ sourceCode: { getLocFromIndex: () => ({ line: 1, column: 0 }) },
60
+ report: (d) => {
61
+ const template = rule.meta.messages[d.messageId] ?? "";
62
+ const message = Object.entries(d.data ?? {}).reduce(
63
+ (m, [k, v]) => m.split(`{{${k}}}`).join(v),
64
+ template,
65
+ );
66
+ out.push({ messageId: d.messageId, message, line: d.loc.start.line });
67
+ },
68
+ });
69
+ visitor["root:exit"]?.();
70
+ return out;
71
+ }
72
+
73
+ describe("paper/sources-fresh — reports", () => {
74
+ it("a paper no build has recorded is reported 'not built', and what that leaves unlinted", () => {
75
+ expect(
76
+ lint({ [`${DIR}/paper.tex`]: "main" }).map((r) => [
77
+ r.messageId,
78
+ r.message,
79
+ r.line,
80
+ ]),
81
+ ).toEqual([
82
+ [
83
+ "notBuilt",
84
+ "the paper has not been built — run `npx paperlint build`, which records the files TeX reads. Until it has, the files `paper.tex` includes are not linted, and the rules that read the bibliography say nothing",
85
+ 1,
86
+ ],
87
+ ]);
88
+ });
89
+
90
+ it("a record this paperlint cannot read is reported, with why", () => {
91
+ expect(
92
+ lint({
93
+ ...FRESH,
94
+ [`${DIR}/_build/sources.json`]: '{"schema":2}',
95
+ }).map((r) => [r.messageId, r.message]),
96
+ ).toEqual([
97
+ [
98
+ "unreadable",
99
+ "the last build's record cannot be used (sources.json is schema 2, this paperlint reads schema 1) — run `npx paperlint build`; until then the files `paper.tex` includes are not linted, and the rules that read the bibliography say nothing",
100
+ ],
101
+ ]);
102
+ });
103
+
104
+ it("a paper changed since the build is reported, naming what changed", () => {
105
+ expect(
106
+ lint({ ...FRESH, [`${DIR}/sections/a.tex`]: "edited" }).map((r) => [
107
+ r.messageId,
108
+ r.message,
109
+ ]),
110
+ ).toEqual([
111
+ [
112
+ "stale",
113
+ "the paper changed since the last build (sections/a.tex edited) — run `npx paperlint build`; until then the files `paper.tex` includes are not linted, and the rules that read the bibliography say nothing",
114
+ ],
115
+ ]);
116
+ });
117
+ });
118
+
119
+ describe("paper/sources-fresh — a .tex TeX wrote from a filecontents block", () => {
120
+ it("🔴 is a file of the paper: current as the build left it, stale once it is gone or edited", () => {
121
+ // v29-generated-input: the block in paper.tex wrote body.tex, and the body \input's it.
122
+ const built = builtFixture("v29-generated-input", DIR);
123
+ const cleaned = Object.fromEntries(
124
+ Object.entries(built).filter(([p]) => p !== `${DIR}/body.tex`),
125
+ );
126
+ expect([
127
+ lint(built),
128
+ lint(cleaned).map((r) => r.message),
129
+ lint({ ...built, [`${DIR}/body.tex`]: "edited\n" }).map(
130
+ (r) => r.messageId,
131
+ ),
132
+ ]).toEqual([
133
+ [],
134
+ [expect.stringContaining("(body.tex deleted)")],
135
+ ["stale"],
136
+ ]);
137
+ });
138
+ });
139
+
140
+ describe("paper/sources-fresh — silent", () => {
141
+ it("a paper whose record is current is not reported", () => {
142
+ expect(lint(FRESH)).toEqual([]);
143
+ });
144
+
145
+ it("only paper.tex is judged: a record that is missing is not reported on another file", () => {
146
+ expect(lint({ [`${DIR}/other.tex`]: "x" }, "other.tex")).toEqual([]);
147
+ });
148
+
149
+ it("the rule is on, as a warning: lint often runs where nothing is built", () => {
150
+ expect(SOURCES_RULE_LEVELS).toEqual({ "paper/sources-fresh": "warn" });
151
+ });
152
+ });