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
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * The paper as one text: includes spliced where they stand, every character mapped back to its
3
- * file, a missing file reported, a subfile's body only, and a file that includes itself entered once.
3
+ * file, a file with nothing behind it left out, a subfile's body only, and a file that includes itself entered once.
4
4
  * The reader is a stand-in that finds `\input{…}` by position, so these tests are about assembling,
5
5
  * not about parsing LaTeX (the LaTeX adapter's own tests cover `includes`).
6
6
  */
@@ -41,8 +41,7 @@ describe("assemblePaper — the text TeX reads", () => {
41
41
  main,
42
42
  deps({ "s/one.tex": "one", "two.tex": "two" }),
43
43
  );
44
- expect(p.text).toBe("AoneBtwoC");
45
- expect(p.missing).toEqual([]);
44
+ expect(p.text).toBe("Aone\nBtwo\nC");
46
45
  expect([...new Set(p.segments.map((g) => g.file))]).toEqual([
47
46
  "paper.tex",
48
47
  "s/one.tex",
@@ -57,7 +56,7 @@ describe("assemblePaper — the text TeX reads", () => {
57
56
  main,
58
57
  deps({ "a.tex": "[\\input{s/b}]", "s/b.tex": "BB" }),
59
58
  );
60
- expect(p.text).toBe("x[BB]y");
59
+ expect(p.text).toBe("x[BB\n]\ny");
61
60
  expect(originOf(p, { start: 2, end: 4 })).toEqual({
62
61
  file: "s/b.tex",
63
62
  source: "BB",
@@ -66,18 +65,10 @@ describe("assemblePaper — the text TeX reads", () => {
66
65
  });
67
66
  });
68
67
 
69
- it("🔴 a missing file is reported where it stands, and left out", () => {
70
- const main = "a\\input{gone}b";
71
- const p = assemblePaper("paper.tex", main, deps({}));
68
+ it("a file the reader has none of contributes nothing", () => {
69
+ const p = assemblePaper("paper.tex", "a\\input{gone}b", deps({}));
72
70
  expect(p.text).toBe("ab");
73
- expect(p.missing).toEqual([
74
- {
75
- file: "paper.tex",
76
- target: "gone",
77
- span: { start: 1, end: 13 },
78
- via: { start: 1, end: 13 },
79
- },
80
- ]);
71
+ expect(p.segments.map((g) => g.file)).toEqual(["paper.tex", "paper.tex"]);
81
72
  });
82
73
  });
83
74
 
@@ -99,7 +90,7 @@ describe("assemblePaper — subfiles and a file that includes itself", () => {
99
90
  "<\\subfile{s}>",
100
91
  deps({ "s.tex": "ALL" }),
101
92
  );
102
- expect(p.text).toBe("<ALL>");
93
+ expect(p.text).toBe("<ALL\n>");
103
94
  });
104
95
 
105
96
  it("a file that includes itself is entered once", () => {
@@ -108,7 +99,54 @@ describe("assemblePaper — subfiles and a file that includes itself", () => {
108
99
  "m\\input{a}",
109
100
  deps({ "a.tex": "a\\input{a}\\input{paper.tex}" }),
110
101
  );
111
- expect(p.text).toBe("ma");
102
+ expect(p.text).toBe("ma\n");
103
+ });
104
+ });
105
+
106
+ describe("assemblePaper — TeX's end of line after the last line of an included file", () => {
107
+ // Measured with TeX Live 2026: `a\input f b` with f = `foo`, no final newline, typesets «afoo b» —
108
+ // the file's last line is ended like any other; `f` = `bar` plus a newline typesets the same; an
109
+ // empty file reads no line and adds nothing.
110
+ it("a file whose last line has no newline is ended, so what follows it is a new word", () => {
111
+ const p = assemblePaper(
112
+ "paper.tex",
113
+ "a\\input{f}b",
114
+ deps({ "f.tex": "foo" }),
115
+ );
116
+ expect(p.text).toBe("afoo\nb");
117
+ });
118
+
119
+ it("a file that ends with a newline gets no second one", () => {
120
+ const p = assemblePaper(
121
+ "paper.tex",
122
+ "a\\input{f}b",
123
+ deps({ "f.tex": "foo\n" }),
124
+ );
125
+ expect(p.text).toBe("afoo\nb");
126
+ });
127
+
128
+ it("an empty file reads no line: it adds nothing", () => {
129
+ const p = assemblePaper("paper.tex", "a\\input{f}b", deps({ "f.tex": "" }));
130
+ expect(p.text).toBe("ab");
131
+ });
132
+
133
+ it("the main file is not ended: its text is the caller's, as given", () => {
134
+ const p = assemblePaper("paper.tex", "no newline", deps({}));
135
+ expect(p.text).toBe("no newline");
136
+ });
137
+
138
+ it("the end of line belongs to the file it ends, at the end of that file", () => {
139
+ const p = assemblePaper(
140
+ "paper.tex",
141
+ "a\\input{f}b",
142
+ deps({ "f.tex": "foo" }),
143
+ );
144
+ expect(originOf(p, { start: 4, end: 5 })).toEqual({
145
+ file: "f.tex",
146
+ source: "foo",
147
+ span: { start: 3, end: 4 },
148
+ via: { start: 1, end: 10 },
149
+ });
112
150
  });
113
151
  });
114
152
 
@@ -129,7 +167,7 @@ describe("originOf — where a span of the assembled text comes from", () => {
129
167
  });
130
168
 
131
169
  it("after the include: offsets in the main file past the macro", () => {
132
- expect(originOf(p, { start: 5, end: 7 })).toEqual({
170
+ expect(originOf(p, { start: 6, end: 8 })).toEqual({
133
171
  file: "paper.tex",
134
172
  source: "ab\\input{x}cd",
135
173
  span: { start: 11, end: 13 },
@@ -147,10 +185,10 @@ describe("originOf — where a span of the assembled text comes from", () => {
147
185
  });
148
186
 
149
187
  it("an empty span at the very end belongs to the last stretch; outside the text there is none", () => {
150
- expect(originOf(p, { start: 7, end: 7 })?.span).toEqual({
188
+ expect(originOf(p, { start: 8, end: 8 })?.span).toEqual({
151
189
  start: 13,
152
190
  end: 13,
153
191
  });
154
- expect(originOf(p, { start: 9, end: 9 })).toBe(null);
192
+ expect(originOf(p, { start: 10, end: 10 })).toBe(null);
155
193
  });
156
194
  });
@@ -9,12 +9,19 @@
9
9
  * answers about one file (the class, the outline, the rendered text, the body's passages) is
10
10
  * answered about the paper, unchanged. A subfile is a document of its own; only its body is spliced.
11
11
  *
12
+ * ── THE END OF AN INCLUDED FILE'S LAST LINE ─────────────────────────────────────
13
+ * TeX ends every line it reads with its end-of-line character, the last line of a file included:
14
+ * `a\input{f}b` with `f` holding `foo` and no final newline typesets "afoo b" (measured, TeX Live
15
+ * 2026), not "afoob". The splice does the same: an included file whose text does not end in a newline
16
+ * gets one, mapped to the end of that file; an empty file read no line and gets nothing. The main file
17
+ * is the caller's text and is left as given.
18
+ *
12
19
  * ── WHAT IS NOT DONE ─────────────────────────────────────────────────────────────
13
20
  * No macro is expanded, so a path built from a macro (`\input{\dir/intro}`) is read as written. A
14
- * file that cannot be found is left out and named in `missing`; a file that includes itself,
15
- * directly or through others, is spliced once.
21
+ * file the caller's `read` has none of contributes nothing; a file that includes itself, directly or
22
+ * through others, is spliced once.
16
23
  */
17
- import { extname, normalize } from "node:path";
24
+ import { posix } from "node:path";
18
25
  import type { Span } from "./tex-document.ts";
19
26
 
20
27
  /** One `\input`, `\include` or `\subfile` in a source: the path as written and where the macro stands. */
@@ -46,27 +53,17 @@ export interface Segment {
46
53
  readonly via: Span | null;
47
54
  }
48
55
 
49
- /** An include whose file was not found, where it stands. */
50
- export interface MissingInclude {
51
- readonly file: string;
52
- readonly target: string;
53
- readonly span: Span;
54
- /** Where it stands in the main file (the include itself, or the one that brought its file in). */
55
- readonly via: Span;
56
- }
57
-
58
- /** The paper as one text, the map back to its files, and the includes that were not found. */
56
+ /** The paper as one text, and the map back to its files. */
59
57
  export interface PaperSource {
60
58
  readonly main: string;
61
59
  readonly text: string;
62
60
  readonly segments: readonly Segment[];
63
- readonly missing: readonly MissingInclude[];
64
61
  }
65
62
 
66
63
  /** The paths TeX tries for a target: `name.tex`, then `name` — or the name alone if it has one. */
67
64
  const candidates = (target: string): readonly string[] => {
68
- const t = normalize(target.trim());
69
- return extname(t) === "" ? [`${t}.tex`, t] : [t];
65
+ const t = posix.normalize(target.trim());
66
+ return posix.extname(t) === "" ? [`${t}.tex`, t] : [t];
70
67
  };
71
68
 
72
69
  /** The first candidate path that exists, with its text. */
@@ -81,11 +78,10 @@ function found(
81
78
  return hits[0] ?? null;
82
79
  }
83
80
 
84
- /** An assembled piece of text: the text, its segments from 0, and the includes not found. */
81
+ /** An assembled piece of text: the text and its segments from 0. */
85
82
  interface Piece {
86
83
  readonly text: string;
87
84
  readonly segments: readonly Segment[];
88
- readonly missing: readonly MissingInclude[];
89
85
  }
90
86
 
91
87
  /** `segments` moved by `by` characters. */
@@ -111,11 +107,10 @@ const copied = ({ file, text, from, to, via }: Stretch): Piece => ({
111
107
  to > from
112
108
  ? [{ start: 0, end: to - from, file, source: text, from, via }]
113
109
  : [],
114
- missing: [],
115
110
  });
116
111
 
117
112
  /** Nothing: an include left out. */
118
- const NOTHING: Piece = { text: "", segments: [], missing: [] };
113
+ const NOTHING: Piece = { text: "", segments: [] };
119
114
 
120
115
  /** Pieces joined in order. */
121
116
  const joined = (pieces: readonly Piece[]): Piece =>
@@ -123,7 +118,6 @@ const joined = (pieces: readonly Piece[]): Piece =>
123
118
  (acc, p) => ({
124
119
  text: acc.text + p.text,
125
120
  segments: [...acc.segments, ...shifted(p.segments, acc.text.length)],
126
- missing: [...acc.missing, ...p.missing],
127
121
  }),
128
122
  NOTHING,
129
123
  );
@@ -153,7 +147,7 @@ function spliced(file: string, text: string, range: Span, ctx: Context): Piece {
153
147
  pieces: [
154
148
  ...acc.pieces,
155
149
  at(acc.cursor, inc.span.start),
156
- included(file, inc, ctx),
150
+ included(inc, ctx),
157
151
  ],
158
152
  cursor: inc.span.end,
159
153
  }),
@@ -162,23 +156,30 @@ function spliced(file: string, text: string, range: Span, ctx: Context): Piece {
162
156
  return joined([...pieces, at(cursor, range.end)]);
163
157
  }
164
158
 
165
- /** One include: the file it names, spliced, or — when not found — nothing, and a report. */
166
- function included(file: string, inc: Include, ctx: Context): Piece {
159
+ /** TeX's end of line after the last line of `file`, standing where its text ends: one newline. */
160
+ const endOfLine = (file: string, text: string, via: Span | null): Piece => ({
161
+ text: "\n",
162
+ segments: [{ start: 0, end: 1, file, source: text, from: text.length, via }],
163
+ });
164
+
165
+ /** One include: the file it names, spliced, or — when the reader has none of it — nothing. */
166
+ function included(inc: Include, ctx: Context): Piece {
167
167
  const via = ctx.via ?? inc.span;
168
168
  const hit = found(inc.target, ctx.deps.read);
169
- if (hit === null)
170
- return {
171
- ...NOTHING,
172
- missing: [{ file, target: inc.target, span: inc.span, via }],
173
- };
174
- if (ctx.stack.includes(hit.path)) return NOTHING;
169
+ if (hit === null || ctx.stack.includes(hit.path)) return NOTHING;
175
170
  const body = inc.macro === "subfile" ? ctx.deps.documentBody(hit.text) : null;
176
- return spliced(
177
- hit.path,
178
- hit.text,
179
- body ?? { start: 0, end: hit.text.length },
180
- { deps: ctx.deps, stack: [...ctx.stack, hit.path], via },
181
- );
171
+ const range = body ?? { start: 0, end: hit.text.length };
172
+ const piece = spliced(hit.path, hit.text, range, {
173
+ deps: ctx.deps,
174
+ stack: [...ctx.stack, hit.path],
175
+ via,
176
+ });
177
+ // The last line of the file is read to its end; a subfile's body ends inside its last line.
178
+ const endsLine =
179
+ range.end === hit.text.length &&
180
+ hit.text !== "" &&
181
+ !hit.text.endsWith("\n");
182
+ return endsLine ? joined([piece, endOfLine(hit.path, hit.text, via)]) : piece;
182
183
  }
183
184
 
184
185
  /** The paper whose main file is `main` (a path relative to the paper directory) with `text`. */
@@ -193,12 +194,7 @@ export function assemblePaper(
193
194
  { start: 0, end: text.length },
194
195
  { deps, stack: [main], via: null },
195
196
  );
196
- return {
197
- main,
198
- text: p.text,
199
- segments: p.segments,
200
- missing: p.missing,
201
- };
197
+ return { main, text: p.text, segments: p.segments };
202
198
  }
203
199
 
204
200
  /** Where a span of the assembled text stands: in which file, at which offsets, and via which include. */
@@ -0,0 +1,94 @@
1
+ /**
2
+ * WHAT A DATABASE IS — the values the bibtex reader (`BibReader`, src/ports/bib-reader.ts) returns for
3
+ * a `.bib`, or for the body of a `filecontents` block: its entries, and the bytes bibtex takes from
4
+ * them. Which databases a paper reads is TeX's answer, recorded by the build (`_build/sources.json`,
5
+ * src/domain/sources-record.ts, docs/design/paper-sources.md §1); nothing here decides it.
6
+ */
7
+ import type { AbsolutePath } from "./paths.ts";
8
+ import type { Span } from "./tex-document.ts";
9
+
10
+ /** The main file's name — the file TeX is given. */
11
+ export const MAIN_FILE = "paper.tex";
12
+
13
+ /** A name in a name list (`author`, `editor`), split into its parts as BibTeX defines them. */
14
+ export interface BibName {
15
+ readonly lastName?: string;
16
+ readonly firstName?: string;
17
+ readonly prefix?: string;
18
+ readonly suffix?: string;
19
+ /** A name written as one unit (`{World Health Organization}`). */
20
+ readonly name?: string;
21
+ }
22
+
23
+ /**
24
+ * The `@string`s in force: each name as bibtex folds it (upper case) → its value as bibtex takes it,
25
+ * the `@string`s it used expanded and its LaTeX as written. bibtex reads a paper's databases one after
26
+ * the other with one table of them, so a `@string` an earlier database defines is in force in a later
27
+ * one (measured: `\\bibliography{abbreviations,references}` typesets an entry of `references.bib` with
28
+ * the author and booktitle `abbreviations.bib` defines; in the other order both are undefined).
29
+ */
30
+ export type BibStrings = Readonly<Record<string, string>>;
31
+
32
+ /** One entry of a database. */
33
+ export interface BibEntry {
34
+ /** Lower-cased (`@Article` → `article`). */
35
+ readonly type: string;
36
+ readonly key: string;
37
+ /** From its `@` to its closing brace or parenthesis, in the text of the file that holds it. */
38
+ readonly span: Span;
39
+ /** Its fields, LaTeX read into text (`{\"u}` → `ü`, `\url{x}` → a link); a list field joined by ` and `. */
40
+ readonly fields: Readonly<Record<string, string>>;
41
+ /** Its name lists (`author`, `editor`, …), each name split into its parts. */
42
+ readonly names: Readonly<Record<string, readonly BibName[]>>;
43
+ }
44
+
45
+ /** The bytes of one database, where they sit, and what the reader read from them. */
46
+ export interface BibText {
47
+ /** The file holding the bytes: the `.bib`, or the `.tex` that holds the block. */
48
+ readonly path: AbsolutePath;
49
+ /** That file's whole text; every span here is an offset into it. */
50
+ readonly text: string;
51
+ /** The database inside `text`: all of it for a `.bib`, the body for a block. */
52
+ readonly body: Span;
53
+ readonly entries: readonly BibEntry[];
54
+ /** The same entries as bibtex takes them, in order: what two databases are compared by. */
55
+ readonly written: readonly WrittenEntry[];
56
+ /** The `@preamble` commands, as written, in order: bibtex writes them into the `.bbl`. */
57
+ readonly preamble: readonly string[];
58
+ /** The `@string`s the databases bibtex read before this one left in force: none for one read alone. */
59
+ readonly inherited: BibStrings;
60
+ /** The `@string`s in force after it — `inherited` and its own, a later one winning: what the next database starts with. */
61
+ readonly strings: BibStrings;
62
+ }
63
+
64
+ /**
65
+ * An entry as bibtex takes it from the database: the type, the key, and each field's value with the
66
+ * `@string`s it uses expanded where it uses them (a later redefinition changes nothing) and its LaTeX
67
+ * as written — `{\"o}` and `ö` are different values here, as they are in the `.bbl` bibtex writes.
68
+ */
69
+ export interface WrittenEntry {
70
+ /** Lower-cased, as bibtex reads it. */
71
+ readonly type: string;
72
+ readonly key: string;
73
+ /** Whitespace runs folded to one space, as bibtex folds them. */
74
+ readonly fields: Readonly<Record<string, string>>;
75
+ }
76
+
77
+ /**
78
+ * What bibtex takes from a database, for comparing two: each entry as bibtex reads it, in order, its
79
+ * fields in name order, and the `@preamble` commands. Nothing else in the bytes reaches the `.bbl`:
80
+ * not the text between entries (a `filecontents` header among it), not a `@string` no entry uses.
81
+ */
82
+ const contentOf = (b: BibText): string =>
83
+ JSON.stringify([
84
+ b.written.map((e) => [
85
+ e.type,
86
+ e.key,
87
+ Object.entries(e.fields).sort(([x], [y]) => x.localeCompare(y)),
88
+ ]),
89
+ b.preamble,
90
+ ]);
91
+
92
+ /** Whether two databases hold the same, as TeX's own copy of a block holds the block's. */
93
+ export const sameDatabase = (a: BibText, b: BibText): boolean =>
94
+ contentOf(a) === contentOf(b);
@@ -0,0 +1,71 @@
1
+ /**
2
+ * The record of a build as a value: the one text it is parsed from, what a rule reads off it, and what
3
+ * changed since it was made. The files it is about are `src/sources-record.test.ts`'s.
4
+ */
5
+ import { describe, expect, it } from "vitest";
6
+ import { parseSha256 } from "./sha256.ts";
7
+ import {
8
+ changesSince,
9
+ describeChanges,
10
+ serializeSourcesRecord,
11
+ type SourcesRecord,
12
+ } from "./sources-record.ts";
13
+
14
+ const A = "a".repeat(64);
15
+ const B = "b".repeat(64);
16
+
17
+ const RECORD: SourcesRecord = {
18
+ schema: 1,
19
+ inputs: [
20
+ { path: "paper.tex", role: "body" },
21
+ { path: "macros.tex", role: "preamble" },
22
+ { path: "sections/intro.tex", role: "body" },
23
+ ],
24
+ written: ["refs.bib"],
25
+ bibdata: ["refs"],
26
+ bibtex: {
27
+ ran: true,
28
+ databases: ["refs.bib"],
29
+ keys: ["k1"],
30
+ exit: 2,
31
+ errors: [{ message: "bad", file: "refs.bib", line: 4 }],
32
+ },
33
+ sha256: { "paper.tex": A, "macros.tex": null },
34
+ };
35
+
36
+ describe("serializing a record", () => {
37
+ it("is two-space JSON with a final newline, so a diff of two builds reads", () => {
38
+ expect(serializeSourcesRecord({ ...RECORD, bibtex: { ran: false } })).toBe(
39
+ `${JSON.stringify({ ...RECORD, bibtex: { ran: false } }, null, 2)}\n`,
40
+ );
41
+ });
42
+ });
43
+
44
+ describe("what changed since the record", () => {
45
+ const now = (files: Readonly<Record<string, string>>) => (p: string) =>
46
+ files[p] === undefined ? null : parseSha256(files[p]);
47
+
48
+ it("a record whose files are as hashed has no change, a file absent then and now included", () => {
49
+ expect(changesSince(RECORD, now({ "paper.tex": A }))).toEqual([]);
50
+ });
51
+
52
+ it("an edited file, a deleted one, and a file that appeared where there was none", () => {
53
+ expect(changesSince(RECORD, now({ "macros.tex": B }))).toEqual([
54
+ { path: "paper.tex", change: "deleted" },
55
+ { path: "macros.tex", change: "added" },
56
+ ]);
57
+ expect(changesSince(RECORD, now({ "paper.tex": B }))).toEqual([
58
+ { path: "paper.tex", change: "edited" },
59
+ ]);
60
+ });
61
+
62
+ it("names the changes in a line a finding can carry", () => {
63
+ expect(
64
+ describeChanges([
65
+ { path: "paper.tex", change: "edited" },
66
+ { path: "sections/a.tex", change: "deleted" },
67
+ ]),
68
+ ).toBe("paper.tex edited, sections/a.tex deleted");
69
+ expect(describeChanges([])).toBe("");
70
+ });
71
+ });
@@ -0,0 +1,136 @@
1
+ /**
2
+ * WHAT THE LAST BUILD READ — `<paper>/_build/sources.json`, written by `paperlint build` from TeX's
3
+ * own files (docs/design/paper-sources.md §1) and read, offline, by the rules that need to know which
4
+ * files make up a paper and which bibliography it uses. Nothing else answers either question: no
5
+ * code of paperlint reads TeX source to decide them.
6
+ *
7
+ * { "schema": 1,
8
+ * "inputs": [ { "path": "paper.tex", "role": "body" }, … ], the files of the paper directory TeX
9
+ * read, in first-read order
10
+ * "written": [ "refs.bib" ], the `.bib` and `.tex` files TeX wrote (filecontents)
11
+ * "bibdata": [ "refs" ], what `\bibdata` told bibtex to read
12
+ * "bibtex": { "ran": true, "databases": [ "refs.bib" ], the files bibtex opened, the keys it
13
+ * "keys": [ … ], "exit": 0, "errors": [] }, typeset, how it ended
14
+ * "sha256": { "paper.tex": "…", … } } every input and every database bibtex
15
+ * opened, by path and bytes
16
+ *
17
+ * 🔴 A role is the file's, and the main file is the body. A file is `preamble` when TeX read it only
18
+ * before `\begin{document}` — before it opened `paper.aux` for writing (measured) — and `body` when
19
+ * any read of it came after. The main file, the first one read, is `body` whatever its place in that
20
+ * order: it holds `\begin{document}` itself.
21
+ *
22
+ * The text of the file is checked once, where it enters: `SourcesCodec` (`src/ports/sources-codec.ts`),
23
+ * whose adapter holds the zod schema of this shape. The domain holds the value.
24
+ *
25
+ * 🔴 STALENESS is the record's own question: `changesSince` compares the bytes now with `sha256`, one
26
+ * key per file, so a rule can tell that the paper changed after the build without running TeX. A `.bib`
27
+ * TeX wrote is hashed too: the rules read it from disk, so an edit to it after the build is a change.
28
+ */
29
+ import type { Sha256 } from "./sha256.ts";
30
+
31
+ export const SOURCES_SCHEMA = 1;
32
+ export const SOURCES_DIR = "_build";
33
+ export const SOURCES_FILE = "sources.json";
34
+
35
+ /** The job name every file a build writes carries (`paper.aux`, `paper.fls`): the build names its job so. */
36
+ export const JOBNAME = "paper";
37
+
38
+ /** Before or after `\begin{document}`. */
39
+ export type InputRole = "preamble" | "body";
40
+
41
+ /** One file of the paper directory that TeX read. `path` is relative to it, with forward slashes. */
42
+ export interface RecordedInput {
43
+ readonly path: string;
44
+ readonly role: InputRole;
45
+ }
46
+
47
+ /** One error bibtex reported, as its `.blg` says it. */
48
+ export interface BibtexError {
49
+ readonly message: string;
50
+ /** The file it was reading (`refs.bib`, `paper.aux`). */
51
+ readonly file: string;
52
+ /** Null when bibtex named no line. */
53
+ readonly line: number | null;
54
+ }
55
+
56
+ /** What the build's bibtex did — or that the build ran none (the paper names no database). */
57
+ export type RecordedBibtex =
58
+ | { readonly ran: false }
59
+ | {
60
+ readonly ran: true;
61
+ /** The database files it opened, as it names them (`refs.bib`, `bibs/x.bib`). */
62
+ readonly databases: readonly string[];
63
+ /** The keys it typeset: every `\bibitem` of the `.bbl`. */
64
+ readonly keys: readonly string[];
65
+ readonly exit: number;
66
+ readonly errors: readonly BibtexError[];
67
+ };
68
+
69
+ export interface SourcesRecord {
70
+ readonly schema: typeof SOURCES_SCHEMA;
71
+ readonly inputs: readonly RecordedInput[];
72
+ readonly written: readonly string[];
73
+ readonly bibdata: readonly string[];
74
+ readonly bibtex: RecordedBibtex;
75
+ /** Path → digest of its bytes; null for a file that was not there when the record was made. */
76
+ readonly sha256: Readonly<Record<string, string | null>>;
77
+ }
78
+
79
+ export type ParsedRecord =
80
+ | { readonly ok: true; readonly record: SourcesRecord }
81
+ | { readonly ok: false; readonly why: string };
82
+
83
+ /** The record as the file holds it: two-space JSON and a final newline, so a diff of two builds reads. */
84
+ export const serializeSourcesRecord = (r: SourcesRecord): string =>
85
+ `${JSON.stringify(r, null, 2)}\n`;
86
+
87
+ // ── staleness ───────────────────────────────────────────────────────────────────────────
88
+
89
+ export type ChangeKind = "edited" | "deleted" | "added";
90
+
91
+ /** One file whose bytes are not those the build hashed. */
92
+ export interface Change {
93
+ readonly path: string;
94
+ readonly change: ChangeKind;
95
+ }
96
+
97
+ /**
98
+ * The files whose bytes now differ from the record's: edited, deleted, or present where the record saw
99
+ * none. `now` answers for one path with the digest of its bytes, or null when there is no such file.
100
+ * Empty when the record is current.
101
+ */
102
+ export function changesSince(
103
+ r: SourcesRecord,
104
+ now: (path: string) => Sha256 | null,
105
+ ): readonly Change[] {
106
+ return Object.entries(r.sha256).flatMap(
107
+ ([path, recorded]): readonly Change[] => {
108
+ const current = now(path);
109
+ if (current === recorded) return [];
110
+ if (current === null) return [{ path, change: "deleted" }];
111
+ return [{ path, change: recorded === null ? "added" : "edited" }];
112
+ },
113
+ );
114
+ }
115
+
116
+ /** `paper.tex edited, sections/a.tex deleted` — what a finding names. */
117
+ export const describeChanges = (cs: readonly Change[]): string =>
118
+ cs.map((c) => `${c.path} ${c.change}`).join(", ");
119
+
120
+ /**
121
+ * The name among `names` that `name` stands for: `name` itself, else the one name equal to it ignoring
122
+ * case — null when there is none, or several. TeX opens a file by the name the source gives it; on a
123
+ * file system that ignores case that is the source's spelling, on one that does not TeX retries
124
+ * without case, and the record holds the disk's spelling either way. A name and the record's spelling
125
+ * of it are matched by this, once, wherever one is looked up in the other.
126
+ */
127
+ export function spelledIn(
128
+ names: readonly string[],
129
+ name: string,
130
+ ): string | null {
131
+ if (names.includes(name)) return name;
132
+ const [only, ...more] = names.filter(
133
+ (n) => n.toLowerCase() === name.toLowerCase(),
134
+ );
135
+ return only !== undefined && more.length === 0 ? only : null;
136
+ }