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,117 @@
1
+ /**
2
+ * `paper/sources-fresh` — the one finding for a paper whose build record cannot be used
3
+ * (docs/design/paper-sources.md §1, docs/rules/paper/sources-fresh.md). Every rule that needs to know
4
+ * which files make up the paper, or which bibliography TeX reads, reads `_build/sources.json`
5
+ * (`paperRecord`, `src/paper-record.ts`); when there is none, or the paper has changed since, they
6
+ * are silent and this rule speaks once instead:
7
+ *
8
+ * no record "not built" — run `npx paperlint build`
9
+ * a record this paperlint cannot read the reason, and the same command
10
+ * a file changed since the build which files, and the same command
11
+ *
12
+ * A warning, not an error, for the reason `pdf/measured` is one: lint often runs where nothing is
13
+ * built (the CI action only lints). Each message says what stays unchecked until a build: the files
14
+ * `paper.tex` includes are not linted, and the rules over the bibliography say nothing.
15
+ */
16
+ import { basename, dirname } from "node:path";
17
+ import { callerPath } from "./caller-path.ts";
18
+ import { describeChanges } from "./domain/sources-record.ts";
19
+ import { MAIN_FILE } from "./domain/paper-sources.ts";
20
+ import { paperRecord, type RecordReadDeps } from "./paper-record.ts";
21
+
22
+ /** One finding: its message, and the values the message names. */
23
+ interface Finding {
24
+ readonly messageId: string;
25
+ readonly data?: Readonly<Record<string, string>>;
26
+ }
27
+
28
+ interface Loc {
29
+ readonly line: number;
30
+ readonly column: number;
31
+ }
32
+ interface RuleContext {
33
+ readonly filename: string;
34
+ readonly sourceCode: { getLocFromIndex(i: number): Loc };
35
+ report(
36
+ d: Finding & { readonly loc: Readonly<{ start: Loc; end: Loc }> },
37
+ ): void;
38
+ }
39
+ export interface SourcesRuleModule {
40
+ readonly meta: {
41
+ readonly type: "problem" | "suggestion";
42
+ readonly docs: {
43
+ readonly description: string;
44
+ readonly url: string;
45
+ };
46
+ readonly schema: readonly object[];
47
+ readonly messages: Readonly<Record<string, string>>;
48
+ };
49
+ create(context: RuleContext): { "root:exit"?: () => void };
50
+ }
51
+
52
+ const BUILD = "`npx paperlint build`";
53
+ const UNTIL =
54
+ "until then the files `paper.tex` includes are not linted, and the rules that read the bibliography say nothing";
55
+
56
+ const META: SourcesRuleModule["meta"] = {
57
+ type: "problem",
58
+ docs: {
59
+ description:
60
+ "the build recorded which files TeX read, and the paper has not changed since (_build/sources.json)",
61
+ url: "https://github.com/zernie/paperlint/blob/main/docs/rules/paper/sources-fresh.md",
62
+ },
63
+ schema: [],
64
+ messages: {
65
+ notBuilt: `the paper has not been built — run ${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`,
66
+ unreadable: `the last build's record cannot be used ({{why}}) — run ${BUILD}; ${UNTIL}`,
67
+ stale: `the paper changed since the last build ({{changed}}) — run ${BUILD}; ${UNTIL}`,
68
+ },
69
+ };
70
+
71
+ /** The finding for a paper's record, or none when it is current. */
72
+ function findingFor(dir: string, deps: RecordReadDeps): Finding | null {
73
+ const r = paperRecord(dir, deps);
74
+ switch (r.kind) {
75
+ case "none":
76
+ return r.unreadable === null
77
+ ? { messageId: "notBuilt" }
78
+ : { messageId: "unreadable", data: { why: r.unreadable } };
79
+ case "stale":
80
+ return {
81
+ messageId: "stale",
82
+ data: { changed: describeChanges(r.changed) },
83
+ };
84
+ case "fresh":
85
+ return null;
86
+ }
87
+ }
88
+
89
+ /** `paper/sources-fresh`, reading the paper through `deps`. It acts on `paper.tex` only. */
90
+ export function sourcesRules(
91
+ deps: RecordReadDeps,
92
+ ): Readonly<Record<"sources-fresh", SourcesRuleModule>> {
93
+ return {
94
+ "sources-fresh": {
95
+ meta: META,
96
+ create: (context) =>
97
+ basename(context.filename) !== MAIN_FILE
98
+ ? {}
99
+ : {
100
+ "root:exit": () => {
101
+ const f = findingFor(
102
+ dirname(callerPath(context.filename)),
103
+ deps,
104
+ );
105
+ if (f === null) return;
106
+ const at = context.sourceCode.getLocFromIndex(0);
107
+ context.report({ loc: { start: at, end: at }, ...f });
108
+ },
109
+ },
110
+ },
111
+ };
112
+ }
113
+
114
+ /** The level it is on at for every `paper.tex` in paperlint's own config. */
115
+ export const SOURCES_RULE_LEVELS = {
116
+ "paper/sources-fresh": "warn",
117
+ } as const;
@@ -47,7 +47,6 @@ const paper = (name: string, files: readonly string[]) => {
47
47
 
48
48
  try {
49
49
  paper("complete", ["PIPELINE-STATUS.md", "paper.tex", "refs.bib"]);
50
- paper("complete-md", ["PIPELINE-STATUS.md", "paper.md"]);
51
50
  // the marker is present (paper.tex), the scorecard is not — the directory is linted by ZERO
52
51
  // rules and reports clean
53
52
  paper("no-scorecard", ["paper.tex", "refs.bib"]);
@@ -65,10 +64,6 @@ try {
65
64
  "a complete paper directory — NOT A SINGLE finding",
66
65
  at("complete").length === 0,
67
66
  );
68
- check(
69
- "and `paper.md` counts on equal footing with `paper.tex` — the corpus holds both forms",
70
- at("complete-md").length === 0,
71
- );
72
67
  check(
73
68
  "a missing scorecard — a finding",
74
69
  at("no-scorecard").length === 1 &&
@@ -86,10 +81,11 @@ try {
86
81
  /no-scorecard/.test(at("no-scorecard")[0]?.message ?? ""),
87
82
  );
88
83
  check(
89
- "a directory with no source — a finding, and BOTH accepted forms are listed",
84
+ "a directory with no source — a finding naming `paper.tex`",
90
85
  at("no-source").length === 1 &&
91
- /`paper\.tex`/.test(at("no-source")[0]?.message ?? "") &&
92
- /`paper\.md`/.test(at("no-source")[0]?.message ?? ""),
86
+ /`paper\.tex` — a paper directory with no source/.test(
87
+ at("no-source")[0]?.message ?? "",
88
+ ),
93
89
  );
94
90
 
95
91
  // 🔴 THE PAIRED HALF: detection is GENEROUS. Without this the check would scream about every
@@ -124,9 +120,7 @@ try {
124
120
  {
125
121
  cwd: root,
126
122
  },
127
- ).some(
128
- (x) => x.file.endsWith("complete-md") && /refs\.bib/.test(x.message),
129
- ),
123
+ ).some((x) => x.file.endsWith("no-source") && /refs\.bib/.test(x.message)),
130
124
  );
131
125
  check(
132
126
  "a nonexistent root does not crash it — that's the empty-set guard talking",
package/src/structure.ts CHANGED
@@ -26,6 +26,7 @@ import { readdirSync, existsSync } from "node:fs";
26
26
  import { join, relative, basename, isAbsolute, sep } from "node:path";
27
27
  import type { StructureConfig, StructureFinding } from "./types.ts";
28
28
  import { CONFIG_FILE } from "#lib/paper-config";
29
+ import { SOURCE_FILE } from "./new-paper.ts";
29
30
 
30
31
  /** The requirements after the consumer's config is laid over the defaults. */
31
32
  type Rules = Required<StructureConfig>;
@@ -38,9 +39,9 @@ type Rules = Required<StructureConfig>;
38
39
  * byte-compared by the `paper/stages` rule anyway.
39
40
  */
40
41
  export const STRUCTURE_DEFAULTS = {
41
- markers: ["PIPELINE-STATUS.md", "paper.tex", "paper.md", CONFIG_FILE],
42
+ markers: ["PIPELINE-STATUS.md", SOURCE_FILE, CONFIG_FILE],
42
43
  require: ["PIPELINE-STATUS.md"],
43
- requireOneOf: [["paper.tex", "paper.md"]],
44
+ requireOneOf: [[SOURCE_FILE]],
44
45
  ignore: [],
45
46
  };
46
47
 
@@ -118,7 +119,7 @@ export function checkStructure(
118
119
  * The message NAMES THE CONSEQUENCE rather than restating the condition. "missing
119
120
  * PIPELINE-STATUS.md" without the second half reads as nitpicking about formatting; with it you
120
121
  * can see which checks the directory silently loses — the rules that read the scorecard; the rules
121
- * over `paper.md` / `paper.tex` still run.
122
+ * over `paper.tex` still run.
122
123
  */
123
124
  function whyMissingMatters(file: string, dirName: string): string {
124
125
  if (file === "PIPELINE-STATUS.md")
@@ -1,106 +1,162 @@
1
1
  /**
2
- * The reading every parse-tree rule shares (`readPaper`), the files of the paper's body that lint
3
- * reads on their own (`bodyFiles`), and where a finding of the assembled text is reported
4
- * (`reportInPaper`).
2
+ * The reading every parse-tree rule shares (`readPaper`): the paper's text with the files TeX read
3
+ * spliced in — the files the build's record lists, and no others — and where a finding of that text
4
+ * is reported (`reportInPaper`). The record fed to it is TeX's own answer for a planted paper
5
+ * (`fixtures/paper-sources/<name>/tex-truth.json`).
5
6
  */
6
7
  import { describe, expect, it } from "vitest";
7
8
  import { latexReader } from "./adapters/latex/index.ts";
8
9
  import { memoryFiles } from "./adapters/memory/index.ts";
10
+ import { sourcesCodec } from "./adapters/sources-record/index.ts";
9
11
  import { join } from "node:path";
10
12
  import { texInputsDir } from "./package-dirs.ts";
11
- import { bodyFiles, readPaper, reportInPaper } from "./tex-paper.ts";
12
-
13
- describe("readPaper — the paper as the rules read it", () => {
14
- it("reads includes from the paper's own directory", () => {
15
- const p = readPaper("/p/paper.tex", "x\\input{s/a}y", {
16
- files: memoryFiles({ "/p/s/a.tex": "AAA" }),
17
- latex: latexReader,
18
- });
19
- expect(p.text).toBe("xAAAy");
13
+ import { readPaper, reportInPaper } from "./tex-paper.ts";
14
+ import {
15
+ builtFixture,
16
+ builtPaper,
17
+ fixtureFiles,
18
+ } from "../test/recorded-fixture.ts";
19
+
20
+ /** What `readPaper` reads with: the files held in memory, the LaTeX reader, the record's schema. */
21
+ const proseDeps = (files: Record<string, string>) => ({
22
+ files: memoryFiles(files),
23
+ latex: latexReader,
24
+ codec: sourcesCodec,
25
+ });
26
+
27
+ const MAIN = "/p/paper.tex";
28
+ const textOf = (files: Record<string, string>): string => files[MAIN] ?? "";
29
+
30
+ describe("readPaper — the paper as the rules read it, from the files TeX read", () => {
31
+ it("🔴 splices a file the record lists where its include stands, and TeX's end of line after it", () => {
32
+ // p1: TeX read sections/intro.tex, and the file's last line ends with a newline.
33
+ const files = builtFixture("p1", "/p");
34
+ const p = readPaper(MAIN, textOf(files), proseDeps(files));
35
+ const main = textOf(files);
36
+ const intro = files["/p/sections/intro.tex"] ?? "";
37
+ expect(p.text).toBe(main.replace("\\input{sections/intro}", intro));
20
38
  expect(p.main).toBe("paper.tex");
21
39
  });
22
40
 
23
- it("an include the paper's directory lacks is found in paperlint's own inputs, as the build finds it", () => {
24
- const p = readPaper("/p/paper.tex", "x\\input{guards}y", {
25
- files: memoryFiles({ [join(texInputsDir(), "guards.tex")]: "G" }),
26
- latex: latexReader,
27
- });
28
- expect({ text: p.text, missing: p.missing }).toEqual({
29
- text: "xGy",
30
- missing: [],
31
- });
41
+ it("an include behind a comment or `\\iffalse` is not spliced: the record does not list it", () => {
42
+ const files = builtFixture("p1", "/p");
43
+ const { text } = readPaper(MAIN, textOf(files), proseDeps(files));
44
+ expect(text).not.toContain("Dead section");
45
+ expect(text).not.toContain("Parked section");
46
+ });
47
+
48
+ it("🔴 a file the text names after \\end{document} is not spliced: TeX never read it", () => {
49
+ // v23-parked-include: `\input{parked}` stands past the end of the document; parked.tex is on
50
+ // disk, and the record lists paper.tex alone.
51
+ const files = builtFixture("v23-parked-include", "/p");
52
+ expect(files["/p/parked.tex"]).toBeDefined();
53
+ const p = readPaper(MAIN, textOf(files), proseDeps(files));
54
+ expect(p.text).toBe(textOf(files));
55
+ expect(p.text).not.toContain("\\bibliography{old}");
56
+ });
57
+
58
+ it("🔴 a .tex a filecontents block wrote is spliced where the body \\input's it: TeX typeset its text", () => {
59
+ // v29-generated-input: the record lists body.tex, which the block in paper.tex wrote.
60
+ const files = builtFixture("v29-generated-input", "/p");
61
+ const p = readPaper(MAIN, textOf(files), proseDeps(files));
62
+ expect(p.text).toBe(
63
+ textOf(files).replace("\\input{body}", files["/p/body.tex"] ?? ""),
64
+ );
65
+ expect([...new Set(p.segments.map((s) => s.file))]).toEqual([
66
+ "paper.tex",
67
+ "body.tex",
68
+ ]);
32
69
  });
33
70
 
34
- it("the paper's directory comes first: its file wins over paperlint's of the same name", () => {
35
- const p = readPaper("/p/paper.tex", "\\input{guards}", {
36
- files: memoryFiles({
37
- "/p/guards.tex": "mine",
38
- [join(texInputsDir(), "guards.tex")]: "package",
39
- }),
40
- latex: latexReader,
41
- });
42
- expect(p.text).toBe("mine");
71
+ it("🔴 with no record, the paper is paper.tex alone: its includes are not read", () => {
72
+ const files = fixtureFiles("p1", "/p");
73
+ const p = readPaper(MAIN, textOf(files), proseDeps(files));
74
+ expect(p.text).toBe(textOf(files));
75
+ expect(p.segments).toHaveLength(1);
76
+ });
77
+
78
+ it("🔴 with a stale record, the paper is paper.tex alone", () => {
79
+ const files = {
80
+ ...builtFixture("p1", "/p"),
81
+ "/p/sections/intro.tex": "\\section{Changed}\n",
82
+ };
83
+ const p = readPaper(MAIN, textOf(files), proseDeps(files));
84
+ expect(p.text).toBe(textOf(files));
43
85
  });
44
86
  });
45
87
 
46
- describe("bodyFiles — the files of the body lint reads on their own", () => {
47
- const doc = (preamble: string, body: string): string =>
48
- `\\documentclass{article}\n${preamble}\n\\begin{document}\n${body}\n\\end{document}\n`;
49
- const files = (extra: Record<string, string> = {}) =>
50
- memoryFiles({
51
- "/p/macros.tex": "\\newcommand{\\x}{y}",
52
- "/p/sections/a.tex": "A\n\\input{sections/b}\n",
53
- "/p/sections/b.tex": "B",
54
- [join(texInputsDir(), "guards.tex")]: "G",
55
- ...extra,
56
- });
57
-
58
- it("every file an include in the document body brings in, nested ones too, by absolute path", () => {
59
- expect(
60
- bodyFiles("/p/paper.tex", doc("", "\\input{sections/a}"), {
61
- files: files(),
62
- latex: latexReader,
63
- }),
64
- ).toEqual({
65
- files: ["/p/sections/a.tex", "/p/sections/b.tex"],
66
- missing: [],
67
- });
88
+ describe("readPaper — which files, in which order", () => {
89
+ it("a file the record lists that is not on disk (it was absent when recorded and still is) contributes nothing", () => {
90
+ const files = builtPaper("/p", { "paper.tex": "x\\input{gone}y" }, [
91
+ { path: "paper.tex", role: "body" },
92
+ { path: "gone.tex", role: "body" },
93
+ ]);
94
+ expect(readPaper(MAIN, textOf(files), proseDeps(files)).text).toBe("xy");
68
95
  });
69
96
 
70
- it("not a preamble include (macros), and not a file found only in paperlint's inputs", () => {
71
- expect(
72
- bodyFiles(
73
- "/p/paper.tex",
74
- doc("\\input{macros}", "\\input{guards}\nText."),
75
- { files: files(), latex: latexReader },
76
- ),
77
- ).toEqual({ files: [], missing: [] });
97
+ it("files are found in the paper's directory only: paperlint's inputs are not the paper's", () => {
98
+ const files = {
99
+ ...builtPaper("/p", { "paper.tex": "x\\input{guards}y" }, [
100
+ { path: "paper.tex", role: "body" },
101
+ ]),
102
+ [join(texInputsDir(), "guards.tex")]: "G",
103
+ };
104
+ const p = readPaper(MAIN, textOf(files), proseDeps(files));
105
+ expect(p.text).toBe("xy");
78
106
  });
79
107
 
80
- it("an include that resolves nowhere is named, with the file that wrote it", () => {
81
- expect(
82
- bodyFiles("/p/paper.tex", doc("", "\\input{gone}"), {
83
- files: files(),
84
- latex: latexReader,
85
- }),
86
- ).toEqual({ files: [], missing: [{ file: "paper.tex", target: "gone" }] });
108
+ it("nested includes are spliced in the order TeX read them, each once", () => {
109
+ const files = builtPaper(
110
+ "/p",
111
+ {
112
+ "paper.tex": "A\n\\input{sections/a}\nZ\n",
113
+ "sections/a.tex": "a1\n\\input{sections/b}\na2\n",
114
+ "sections/b.tex": "b1",
115
+ },
116
+ [
117
+ { path: "paper.tex", role: "body" },
118
+ { path: "sections/a.tex", role: "body" },
119
+ { path: "sections/b.tex", role: "body" },
120
+ ],
121
+ );
122
+ const p = readPaper(MAIN, textOf(files), proseDeps(files));
123
+ expect(p.text).toBe("A\na1\nb1\n\na2\n\nZ\n");
124
+ expect([...new Set(p.segments.map((s) => s.file))]).toEqual([
125
+ "paper.tex",
126
+ "sections/a.tex",
127
+ "sections/b.tex",
128
+ ]);
87
129
  });
88
130
 
89
- it("a main file with no document environment: every include is the body", () => {
90
- expect(
91
- bodyFiles("/p/paper.tex", "\\input{sections/b}", {
92
- files: files(),
93
- latex: latexReader,
94
- }),
95
- ).toEqual({ files: ["/p/sections/b.tex"], missing: [] });
131
+ it("a preamble file the record lists is spliced too: it is a file of the paper", () => {
132
+ const files = builtPaper(
133
+ "/p",
134
+ {
135
+ "paper.tex": "\\input{macros}\n\\begin{document}\nText.\n",
136
+ "macros.tex": "\\newcommand{\\x}{y}\n",
137
+ },
138
+ [
139
+ { path: "paper.tex", role: "body" },
140
+ { path: "macros.tex", role: "preamble" },
141
+ ],
142
+ );
143
+ const p = readPaper(MAIN, textOf(files), proseDeps(files));
144
+ expect(p.text).toBe("\\newcommand{\\x}{y}\n\n\\begin{document}\nText.\n");
96
145
  });
97
146
  });
98
147
 
99
148
  describe("reportInPaper — where a finding of the assembled text is reported", () => {
100
- const paper = readPaper("/p/paper.tex", "x\n\\input{a}\ny", {
101
- files: memoryFiles({ "/p/a.tex": "one\ntwo" }),
102
- latex: latexReader,
103
- });
149
+ const main = "x\n\\input{a}\ny";
150
+ const paper = readPaper(
151
+ "/p/paper.tex",
152
+ main,
153
+ proseDeps(
154
+ builtPaper("/p", { "paper.tex": main, "a.tex": "one\ntwo" }, [
155
+ { path: "paper.tex", role: "body" },
156
+ { path: "a.tex", role: "body" },
157
+ ]),
158
+ ),
159
+ );
104
160
  const report = (
105
161
  at: { start: number; end: number } | null,
106
162
  messageId = "m",
@@ -146,3 +202,17 @@ describe("reportInPaper — where a finding of the assembled text is reported",
146
202
  ]);
147
203
  });
148
204
  });
205
+
206
+ describe("readPaper — how an include names its file", () => {
207
+ it("🔴 an include spelled otherwise than the disk is the file the record names: TeX opened it by either spelling", () => {
208
+ const files = builtPaper(
209
+ "/p",
210
+ { "paper.tex": "x\\input{Sections/Intro}y", "sections/intro.tex": "I" },
211
+ [
212
+ { path: "paper.tex", role: "body" },
213
+ { path: "sections/intro.tex", role: "body" },
214
+ ],
215
+ );
216
+ expect(readPaper(MAIN, textOf(files), proseDeps(files)).text).toBe("xI\ny");
217
+ });
218
+ });
package/src/tex-paper.ts CHANGED
@@ -7,9 +7,13 @@
7
7
  * file at the include in `paper.tex` that brought it in — the one place ESLint can point to in this
8
8
  * file — with the included file, line and column at the front of the message.
9
9
  *
10
- * An include is looked for where the build tells TeX to look (`texSearchPath`): the paper's own
11
- * directory, then paperlint's inputs. `bodyFiles` names the files of the paper's BODY the author
12
- * wrote — what the rules over ESLint's own LaTeX text read one by one, each at its own path.
10
+ * 🔴 WHICH FILES ARE THE PAPER'S IS TEX'S ANSWER, NOT THIS MODULE'S (docs/design/paper-sources.md §1).
11
+ * `readPaper` splices only the files the build's record lists (`_build/sources.json`, `paperRecord`):
12
+ * the files of the paper directory TeX read. An include of a file the record does not list — behind
13
+ * `\iffalse`, never reached — contributes nothing, as it did to TeX. With no
14
+ * record, or a stale one, the paper is `paper.tex` alone, and `paper/sources-fresh` says the files it
15
+ * includes went unlinted. Where an include stands in the text is the only thing read from the text:
16
+ * whether the file it names is part of the paper is the record's to say, never the disk's.
13
17
  */
14
18
  import { basename, dirname, join } from "node:path";
15
19
  import {
@@ -21,8 +25,8 @@ import type { Span } from "./domain/tex-document.ts";
21
25
  import type { LatexReader } from "./ports/latex.ts";
22
26
  import type { Files } from "./ports/files.ts";
23
27
  import { callerPath } from "./caller-path.ts";
24
- import type { AbsolutePath } from "./domain/paths.ts";
25
- import { texSearchPath } from "./package-dirs.ts";
28
+ import { paperRecord, type RecordReadDeps } from "./paper-record.ts";
29
+ import { spelledIn } from "./domain/sources-record.ts";
26
30
  import type { Located, TexRuleContext } from "./tex-venue-rules.ts";
27
31
 
28
32
  /** What reading a paper needs: the disk, and the LaTeX reader. */
@@ -31,75 +35,41 @@ export interface PaperDeps {
31
35
  readonly latex: LatexReader;
32
36
  }
33
37
 
34
- /** The paper whose main file is `filename` with the text `src`, its includes spliced. */
38
+ /** What `readPaper` needs: the disk, the LaTeX reader (where an include stands), and the record's schema. */
39
+ export interface ProseDeps extends PaperDeps, RecordReadDeps {}
40
+
41
+ /** The text of a file read from the disk, or null when there was none. */
42
+ export const decoded = (b: Uint8Array | null): string | null =>
43
+ b === null ? null : new TextDecoder().decode(b);
44
+
45
+ /**
46
+ * The paper whose main file is `filename` with the text `src`: the files the last build's record lists
47
+ * spliced where their includes stand. No record, or one the paper has outgrown, is `src` alone.
48
+ */
35
49
  export function readPaper(
36
50
  filename: string,
37
51
  src: string,
38
- deps: PaperDeps,
52
+ deps: ProseDeps,
39
53
  ): PaperSource {
40
54
  const dir = dirname(filename);
55
+ const record = paperRecord(dir, deps);
56
+ const listed =
57
+ record.kind === "fresh" ? record.record.inputs.map((i) => i.path) : [];
41
58
  return assemblePaper(basename(filename), src, {
42
- includes: deps.latex.includes,
59
+ // No record: no include is looked at, so there is nothing to splice.
60
+ includes: record.kind === "fresh" ? deps.latex.includes : () => [],
43
61
  documentBody: deps.latex.documentBody,
44
62
  read: (rel) => {
45
- const at = located(dir, rel, deps.files);
46
- const b = at === null ? null : deps.files.readBytes(at);
47
- return b === null ? null : new TextDecoder().decode(b);
63
+ const recorded = spelledIn(listed, rel);
64
+ return recorded === null
65
+ ? null
66
+ : decoded(deps.files.readBytes(callerPath(join(dir, recorded))));
48
67
  },
49
68
  });
50
69
  }
51
70
 
52
- /** `rel` in the first directory of the paper's TeX search path that holds it, or null. */
53
- const located = (dir: string, rel: string, files: Files): AbsolutePath | null =>
54
- texSearchPath(dir)
55
- .map((d) => callerPath(join(d, rel)))
56
- .find((p) => files.isFile(p)) ?? null;
57
-
58
- /** An include that resolved nowhere: the file that wrote it, and the path as written. */
59
- export interface Unread {
60
- readonly file: string;
61
- readonly target: string;
62
- }
63
-
64
- /**
65
- * The files of the body of the paper whose main file is `filename`: every file an include inside its
66
- * `document` environment brings in, nested ones too, found in the paper's own directory. Not a
67
- * preamble include (macros are not the body), and not a file found only in paperlint's inputs (not
68
- * the author's text). `missing` is every include, anywhere, that resolved nowhere.
69
- */
70
- export function bodyFiles(
71
- filename: string,
72
- src: string,
73
- deps: PaperDeps,
74
- ): {
75
- readonly files: readonly AbsolutePath[];
76
- readonly missing: readonly Unread[];
77
- } {
78
- const dir = dirname(filename);
79
- const paper = readPaper(filename, src, deps);
80
- const body = deps.latex.documentBody(src);
81
- const inBody = (via: Span | null): boolean =>
82
- via !== null &&
83
- (body === null || (via.start >= body.start && via.end <= body.end));
84
- const own = (rel: string): AbsolutePath | null => {
85
- const at = located(dir, rel, deps.files);
86
- return at === callerPath(join(dir, rel)) ? at : null;
87
- };
88
- return {
89
- files: [
90
- ...new Set(
91
- paper.segments
92
- .filter((s) => s.file !== paper.main && inBody(s.via))
93
- .map((s) => own(s.file))
94
- .filter((f): f is AbsolutePath => f !== null),
95
- ),
96
- ],
97
- missing: paper.missing.map(({ file, target }) => ({ file, target })),
98
- };
99
- }
100
-
101
71
  /** Line and column (1-based) of an offset in a text. */
102
- function lineColumn(text: string, at: number): string {
72
+ export function lineColumn(text: string, at: number): string {
103
73
  const line = text.slice(0, at).split("\n").length;
104
74
  const column = at - text.lastIndexOf("\n", at - 1);
105
75
  return `${String(line)}:${String(column)}`;