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,356 @@
1
+ /**
2
+ * The `bib` rules through real ESLint on the `tex/latex` language: `bib/reachable-entry` judges the
3
+ * entries of the databases the build's bibtex OPENED (`_build/sources.json`), fires, stays silent, and
4
+ * reports where the entry is — on its line in `paper.tex` for a `.bib` TeX wrote from a block there,
5
+ * at the top of `paper.tex` with `refs.bib:3:1:` first for any other. A paper with no record, or one
6
+ * that changed since, gets nothing from this rule (`paper/sources-fresh` speaks once instead).
7
+ * On the planted papers of fixtures/paper-sources too, with the record TeX wrote for each.
8
+ */
9
+ import { join } from "node:path";
10
+ import { ESLint, type Linter } from "eslint";
11
+ import { describe, expect, it } from "vitest";
12
+ import { texLanguage } from "../eslint-rules/latex-language.ts";
13
+ import { bibReader } from "./adapters/bibtex/index.ts";
14
+ import { latexReader } from "./adapters/latex/index.ts";
15
+ import { memoryFiles } from "./adapters/memory/index.ts";
16
+ import { sourcesCodec } from "./adapters/sources-record/index.ts";
17
+ import { BIB_RULE_LEVELS, bibRules } from "./bib-rules.ts";
18
+ import { eslintConfig } from "./cli.ts";
19
+ import {
20
+ leftByTeX,
21
+ planted,
22
+ recordText,
23
+ type Shape,
24
+ } from "../test/recorded-fixture.ts";
25
+ import { present } from "../test/support.ts";
26
+
27
+ const PAPER = "/work/papers/p";
28
+
29
+ /** A paper in memory: its files by relative path, and the record its build wrote (none: not built). */
30
+ interface Paper {
31
+ readonly tex: string;
32
+ readonly files?: Readonly<Record<string, string>>;
33
+ readonly record?: string | Shape | null;
34
+ }
35
+
36
+ /** Lint `paper.tex` (or `file`) of a paper laid out as TeX leaves it. */
37
+ async function lint(
38
+ p: Paper,
39
+ { file = "paper.tex", buffer = p.tex } = {},
40
+ ): Promise<{ messages: Linter.LintMessage[] }> {
41
+ const all = { "paper.tex": p.tex, ...p.files };
42
+ const record =
43
+ p.record === undefined || p.record === null
44
+ ? null
45
+ : typeof p.record === "string"
46
+ ? p.record
47
+ : recordText(all, p.record);
48
+ const files = memoryFiles({
49
+ ...Object.fromEntries(
50
+ Object.entries(all).map(([rel, text]) => [`${PAPER}/${rel}`, text]),
51
+ ),
52
+ ...(record === null ? {} : { [`${PAPER}/_build/sources.json`]: record }),
53
+ });
54
+ const eslint = new ESLint({
55
+ cwd: "/work",
56
+ overrideConfigFile: true,
57
+ overrideConfig: eslintConfig([
58
+ {
59
+ files: ["**/*.tex"],
60
+ plugins: {
61
+ tex: { languages: { latex: texLanguage } },
62
+ bib: {
63
+ rules: bibRules({
64
+ files,
65
+ codec: sourcesCodec,
66
+ latex: latexReader,
67
+ bib: bibReader,
68
+ }),
69
+ },
70
+ },
71
+ language: "tex/latex",
72
+ linterOptions: { reportUnusedDisableDirectives: "error" },
73
+ rules: BIB_RULE_LEVELS,
74
+ },
75
+ ]),
76
+ });
77
+ const [res] = await eslint.lintText(buffer, {
78
+ filePath: join(PAPER, file),
79
+ });
80
+ const r = present(res, "one lint result");
81
+ expect(r.messages.filter((m) => m.fatal)).toEqual([]);
82
+ return { messages: r.messages };
83
+ }
84
+
85
+ const found = (
86
+ messages: readonly Linter.LintMessage[],
87
+ rule: string,
88
+ ): readonly (readonly [number, number, string])[] =>
89
+ messages
90
+ .filter((m) => m.ruleId === `bib/${rule}`)
91
+ .map((m) => [m.line, m.column, m.message] as const);
92
+
93
+ /** A paper whose bibliography is a block, and the `.bib` TeX wrote from it, as a build leaves them. */
94
+ const inline = (entries: string): Paper => ({
95
+ tex: `\\documentclass{acmart}\n\\begin{filecontents*}[overwrite]{refs.bib}\n${entries}\n\\end{filecontents*}\n\\begin{document}\nx\n\\bibliography{refs}\n\\end{document}\n`,
96
+ files: { "refs.bib": `${entries}\n` },
97
+ record: {
98
+ inputs: [["paper.tex", "body"]],
99
+ written: ["refs.bib"],
100
+ databases: ["refs.bib"],
101
+ },
102
+ });
103
+
104
+ const DECLARING =
105
+ "\\documentclass{acmart}\n\\begin{document}\nx\n\\bibliography{refs}\n\\end{document}\n";
106
+ const KEPT: Shape = {
107
+ inputs: [
108
+ ["paper.tex", "body"],
109
+ ["refs.bib", "preamble"],
110
+ ],
111
+ databases: ["refs.bib"],
112
+ };
113
+ /** A paper whose bibliography is the `refs.bib` the author keeps. */
114
+ const declaring = (bib: string): Paper => ({
115
+ tex: DECLARING,
116
+ files: { "refs.bib": bib },
117
+ record: KEPT,
118
+ });
119
+
120
+ describe("bib/reachable-entry", () => {
121
+ it("a doi, a url or an arXiv id each make an entry reachable; @string is not an entry", async () => {
122
+ const { messages } = await lint(
123
+ inline(
124
+ "@article{a, doi = {10.1/x}}\n@misc{b, url = {https://x.org}}\n@misc{c, note = {arXiv:2310.05736}}\n@string{v = {x}}",
125
+ ),
126
+ );
127
+ expect(found(messages, "reachable-entry")).toEqual([]);
128
+ });
129
+
130
+ // Measured on the accepted corpus: 89 entries carry their only link as `howpublished = {\url{…}}`,
131
+ // which the bibliography style typesets — a link the reader has.
132
+ it("a \\url or \\href in any field is a link the reader has; a bare address in a field the style drops is not", async () => {
133
+ const { messages } = await lint(
134
+ inline(
135
+ "@misc{web, howpublished = {\\url{https://x.org}}}\n@misc{ref, note = {see \\href{https://x.org}{here}}}\n@techreport{src, source = {http://x.org}}",
136
+ ),
137
+ );
138
+ expect(
139
+ found(messages, "reachable-entry").map((m) => m[2].slice(0, 5)),
140
+ ).toEqual(["`src`"]);
141
+ });
142
+ });
143
+
144
+ describe("bib/reachable-entry — keys and where a finding goes", () => {
145
+ it("an entry written with no key is named `?`", async () => {
146
+ const { messages } = await lint(inline("@article{, title = {x}}"));
147
+ expect(
148
+ found(messages, "reachable-entry").map((m) => m[2].slice(0, 3)),
149
+ ).toEqual(["`?`"]);
150
+ });
151
+
152
+ it("an entry of a `.bib` TeX wrote from a block of paper.tex is reported on its own line there", async () => {
153
+ const { messages } = await lint(
154
+ inline("@misc{ok, url = {https://x.org}}\n@article{lost, title = {x}}"),
155
+ );
156
+ expect(found(messages, "reachable-entry")).toEqual([
157
+ [4, 1, expect.stringMatching(/^`lost` has no doi/)],
158
+ ]);
159
+ });
160
+
161
+ it("🔴 an entry in refs.bib — a file the author keeps — is reported at the top of paper.tex, with its file and line", async () => {
162
+ const { messages } = await lint(
163
+ declaring(
164
+ "@misc{ok, url = {https://x.org}}\n\n@article{lost, title = {x}}\n",
165
+ ),
166
+ );
167
+ expect(found(messages, "reachable-entry")).toEqual([
168
+ [1, 1, expect.stringMatching(/^refs\.bib:3:1: `lost` has no doi/)],
169
+ ]);
170
+ });
171
+
172
+ it("in a .bib, the directive on the line above the entry keeps the exception where the entry is", async () => {
173
+ const { messages } = await lint(
174
+ declaring(
175
+ "% eslint-disable-next-line bib/reachable-entry -- an invited talk, no recording exists\n@misc{talk, title = {x}}\n" +
176
+ "% eslint-disable-next-line bib/other-rule\n@misc{lost, title = {y}}\n",
177
+ ),
178
+ );
179
+ expect(found(messages, "reachable-entry").map((m) => m[2])).toEqual([
180
+ expect.stringMatching(/^refs\.bib:4:1: `lost`/),
181
+ ]);
182
+ });
183
+
184
+ it("🔴 inside the block, a disable directive silences exactly the next entry", async () => {
185
+ const { messages } = await lint(
186
+ inline(
187
+ "% eslint-disable-next-line bib/reachable-entry -- an invited talk, no recording exists\n@misc{talk, title = {x}}\n@misc{lost, title = {y}}",
188
+ ),
189
+ );
190
+ expect(found(messages, "reachable-entry").map((m) => m[2])).toEqual([
191
+ expect.stringMatching(/^`lost`/),
192
+ ]);
193
+ });
194
+ });
195
+
196
+ describe("bib/reachable-entry — a block is only a lookup of where the text stands", () => {
197
+ it("🔴 the block is a lookup of where the text stands: when paper.tex no longer holds what TeX read, the finding is at the top, naming the .bib", async () => {
198
+ const p = inline("@article{lost, title = {x}}");
199
+ const { messages } = await lint(p, {
200
+ buffer: p.tex.replace("{x}", "{edited since the build}"),
201
+ });
202
+ expect(found(messages, "reachable-entry")).toEqual([
203
+ [1, 1, expect.stringMatching(/^refs\.bib:1:1: `lost`/)],
204
+ ]);
205
+ });
206
+
207
+ it("a block in an included file: the finding is at the top of paper.tex, naming the entry in the file that holds the block", async () => {
208
+ const entries =
209
+ "@misc{ok, url = {https://x.org}}\n@article{lost, title = {x}}";
210
+ const { messages } = await lint({
211
+ tex: "\\documentclass{acmart}\n\\input{bibblock}\n\\begin{document}\nx\n\\bibliography{refs}\n\\end{document}\n",
212
+ files: {
213
+ "bibblock.tex": `\\begin{filecontents*}{refs.bib}\n${entries}\n\\end{filecontents*}\n`,
214
+ "refs.bib": `${entries}\n`,
215
+ },
216
+ record: {
217
+ inputs: [
218
+ ["paper.tex", "body"],
219
+ ["bibblock.tex", "preamble"],
220
+ ],
221
+ written: ["refs.bib"],
222
+ databases: ["refs.bib"],
223
+ },
224
+ });
225
+ expect(found(messages, "reachable-entry")).toEqual([
226
+ [1, 1, expect.stringMatching(/^bibblock\.tex:3:1: `lost`/)],
227
+ ]);
228
+ });
229
+
230
+ it("🔴 two blocks with the same entries: the finding is in the block that wrote the database bibtex opened", async () => {
231
+ const entry = "@article{lost, title = {x}}";
232
+ const block = (name: string) =>
233
+ `\\begin{filecontents*}{${name}}\n${entry}\n\\end{filecontents*}\n`;
234
+ const { messages } = await lint({
235
+ tex: `\\documentclass{acmart}\n${block("a.bib")}${block("b.bib")}\\begin{document}\nx\n\\bibliography{b}\n\\end{document}\n`,
236
+ files: { "a.bib": `${entry}\n`, "b.bib": `${entry}\n` },
237
+ record: {
238
+ inputs: [["paper.tex", "body"]],
239
+ written: ["a.bib", "b.bib"],
240
+ databases: ["b.bib"],
241
+ },
242
+ });
243
+ expect(found(messages, "reachable-entry")).toEqual([
244
+ [6, 1, expect.stringMatching(/`lost`/)],
245
+ ]);
246
+ });
247
+ });
248
+
249
+ describe("bib/reachable-entry — which bibliography: the one bibtex opened", () => {
250
+ it("only the databases the build's bibtex opened are judged, not every .bib that lies in the folder", async () => {
251
+ const { messages } = await lint({
252
+ ...declaring("@misc{r1, title = {x}}\n"),
253
+ files: {
254
+ "refs.bib": "@misc{r1, title = {x}}\n",
255
+ "anon.bib": "@misc{a1, title = {x}}\n",
256
+ },
257
+ });
258
+ expect(
259
+ found(messages, "reachable-entry").map((m) => m[2].slice(0, 16)),
260
+ ).toEqual(["refs.bib:1:1: `r"]);
261
+ });
262
+
263
+ it("a paper with no record is not judged: paper/sources-fresh says it was not built", async () => {
264
+ const p = declaring("@misc{lost, title = {x}}\n");
265
+ expect(
266
+ found((await lint({ ...p, record: null })).messages, "reachable-entry"),
267
+ ).toEqual([]);
268
+ });
269
+
270
+ it("🔴 a record the paper has changed since is not judged either — the entries it names may be gone", async () => {
271
+ const p = declaring("@misc{lost, title = {x}}\n");
272
+ const record = recordText({ "paper.tex": p.tex, ...p.files }, KEPT);
273
+ expect(
274
+ found(
275
+ (
276
+ await lint({
277
+ ...p,
278
+ files: {
279
+ "refs.bib": "@misc{lost, title = {x}, note = {edited}}\n",
280
+ },
281
+ record,
282
+ })
283
+ ).messages,
284
+ "reachable-entry",
285
+ ),
286
+ ).toEqual([]);
287
+ });
288
+
289
+ it("a build whose bibtex ran none, and a database that is not on disk now, hold no entries to judge", async () => {
290
+ const none = declaring("@misc{lost, title = {x}}\n");
291
+ expect(
292
+ found(
293
+ (await lint({ ...none, record: { inputs: [["paper.tex", "body"]] } }))
294
+ .messages,
295
+ "reachable-entry",
296
+ ),
297
+ ).toEqual([]);
298
+ const gone = await lint({
299
+ tex: DECLARING,
300
+ files: {},
301
+ record: {
302
+ inputs: [["paper.tex", "body"]],
303
+ databases: ["refs.bib"],
304
+ },
305
+ });
306
+ expect(found(gone.messages, "reachable-entry")).toEqual([]);
307
+ });
308
+ });
309
+
310
+ describe("bib/reachable-entry — which file", () => {
311
+ it("only paper.tex is judged", async () => {
312
+ const { messages } = await lint(inline("@article{lost, title = {x}}"), {
313
+ file: "sections/a.tex",
314
+ });
315
+ expect(messages).toEqual([]);
316
+ });
317
+ });
318
+
319
+ // ── the planted papers ─────────────────────────────────────────────────────────────────────
320
+
321
+ describe("on the planted papers (fixtures/paper-sources, the record TeX wrote in tex-truth.json)", () => {
322
+ // The entries of the databases bibtex opened that carry no doi, url or arXiv id. A `.bib` TeX
323
+ // wrote is on disk after a build (`written`), holding the lines of its block. An entry only
324
+ // bibtex reads (v5: behind `%`) is the post-build check's, not this rule's.
325
+ it.each([
326
+ ["v1-stale", []],
327
+ ["v5-percent-entry", []],
328
+ ["v6-unclosed", ["a3"]],
329
+ ["v8-jobname", ["jkey"]],
330
+ ["v16-included-block", ["stale"]],
331
+ ["v24-quoted-fields", ["q2", "q3", "q4", "q5"]],
332
+ ["v25-string-differs", ["k1"]],
333
+ ])("%s: %j", async (name, keys) => {
334
+ const p = planted(name);
335
+ const { messages } = await lint({
336
+ tex: p.files["paper.tex"] ?? "",
337
+ files: leftByTeX(p),
338
+ record: p.record,
339
+ });
340
+ expect(
341
+ messages.map((m) => [m.ruleId, /`([^`]+)`/.exec(m.message)?.[1]]),
342
+ ).toEqual(keys.map((k) => ["bib/reachable-entry", k]));
343
+ });
344
+
345
+ it("v8-jobname: the entry of the paper.bib TeX wrote is at its line in the block that wrote it", async () => {
346
+ const p = planted("v8-jobname");
347
+ const { messages } = await lint({
348
+ tex: p.files["paper.tex"] ?? "",
349
+ files: leftByTeX(p),
350
+ record: p.record,
351
+ });
352
+ expect(found(messages, "reachable-entry")).toEqual([
353
+ [3, 1, expect.stringMatching(/^`jkey` has no doi/)],
354
+ ]);
355
+ });
356
+ });
@@ -0,0 +1,228 @@
1
+ /**
2
+ * The `bib` rules — about the bibliography TeX read, as the last build recorded it
3
+ * (`_build/sources.json`, src/recorded-bibliography.ts), on `paper.tex`:
4
+ *
5
+ * bib/reachable-entry an entry with no doi, url or arXiv id
6
+ *
7
+ * They judge the entries of the databases bibtex OPENED, and say nothing for a paper with no record or
8
+ * one that changed since: `paper/sources-fresh` speaks once for it.
9
+ *
10
+ * WHERE A FINDING ABOUT AN ENTRY GOES (`entryReports`). An entry of a `.bib` TeX wrote from a block of
11
+ * `paper.tex` is reported on its own line there, and a disable directive above it works as everywhere.
12
+ * Any other entry — a `.bib` the author keeps, which ESLint does not lint, or a block that is in an
13
+ * included file — is reported at the top of `paper.tex`, with the file, line and column at the front of
14
+ * the message (the way `reportInPaper` reports text from an included file); there, the same directive
15
+ * written in the `.bib` on the line above the entry keeps the exception where the entry is
16
+ * (`directiveAbove`).
17
+ *
18
+ * The main file's blocks are read from the editor's buffer, so a finding in `paper.tex` points at the
19
+ * text being edited; the `.bib` files and the includes are read from disk.
20
+ */
21
+ import { basename, dirname } from "node:path";
22
+ import { callerPath } from "./caller-path.ts";
23
+ import { MAIN_FILE, type BibEntry } from "./domain/paper-sources.ts";
24
+ import {
25
+ entriesOfDatabases,
26
+ entryReports,
27
+ recordedBibliography,
28
+ type EntryReport,
29
+ type RecordedDeps,
30
+ type RecordedEntry,
31
+ } from "./recorded-bibliography.ts";
32
+
33
+ export type { EntryReport } from "./recorded-bibliography.ts";
34
+
35
+ /** `% eslint-disable-next-line <rule>` (a reason after ` -- ` or not) on the line above `at` in `text`. */
36
+ function directiveAbove(text: string, at: number, rule: string): boolean {
37
+ const lineStart = text.lastIndexOf("\n", at - 1) + 1;
38
+ const above = text.slice(
39
+ text.lastIndexOf("\n", lineStart - 2) + 1,
40
+ Math.max(lineStart - 1, 0),
41
+ );
42
+ const m = /^\s*%\s*eslint-disable-next-line\s+(.*?)(?:\s+--\s.*)?$/.exec(
43
+ above,
44
+ );
45
+ return (m?.[1] ?? "")
46
+ .split(",")
47
+ .map((r) => r.trim())
48
+ .includes(rule);
49
+ }
50
+
51
+ /** Whether a rule should judge this file: the paper's main file only. */
52
+ export const isMain = (filename: string): boolean =>
53
+ basename(filename) === MAIN_FILE;
54
+
55
+ // ── the rules ───────────────────────────────────────────────────────────────────────────
56
+
57
+ interface Loc {
58
+ readonly line: number;
59
+ readonly column: number;
60
+ }
61
+
62
+ /** The slice of ESLint's rule API the `bib` rules use. */
63
+ export interface BibRuleContext {
64
+ readonly filename: string;
65
+ /** The `tex/latex` language's source code: `raw` is the file as written. */
66
+ readonly sourceCode: {
67
+ readonly raw: string;
68
+ getLocFromIndex(index: number): Loc;
69
+ };
70
+ report(d: {
71
+ readonly loc: { readonly start: Loc; readonly end: Loc };
72
+ readonly messageId: string;
73
+ readonly data?: Readonly<Record<string, string>>;
74
+ }): void;
75
+ }
76
+
77
+ export interface BibRuleModule {
78
+ readonly meta: {
79
+ readonly type: "problem" | "suggestion";
80
+ readonly docs: { readonly description: string; readonly url: string };
81
+ readonly schema: readonly object[];
82
+ readonly messages: Readonly<Record<string, string>>;
83
+ };
84
+ create(context: BibRuleContext): { "root:exit"?: () => void };
85
+ }
86
+
87
+ type BibRuleName = "reachable-entry";
88
+
89
+ const docsUrl = (name: BibRuleName): string =>
90
+ `https://github.com/zernie/paperlint/blob/main/docs/rules/bib/${name}.md`;
91
+
92
+ /** One finding a `bib` rule makes about an entry. */
93
+ interface EntryFinding {
94
+ readonly at: EntryReport;
95
+ readonly messageId: string;
96
+ readonly key: string;
97
+ }
98
+
99
+ /**
100
+ * One finding about an entry, where `entryReports` puts it: on the entry, or at the top of paper.tex
101
+ * with the `…In` message that names the entry's file, line and column first.
102
+ */
103
+ function reportEntry(context: BibRuleContext, f: EntryFinding): void {
104
+ const loc = (i: number) => context.sourceCode.getLocFromIndex(i);
105
+ context.report({
106
+ loc: { start: loc(f.at.span.start), end: loc(f.at.span.end) },
107
+ ...(f.at.kind === "here"
108
+ ? { messageId: f.messageId, data: { key: f.key } }
109
+ : {
110
+ messageId: `${f.messageId}In`,
111
+ data: { key: f.key, where: f.at.where },
112
+ }),
113
+ });
114
+ }
115
+
116
+ /** What a rule judges: the rule's name, the entries it flags, and the message each flagged entry gets. */
117
+ interface Judge {
118
+ readonly rule: string;
119
+ readonly flags: (e: BibEntry) => boolean;
120
+ readonly messageId: string;
121
+ }
122
+
123
+ /** A rule that judges the entries of the databases bibtex opened. */
124
+ const entryRule = (
125
+ meta: BibRuleModule["meta"],
126
+ judge: Judge,
127
+ deps: RecordedDeps,
128
+ ): BibRuleModule => ({
129
+ meta,
130
+ create: (context) => ({
131
+ "root:exit": () => {
132
+ findings(context, judge, deps).forEach((f) => {
133
+ reportEntry(context, f);
134
+ });
135
+ },
136
+ }),
137
+ });
138
+
139
+ /** An entry reported outside paper.tex whose line above disables `rule` for it (in paper.tex ESLint does that). */
140
+ const excepted = (f: RecordedEntry, at: EntryReport, rule: string): boolean =>
141
+ at.kind === "elsewhere" &&
142
+ directiveAbove(f.database.bib.text, f.entry.span.start, rule);
143
+
144
+ /** The findings for the paper being linted: none for any file but its main one, none without a current record. */
145
+ function findings(
146
+ context: BibRuleContext,
147
+ judge: Judge,
148
+ deps: RecordedDeps,
149
+ ): readonly EntryFinding[] {
150
+ if (!isMain(context.filename)) return [];
151
+ const main = callerPath(context.filename);
152
+ const dir = dirname(main);
153
+ const r = recordedBibliography(dir, deps);
154
+ if (r.kind === "unrecorded") return [];
155
+ const flagged = entriesOfDatabases(r.databases).filter((f) =>
156
+ judge.flags(f.entry),
157
+ );
158
+ const at = entryReports(
159
+ {
160
+ dir,
161
+ record: r.record,
162
+ main: { path: main, text: context.sourceCode.raw },
163
+ },
164
+ flagged,
165
+ deps,
166
+ );
167
+ return flagged.flatMap((f, i): readonly EntryFinding[] => {
168
+ const place = at[i];
169
+ return place === undefined || excepted(f, place, judge.rule)
170
+ ? []
171
+ : [{ at: place, messageId: judge.messageId, key: f.entry.key || "?" }];
172
+ });
173
+ }
174
+
175
+ /** An arXiv identifier in a field's text: `arXiv:2401.00001`, `arXiv preprint arXiv:2401.00001`. */
176
+ const ARXIV_ID = /arxiv[:\s]*\d{4}\.\d{4,5}/i;
177
+ /** A link the reader parsed out of `\url{…}` or `\href{…}`, in any field (`howpublished`, `note`). */
178
+ const PARSED_LINK = '<a href="';
179
+
180
+ const has = (e: BibEntry, field: string): boolean =>
181
+ (e.fields[field] ?? "").trim() !== "";
182
+
183
+ /** A doi, a url, an arXiv eprint, or a link or arXiv id in any field: something a reader can follow. */
184
+ const reachable = (e: BibEntry): boolean =>
185
+ has(e, "doi") ||
186
+ has(e, "url") ||
187
+ (has(e, "eprint") &&
188
+ /arxiv/i.test(e.fields["archiveprefix"] ?? e.fields["eprinttype"] ?? "")) ||
189
+ Object.values(e.fields).some(
190
+ (v) => ARXIV_ID.test(v) || v.includes(PARSED_LINK),
191
+ );
192
+
193
+ const UNREACHABLE =
194
+ "`{{key}}` has no doi, url or arXiv id — a reader has nothing to follow. If none exists, keep the exception with `% eslint-disable-next-line bib/reachable-entry -- <why>` above the entry";
195
+ /** `bib/reachable-entry`: every entry TeX reads carries something a reader can follow. */
196
+ const REACHABLE_META: BibRuleModule["meta"] = {
197
+ type: "suggestion",
198
+ docs: {
199
+ description:
200
+ "a bibliography entry carries a doi, a url or an arXiv id — something a reader can follow",
201
+ url: docsUrl("reachable-entry"),
202
+ },
203
+ schema: [],
204
+ messages: {
205
+ unreachable: UNREACHABLE,
206
+ unreachableIn: `{{where}}: ${UNREACHABLE}`,
207
+ },
208
+ };
209
+
210
+ /** The `bib` plugin's rules, reading the paper's record through `deps`. They act on `paper.tex` only. */
211
+ export const bibRules = (
212
+ deps: RecordedDeps,
213
+ ): Readonly<Record<BibRuleName, BibRuleModule>> => ({
214
+ "reachable-entry": entryRule(
215
+ REACHABLE_META,
216
+ {
217
+ rule: "bib/reachable-entry",
218
+ flags: (e) => !reachable(e),
219
+ messageId: "unreachable",
220
+ },
221
+ deps,
222
+ ),
223
+ });
224
+
225
+ /** The level each is on at for every `paper.tex` in paperlint's own config. */
226
+ export const BIB_RULE_LEVELS = {
227
+ "bib/reachable-entry": "warn",
228
+ } as const;