paperlint 7.2.0 → 9.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (474) hide show
  1. package/CLAUDE.md +9 -25
  2. package/CONTRIBUTING.md +5 -4
  3. package/README.md +4 -3
  4. package/dist/adapters/bibtex/index.d.ts +3 -0
  5. package/dist/adapters/bibtex/index.d.ts.map +1 -0
  6. package/dist/adapters/bibtex/index.js +103 -0
  7. package/dist/adapters/bibtex/index.js.map +1 -0
  8. package/dist/adapters/latex/filecontents.d.ts +33 -0
  9. package/dist/adapters/latex/filecontents.d.ts.map +1 -0
  10. package/dist/adapters/latex/filecontents.js +65 -0
  11. package/dist/adapters/latex/filecontents.js.map +1 -0
  12. package/dist/adapters/latex/includes.d.ts +3 -3
  13. package/dist/adapters/latex/includes.d.ts.map +1 -1
  14. package/dist/adapters/latex/includes.js +18 -10
  15. package/dist/adapters/latex/includes.js.map +1 -1
  16. package/dist/adapters/latex/index.d.ts.map +1 -1
  17. package/dist/adapters/latex/index.js +2 -0
  18. package/dist/adapters/latex/index.js.map +1 -1
  19. package/dist/adapters/latex/nodes.d.ts +12 -0
  20. package/dist/adapters/latex/nodes.d.ts.map +1 -1
  21. package/dist/adapters/latex/nodes.js +39 -0
  22. package/dist/adapters/latex/nodes.js.map +1 -1
  23. package/dist/adapters/latex/parse.d.ts +7 -1
  24. package/dist/adapters/latex/parse.d.ts.map +1 -1
  25. package/dist/adapters/latex/parse.js +55 -6
  26. package/dist/adapters/latex/parse.js.map +1 -1
  27. package/dist/adapters/memory/index.d.ts +3 -0
  28. package/dist/adapters/memory/index.d.ts.map +1 -1
  29. package/dist/adapters/memory/index.js +9 -0
  30. package/dist/adapters/memory/index.js.map +1 -1
  31. package/dist/adapters/references/index.d.ts.map +1 -1
  32. package/dist/adapters/references/index.js +27 -24
  33. package/dist/adapters/references/index.js.map +1 -1
  34. package/dist/adapters/sources-record/index.d.ts +9 -0
  35. package/dist/adapters/sources-record/index.d.ts.map +1 -0
  36. package/dist/adapters/sources-record/index.js +68 -0
  37. package/dist/adapters/sources-record/index.js.map +1 -0
  38. package/dist/adapters/tex-output/blg.d.ts +9 -0
  39. package/dist/adapters/tex-output/blg.d.ts.map +1 -0
  40. package/dist/adapters/tex-output/blg.js +20 -0
  41. package/dist/adapters/tex-output/blg.js.map +1 -0
  42. package/dist/adapters/tex-output/fls.d.ts +8 -0
  43. package/dist/adapters/tex-output/fls.d.ts.map +1 -0
  44. package/dist/adapters/tex-output/fls.js +16 -0
  45. package/dist/adapters/tex-output/fls.js.map +1 -0
  46. package/dist/adapters/tex-output/index.d.ts +7 -0
  47. package/dist/adapters/tex-output/index.d.ts.map +1 -0
  48. package/dist/adapters/tex-output/index.js +7 -0
  49. package/dist/adapters/tex-output/index.js.map +1 -0
  50. package/dist/bib-rules.d.ts +47 -0
  51. package/dist/bib-rules.d.ts.map +1 -0
  52. package/dist/bib-rules.js +127 -0
  53. package/dist/bib-rules.js.map +1 -0
  54. package/dist/build.d.ts +29 -1
  55. package/dist/build.d.ts.map +1 -1
  56. package/dist/build.js +41 -30
  57. package/dist/build.js.map +1 -1
  58. package/dist/claim-provenance.d.ts +2 -2
  59. package/dist/cli.d.ts +9 -5
  60. package/dist/cli.d.ts.map +1 -1
  61. package/dist/cli.js +43 -68
  62. package/dist/cli.js.map +1 -1
  63. package/dist/domain/paper-source.d.ts +1 -10
  64. package/dist/domain/paper-source.d.ts.map +1 -1
  65. package/dist/domain/paper-source.js +34 -24
  66. package/dist/domain/paper-source.js.map +1 -1
  67. package/dist/domain/paper-sources.d.ts +72 -0
  68. package/dist/domain/paper-sources.d.ts.map +1 -0
  69. package/dist/domain/paper-sources.js +18 -0
  70. package/dist/domain/paper-sources.js.map +1 -0
  71. package/dist/domain/sources-record.d.ts +102 -0
  72. package/dist/domain/sources-record.d.ts.map +1 -0
  73. package/dist/domain/sources-record.js +38 -0
  74. package/dist/domain/sources-record.js.map +1 -0
  75. package/dist/domain/tex-run.d.ts +25 -0
  76. package/dist/domain/tex-run.d.ts.map +1 -0
  77. package/dist/domain/tex-run.js +79 -0
  78. package/dist/domain/tex-run.js.map +1 -0
  79. package/dist/eslint-rules/latex-language.d.ts.map +1 -1
  80. package/dist/eslint-rules/latex-language.js +10 -13
  81. package/dist/eslint-rules/latex-language.js.map +1 -1
  82. package/dist/eslint-rules/paper-context.d.ts.map +1 -1
  83. package/dist/eslint-rules/paper-context.js +6 -5
  84. package/dist/eslint-rules/paper-context.js.map +1 -1
  85. package/dist/eslint-rules/paper-typography.d.ts +0 -9
  86. package/dist/eslint-rules/paper-typography.d.ts.map +1 -1
  87. package/dist/eslint-rules/paper-typography.js +34 -96
  88. package/dist/eslint-rules/paper-typography.js.map +1 -1
  89. package/dist/eslint-rules/papers.d.ts +1 -3
  90. package/dist/eslint-rules/papers.d.ts.map +1 -1
  91. package/dist/eslint-rules/papers.js +2 -4
  92. package/dist/eslint-rules/papers.js.map +1 -1
  93. package/dist/eslint-rules/tex-build.d.ts.map +1 -1
  94. package/dist/eslint-rules/tex-build.js +8 -25
  95. package/dist/eslint-rules/tex-build.js.map +1 -1
  96. package/dist/init.d.ts +1 -4
  97. package/dist/init.d.ts.map +1 -1
  98. package/dist/init.js +3 -9
  99. package/dist/init.js.map +1 -1
  100. package/dist/latex-log.d.ts +10 -0
  101. package/dist/latex-log.d.ts.map +1 -1
  102. package/dist/latex-log.js +10 -0
  103. package/dist/latex-log.js.map +1 -1
  104. package/dist/latex-loop.d.ts +2 -0
  105. package/dist/latex-loop.d.ts.map +1 -1
  106. package/dist/latex-loop.js.map +1 -1
  107. package/dist/lib/markdown.d.ts +5 -81
  108. package/dist/lib/markdown.d.ts.map +1 -1
  109. package/dist/lib/markdown.js +5 -116
  110. package/dist/lib/markdown.js.map +1 -1
  111. package/dist/lib/paper-config.d.ts.map +1 -1
  112. package/dist/lib/paper-config.js +0 -2
  113. package/dist/lib/paper-config.js.map +1 -1
  114. package/dist/lib/skill-eval-fixture.d.ts +6 -4
  115. package/dist/lib/skill-eval-fixture.d.ts.map +1 -1
  116. package/dist/lib/skill-eval-fixture.js +25 -18
  117. package/dist/lib/skill-eval-fixture.js.map +1 -1
  118. package/dist/new-paper.d.ts +5 -11
  119. package/dist/new-paper.d.ts.map +1 -1
  120. package/dist/new-paper.js +12 -23
  121. package/dist/new-paper.js.map +1 -1
  122. package/dist/package-dirs.d.ts.map +1 -1
  123. package/dist/package-dirs.js +3 -2
  124. package/dist/package-dirs.js.map +1 -1
  125. package/dist/paper-files.d.ts +4 -4
  126. package/dist/paper-includes.d.ts +4 -7
  127. package/dist/paper-includes.d.ts.map +1 -1
  128. package/dist/paper-includes.js +29 -21
  129. package/dist/paper-includes.js.map +1 -1
  130. package/dist/paper-prose.d.ts +17 -0
  131. package/dist/paper-prose.d.ts.map +1 -0
  132. package/dist/paper-prose.js +46 -0
  133. package/dist/paper-prose.js.map +1 -0
  134. package/dist/paper-record.d.ts +25 -0
  135. package/dist/paper-record.d.ts.map +1 -0
  136. package/dist/paper-record.js +39 -0
  137. package/dist/paper-record.js.map +1 -0
  138. package/dist/paper-sources.d.ts +66 -0
  139. package/dist/paper-sources.d.ts.map +1 -0
  140. package/dist/paper-sources.js +92 -0
  141. package/dist/paper-sources.js.map +1 -0
  142. package/dist/ports/bib-reader.d.ts +18 -0
  143. package/dist/ports/bib-reader.d.ts.map +1 -0
  144. package/dist/ports/bib-reader.js +2 -0
  145. package/dist/ports/bib-reader.js.map +1 -0
  146. package/dist/ports/check-references.d.ts +9 -4
  147. package/dist/ports/check-references.d.ts.map +1 -1
  148. package/dist/ports/latex.d.ts +15 -0
  149. package/dist/ports/latex.d.ts.map +1 -1
  150. package/dist/ports/sources-codec.d.ts +11 -0
  151. package/dist/ports/sources-codec.d.ts.map +1 -0
  152. package/dist/ports/sources-codec.js +2 -0
  153. package/dist/ports/sources-codec.js.map +1 -0
  154. package/dist/ports/tex-output.d.ts +17 -0
  155. package/dist/ports/tex-output.d.ts.map +1 -0
  156. package/dist/ports/tex-output.js +2 -0
  157. package/dist/ports/tex-output.js.map +1 -0
  158. package/dist/recorded-bibliography.d.ts +89 -0
  159. package/dist/recorded-bibliography.d.ts.map +1 -0
  160. package/dist/recorded-bibliography.js +136 -0
  161. package/dist/recorded-bibliography.js.map +1 -0
  162. package/dist/reference-rules.d.ts +5 -5
  163. package/dist/reference-rules.d.ts.map +1 -1
  164. package/dist/reference-rules.js +150 -77
  165. package/dist/reference-rules.js.map +1 -1
  166. package/dist/references.d.ts +71 -16
  167. package/dist/references.d.ts.map +1 -1
  168. package/dist/references.js +110 -26
  169. package/dist/references.js.map +1 -1
  170. package/dist/register-bands.d.ts +3 -3
  171. package/dist/register-bands.d.ts.map +1 -1
  172. package/dist/register-bands.js.map +1 -1
  173. package/dist/register.d.ts +2 -2
  174. package/dist/sources-record.d.ts +38 -0
  175. package/dist/sources-record.d.ts.map +1 -0
  176. package/dist/sources-record.js +123 -0
  177. package/dist/sources-record.js.map +1 -0
  178. package/dist/sources-rules.d.ts +44 -0
  179. package/dist/sources-rules.d.ts.map +1 -0
  180. package/dist/sources-rules.js +76 -0
  181. package/dist/sources-rules.js.map +1 -0
  182. package/dist/structure.d.ts.map +1 -1
  183. package/dist/structure.js +4 -3
  184. package/dist/structure.js.map +1 -1
  185. package/dist/tex-paper.d.ts +10 -15
  186. package/dist/tex-paper.d.ts.map +1 -1
  187. package/dist/tex-paper.js +24 -40
  188. package/dist/tex-paper.js.map +1 -1
  189. package/dist/tex-venue-rules.d.ts +3 -3
  190. package/dist/tex-venue-rules.d.ts.map +1 -1
  191. package/dist/tex-venue-rules.js.map +1 -1
  192. package/dist/types.d.ts +0 -2
  193. package/dist/types.d.ts.map +1 -1
  194. package/docs/configuration.md +18 -12
  195. package/docs/design/paper-sources.md +197 -0
  196. package/docs/e2e.md +4 -6
  197. package/docs/package-shape-options.md +1 -1
  198. package/docs/prior-art/paper-folder-scaffolding.md +24 -29
  199. package/docs/references.md +34 -8
  200. package/docs/rules/bib/reachable-entry.md +64 -0
  201. package/docs/rules/paper/sources-fresh.md +81 -0
  202. package/docs/rules/tex/claim-provenance.md +0 -1
  203. package/docs/rules.md +62 -61
  204. package/eslint-rules/latex-language.harness.mjs +3 -3
  205. package/eslint-rules/latex-language.ts +10 -13
  206. package/eslint-rules/paper-context.ts +6 -5
  207. package/eslint-rules/paper-typography.test.mjs +29 -157
  208. package/eslint-rules/paper-typography.ts +35 -104
  209. package/eslint-rules/papers.harness.mjs +1 -1
  210. package/eslint-rules/papers.ts +2 -4
  211. package/eslint-rules/tex-build.harness.mjs +2 -2
  212. package/eslint-rules/tex-build.ts +8 -25
  213. package/eslint.config.mjs +22 -1
  214. package/fixtures/build-e2e/aidc/paper.tex +1 -1
  215. package/fixtures/build-e2e/anon-ack/paper.tex +1 -1
  216. package/fixtures/build-e2e/anon-author/paper.tex +1 -1
  217. package/fixtures/build-e2e/anon-bib/paper.tex +1 -1
  218. package/fixtures/build-e2e/anon-href/paper.tex +1 -1
  219. package/fixtures/build-e2e/anon-macro/paper.tex +1 -1
  220. package/fixtures/build-e2e/anon-pdfauthor/paper.tex +1 -1
  221. package/fixtures/paper-sources/p1/paper.tex +30 -0
  222. package/fixtures/paper-sources/p1/refs.bib +6 -0
  223. package/fixtures/paper-sources/p1/sections/dead.tex +6 -0
  224. package/fixtures/paper-sources/p1/sections/intro.tex +7 -0
  225. package/fixtures/paper-sources/p1/sections/parked.tex +6 -0
  226. package/fixtures/paper-sources/p1/tex-truth.json +37 -0
  227. package/fixtures/paper-sources/p1/versions/2026-01-01-review.tex +4 -0
  228. package/fixtures/paper-sources/v1-stale/paper.tex +14 -0
  229. package/fixtures/paper-sources/v1-stale/refs.bib +6 -0
  230. package/fixtures/paper-sources/v1-stale/tex-truth.json +32 -0
  231. package/fixtures/paper-sources/v10-ifthenelse/anon.bib +1 -0
  232. package/fixtures/paper-sources/v10-ifthenelse/paper.tex +9 -0
  233. package/fixtures/paper-sources/v10-ifthenelse/real.bib +1 -0
  234. package/fixtures/paper-sources/v10-ifthenelse/tex-truth.json +28 -0
  235. package/fixtures/paper-sources/v11-two-blocks/paper.tex +12 -0
  236. package/fixtures/paper-sources/v11-two-blocks/tex-truth.json +30 -0
  237. package/fixtures/paper-sources/v13-percent-text/paper.tex +6 -0
  238. package/fixtures/paper-sources/v13-percent-text/refs.bib +3 -0
  239. package/fixtures/paper-sources/v13-percent-text/tex-truth.json +30 -0
  240. package/fixtures/paper-sources/v15-after-end/paper.tex +6 -0
  241. package/fixtures/paper-sources/v15-after-end/refs.bib +1 -0
  242. package/fixtures/paper-sources/v15-after-end/tex-truth.json +17 -0
  243. package/fixtures/paper-sources/v16-included-block/bibblock.tex +3 -0
  244. package/fixtures/paper-sources/v16-included-block/paper.tex +7 -0
  245. package/fixtures/paper-sources/v16-included-block/refs.bib +1 -0
  246. package/fixtures/paper-sources/v16-included-block/tex-truth.json +35 -0
  247. package/fixtures/paper-sources/v17-include-order/bibsetup.tex +1 -0
  248. package/fixtures/paper-sources/v17-include-order/first.bib +1 -0
  249. package/fixtures/paper-sources/v17-include-order/later.bib +1 -0
  250. package/fixtures/paper-sources/v17-include-order/paper.tex +7 -0
  251. package/fixtures/paper-sources/v17-include-order/tex-truth.json +40 -0
  252. package/fixtures/paper-sources/v2-overwrite/paper.tex +14 -0
  253. package/fixtures/paper-sources/v2-overwrite/refs.bib +6 -0
  254. package/fixtures/paper-sources/v2-overwrite/tex-truth.json +30 -0
  255. package/fixtures/paper-sources/v22-parked-thebibliography/paper.tex +7 -0
  256. package/fixtures/paper-sources/v22-parked-thebibliography/tex-truth.json +17 -0
  257. package/fixtures/paper-sources/v23-parked-include/old.bib +1 -0
  258. package/fixtures/paper-sources/v23-parked-include/paper.tex +7 -0
  259. package/fixtures/paper-sources/v23-parked-include/parked.tex +1 -0
  260. package/fixtures/paper-sources/v23-parked-include/refs.bib +1 -0
  261. package/fixtures/paper-sources/v23-parked-include/tex-truth.json +28 -0
  262. package/fixtures/paper-sources/v24-quoted-fields/paper.tex +6 -0
  263. package/fixtures/paper-sources/v24-quoted-fields/refs.bib +5 -0
  264. package/fixtures/paper-sources/v24-quoted-fields/tex-truth.json +32 -0
  265. package/fixtures/paper-sources/v25-string-differs/paper.tex +10 -0
  266. package/fixtures/paper-sources/v25-string-differs/refs.bib +2 -0
  267. package/fixtures/paper-sources/v25-string-differs/tex-truth.json +32 -0
  268. package/fixtures/paper-sources/v26-comment-in-names/one.bib +1 -0
  269. package/fixtures/paper-sources/v26-comment-in-names/paper.tex +7 -0
  270. package/fixtures/paper-sources/v26-comment-in-names/tex-truth.json +32 -0
  271. package/fixtures/paper-sources/v26-comment-in-names/two.bib +1 -0
  272. package/fixtures/paper-sources/v27-shared-string/abbreviations.bib +2 -0
  273. package/fixtures/paper-sources/v27-shared-string/paper.tex +6 -0
  274. package/fixtures/paper-sources/v27-shared-string/references.bib +6 -0
  275. package/fixtures/paper-sources/v27-shared-string/tex-truth.json +31 -0
  276. package/fixtures/paper-sources/v28-shared-string-block/abbreviations.bib +2 -0
  277. package/fixtures/paper-sources/v28-shared-string-block/paper.tex +14 -0
  278. package/fixtures/paper-sources/v28-shared-string-block/tex-truth.json +33 -0
  279. package/fixtures/paper-sources/v29-generated-input/paper.tex +9 -0
  280. package/fixtures/paper-sources/v29-generated-input/tex-truth.json +24 -0
  281. package/fixtures/paper-sources/v3-declared/paper.bib +6 -0
  282. package/fixtures/paper-sources/v3-declared/paper.tex +6 -0
  283. package/fixtures/paper-sources/v3-declared/tex-truth.json +28 -0
  284. package/fixtures/paper-sources/v30-name-next-line/paper.tex +14 -0
  285. package/fixtures/paper-sources/v30-name-next-line/tex-truth.json +30 -0
  286. package/fixtures/paper-sources/v4-commented/paper.tex +14 -0
  287. package/fixtures/paper-sources/v4-commented/refs.bib +6 -0
  288. package/fixtures/paper-sources/v4-commented/tex-truth.json +28 -0
  289. package/fixtures/paper-sources/v5-percent-entry/paper.tex +20 -0
  290. package/fixtures/paper-sources/v5-percent-entry/tex-truth.json +37 -0
  291. package/fixtures/paper-sources/v6-unclosed/paper.tex +6 -0
  292. package/fixtures/paper-sources/v6-unclosed/refs.bib +5 -0
  293. package/fixtures/paper-sources/v6-unclosed/tex-truth.json +37 -0
  294. package/fixtures/paper-sources/v8-jobname/paper.tex +9 -0
  295. package/fixtures/paper-sources/v8-jobname/tex-truth.json +30 -0
  296. package/fixtures/paper-sources/v9-two-bibliographies/one.bib +1 -0
  297. package/fixtures/paper-sources/v9-two-bibliographies/paper.tex +7 -0
  298. package/fixtures/paper-sources/v9-two-bibliographies/tex-truth.json +35 -0
  299. package/fixtures/paper-sources/v9-two-bibliographies/two.bib +1 -0
  300. package/fixtures/paper-typography/clean-paper/paper.tex +1 -1
  301. package/fixtures/paper-typography/messy-paper/paper.tex +1 -1
  302. package/hooks/hooks.harness.ts +15 -15
  303. package/hooks/paper-edit-guard.hook.mjs +5 -8
  304. package/hooks/paper-edit-guard.test.ts +12 -3
  305. package/hooks/paper-skills-nudge.hook.mjs +2 -3
  306. package/hooks/paper-skills-nudge.test.ts +13 -3
  307. package/hooks/paper-status-gates.hook.mjs +2 -2
  308. package/hooks/paper-status-gates.test.ts +14 -2
  309. package/lib/markdown.test.mjs +5 -60
  310. package/lib/markdown.ts +9 -136
  311. package/lib/paper-config.ts +0 -2
  312. package/lib/skill-checks.test.mjs +4 -1
  313. package/lib/skill-eval-fixture.ts +25 -18
  314. package/package.json +2 -12
  315. package/presets/realm.jsonc +1 -1
  316. package/scripts/check.test.ts +23 -1
  317. package/scripts/check.ts +7 -3
  318. package/scripts/coverage-config.test.ts +19 -10
  319. package/scripts/harness-api.frozen.json +0 -5
  320. package/scripts/layer-legacy-frozen.test.ts +41 -2
  321. package/scripts/layer-legacy-frozen.ts +31 -2
  322. package/skills/cold-read-diff/SKILL.md +3 -3
  323. package/skills/cold-read-diff/SKILL.md.spec.ts +2 -2
  324. package/skills/grade-paper-writing/SKILL.md +15 -22
  325. package/skills/grade-paper-writing/SKILL.md.spec.ts +14 -21
  326. package/skills/grade-paper-writing/__snapshots__/prose-lint.test.ts.snap +40 -26
  327. package/skills/grade-paper-writing/prose-lint.mjs +117 -278
  328. package/skills/grade-paper-writing/prose-lint.test.ts +230 -79
  329. package/skills/paper-adversarial-review/SKILL.md +2 -2
  330. package/skills/paper-adversarial-review/SKILL.md.spec.ts +1 -1
  331. package/skills/paper-pipeline/SKILL.md +9 -12
  332. package/skills/paper-pipeline/SKILL.md.spec.ts +8 -11
  333. package/skills/paper-pipeline/description-language.eval.mjs +25 -15
  334. package/skills/paper-pipeline/framing-vs-vocabulary.eval.mjs +25 -15
  335. package/skills/paper-pipeline/grade-paper-writing-ablation.eval.mjs +25 -15
  336. package/skills/paper-pipeline/pipeline-firing.eval.mjs +27 -16
  337. package/skills/paper-pipeline/pipeline-language.eval.mjs +25 -15
  338. package/skills/paper-pipeline/references/acceptance-gate.md +16 -20
  339. package/skills/paper-pipeline/references/artifact-checklist.md +4 -5
  340. package/skills/paper-pipeline/references/pipeline-status-template.md +1 -1
  341. package/skills/paper-pipeline/scripts/README.md +0 -4
  342. package/skills/paper-pipeline/scripts/extract-ref-facts.harness.mjs +91 -105
  343. package/skills/paper-pipeline/scripts/extract-ref-facts.mjs +85 -241
  344. package/skills/paper-pipeline/scripts/extract-ref-facts.test.mjs +166 -153
  345. package/skills/paper-pipeline/scripts/generated-code.harness.mjs +3 -3
  346. package/skills/paper-status/SKILL.md +2 -2
  347. package/skills/paper-status/SKILL.md.spec.ts +1 -1
  348. package/skills/render-paper/SKILL.md +2 -2
  349. package/skills/render-paper/SKILL.md.spec.ts +1 -1
  350. package/skills/render-paper/extract-pdf-facts.mjs +2 -2
  351. package/skills/tighten-paper/SKILL.md +29 -42
  352. package/skills/tighten-paper/SKILL.md.spec.ts +28 -41
  353. package/skills/verify-citations/SKILL.md +7 -4
  354. package/skills/verify-citations/SKILL.md.spec.ts +6 -3
  355. package/skills/verify-citations/scripts/bib-authors.check.test.mjs +17 -32
  356. package/skills/verify-citations/scripts/bib-authors.cli.test.ts +138 -43
  357. package/skills/verify-citations/scripts/bib-authors.d.mts +22 -2
  358. package/skills/verify-citations/scripts/bib-authors.mjs +54 -193
  359. package/skills/verify-citations/scripts/bib-authors.test.mjs +1 -44
  360. package/skills/verify-citations/scripts/verify-cites.cli.test.ts +16 -0
  361. package/skills/verify-citations/scripts/verify-cites.d.mts +19 -3
  362. package/skills/verify-citations/scripts/verify-cites.mjs +42 -108
  363. package/skills/verify-citations/scripts/verify-cites.test.mjs +23 -36
  364. package/src/adapters/bibtex/index.test.ts +163 -0
  365. package/src/adapters/bibtex/index.ts +155 -0
  366. package/src/adapters/latex/filecontents.ts +114 -0
  367. package/src/adapters/latex/includes.ts +18 -10
  368. package/src/adapters/latex/index.test.ts +73 -0
  369. package/src/adapters/latex/index.ts +2 -0
  370. package/src/adapters/latex/nodes.ts +64 -0
  371. package/src/adapters/latex/parse.test.ts +38 -0
  372. package/src/adapters/latex/parse.ts +67 -6
  373. package/src/adapters/memory/index.ts +17 -0
  374. package/src/adapters/references/index.test.ts +75 -21
  375. package/src/adapters/references/index.ts +37 -32
  376. package/src/adapters/sources-record/index.test.ts +87 -0
  377. package/src/adapters/sources-record/index.ts +82 -0
  378. package/src/adapters/tex-output/blg.ts +27 -0
  379. package/src/adapters/tex-output/fls.ts +24 -0
  380. package/src/adapters/tex-output/index.test.ts +171 -0
  381. package/src/adapters/tex-output/index.ts +12 -0
  382. package/src/bib-rules.test.ts +356 -0
  383. package/src/bib-rules.ts +228 -0
  384. package/src/bibliography-at.test.ts +286 -0
  385. package/src/build.harness.mjs +2 -2
  386. package/src/build.test.ts +211 -1
  387. package/src/build.ts +101 -41
  388. package/src/claim-provenance.test.ts +33 -7
  389. package/src/claim-provenance.ts +2 -2
  390. package/src/cli.harness.mjs +46 -56
  391. package/src/cli.test.ts +44 -48
  392. package/src/cli.ts +51 -82
  393. package/src/domain/paper-source.test.ts +58 -20
  394. package/src/domain/paper-source.ts +38 -42
  395. package/src/domain/paper-sources.ts +94 -0
  396. package/src/domain/sources-record.test.ts +71 -0
  397. package/src/domain/sources-record.ts +136 -0
  398. package/src/domain/tex-run.test.ts +228 -0
  399. package/src/domain/tex-run.ts +139 -0
  400. package/src/folder-venue-rule.test.ts +1 -1
  401. package/src/heading-case.test.ts +24 -10
  402. package/src/init.test.ts +6 -7
  403. package/src/init.ts +3 -22
  404. package/src/latex-log.ts +20 -0
  405. package/src/latex-loop.harness.ts +1 -0
  406. package/src/latex-loop.ts +2 -0
  407. package/src/lint-fix.test.ts +10 -4
  408. package/src/linted-set.test.ts +9 -1
  409. package/src/new-paper.harness.mjs +12 -30
  410. package/src/new-paper.test.ts +15 -11
  411. package/src/new-paper.ts +11 -30
  412. package/src/package-dirs.ts +3 -2
  413. package/src/paper-files.test.ts +5 -4
  414. package/src/paper-files.ts +4 -4
  415. package/src/paper-includes.test.ts +157 -38
  416. package/src/paper-includes.ts +39 -35
  417. package/src/paper-prose.test.ts +78 -0
  418. package/src/paper-prose.ts +67 -0
  419. package/src/paper-record.test.ts +173 -0
  420. package/src/paper-record.ts +66 -0
  421. package/src/paper-settings-commands.test.ts +5 -2
  422. package/src/paper-sources.ts +151 -0
  423. package/src/parse-args.test.ts +0 -1
  424. package/src/ports/bib-reader.ts +27 -0
  425. package/src/ports/check-references.ts +7 -3
  426. package/src/ports/latex.ts +19 -0
  427. package/src/ports/sources-codec.ts +11 -0
  428. package/src/ports/tex-output.ts +17 -0
  429. package/src/recorded-bibliography.ts +297 -0
  430. package/src/reference-rules.ts +217 -104
  431. package/src/references.test.ts +520 -177
  432. package/src/references.ts +185 -37
  433. package/src/register-bands.test.ts +5 -1
  434. package/src/register-bands.ts +3 -5
  435. package/src/register.test.ts +6 -1
  436. package/src/register.ts +2 -2
  437. package/src/rule-cases.test.ts +68 -12
  438. package/src/rule-docs.test.ts +0 -1
  439. package/src/sources-record.test.ts +308 -0
  440. package/src/sources-record.ts +231 -0
  441. package/src/sources-rules.test.ts +152 -0
  442. package/src/sources-rules.ts +117 -0
  443. package/src/structure.harness.ts +5 -11
  444. package/src/structure.ts +4 -3
  445. package/src/tex-paper.test.ts +148 -78
  446. package/src/tex-paper.ts +31 -61
  447. package/src/tex-venue-rules.test.ts +36 -8
  448. package/src/tex-venue-rules.ts +3 -5
  449. package/src/types.ts +0 -2
  450. package/dist/eslint-rules/bib-reachable-entry.d.ts +0 -22
  451. package/dist/eslint-rules/bib-reachable-entry.d.ts.map +0 -1
  452. package/dist/eslint-rules/bib-reachable-entry.js +0 -75
  453. package/dist/eslint-rules/bib-reachable-entry.js.map +0 -1
  454. package/eslint-rules/bib-reachable-entry.ts +0 -76
  455. package/fixtures/real-markdown-paper/baseline.json +0 -22
  456. package/fixtures/real-markdown-paper/baseline.ts +0 -70
  457. package/skills/paper-pipeline/PIPELINE-MAP.md +0 -381
  458. package/skills/paper-pipeline/pipeline-firing.baseline.json +0 -270
  459. package/skills/paper-pipeline/references/sweep-2026-08-09-ideal-pipeline.md +0 -585
  460. package/skills/paper-pipeline/scripts/artifact-coverage.harness.mjs +0 -492
  461. package/skills/paper-pipeline/scripts/artifact-coverage.mjs +0 -397
  462. package/skills/paper-pipeline/scripts/artifact-coverage.test.ts +0 -86
  463. package/skills/paper-pipeline/scripts/check-provenance.mjs +0 -184
  464. package/skills/paper-pipeline/scripts/population-map.harness.mjs +0 -135
  465. package/skills/paper-pipeline/scripts/population-map.mjs +0 -188
  466. package/skills/paper-pipeline/scripts/population-map.test.mjs +0 -154
  467. package/skills/paper-pipeline/scripts/provenance.harness.mjs +0 -266
  468. package/skills/paper-pipeline/scripts/round-diff.harness.ts +0 -907
  469. package/skills/paper-pipeline/scripts/round-diff.mjs +0 -583
  470. package/skills/paper-pipeline/scripts/round-diff.test.mjs +0 -159
  471. package/skills/tighten-paper/__snapshots__/structure.test.ts.snap +0 -133
  472. package/skills/tighten-paper/structure.mjs +0 -364
  473. package/skills/tighten-paper/structure.test.ts +0 -117
  474. package/templates/paper/paper.md +0 -4
@@ -1,9 +1,9 @@
1
1
  /**
2
- * paper/section-word · paper/leading-zero · paper/figure-ref-style · bib/reachable-entry — each
3
- * FIRES on a bad input, stays SILENT on a good one, reports every occurrence at its own place,
4
- * and (the first three) fixes to an exact output. Plus the escape hatch: a disable directive in a
5
- * `%` comment, including inside the `filecontents` bibliography, silences exactly one finding,
6
- * and a directive that silences nothing is itself reported.
2
+ * paper/section-word · paper/leading-zero · paper/figure-ref-style — each FIRES on a bad input,
3
+ * stays SILENT on a good one, reports every occurrence at its own place, and fixes to an exact
4
+ * output. Plus the escape hatch: a disable directive in a `%` comment silences exactly one finding,
5
+ * and a directive that silences nothing is itself reported. (`bib/reachable-entry`, the fourth check
6
+ * of the old `paper/typography`, is tested beside it in src/bib-rules.test.ts.)
7
7
  *
8
8
  * The leading-zero cases are the ones paperlint#44 measured: a regex over the raw source found 22
9
9
  * decimals on a real corpus and none was a defect, so the SILENT list is as load-bearing as the
@@ -11,7 +11,6 @@
11
11
  */
12
12
  import { describe, expect, it } from "vitest";
13
13
  import { ESLint } from "eslint";
14
- import markdown from "@eslint/markdown";
15
14
  import { readFileSync } from "node:fs";
16
15
  import { dirname, join } from "node:path";
17
16
  import { fileURLToPath } from "node:url";
@@ -20,7 +19,6 @@ import typography, {
20
19
  crefDefinitionRanges,
21
20
  texVisibleRuns,
22
21
  } from "./paper-typography.ts";
23
- import bib from "./bib-reachable-entry.ts";
24
22
 
25
23
  const HERE = dirname(fileURLToPath(import.meta.url));
26
24
  const FIX = join(HERE, "..", "fixtures", "paper-typography");
@@ -29,7 +27,6 @@ const RULES = {
29
27
  "paper/section-word": "warn",
30
28
  "paper/leading-zero": "warn",
31
29
  "paper/figure-ref-style": "warn",
32
- "bib/reachable-entry": "warn",
33
30
  };
34
31
 
35
32
  const eslint = (fix = false) =>
@@ -43,22 +40,11 @@ const eslint = (fix = false) =>
43
40
  plugins: {
44
41
  tex: { languages: { latex: texLanguage } },
45
42
  paper: typography,
46
- bib,
47
43
  },
48
44
  language: "tex/latex",
49
45
  linterOptions: { reportUnusedDisableDirectives: "error" },
50
46
  rules: RULES,
51
47
  },
52
- {
53
- files: ["**/*.md"],
54
- plugins: { markdown, paper: typography },
55
- language: "markdown/gfm",
56
- languageOptions: { frontmatter: "yaml" },
57
- rules: {
58
- "paper/section-word": "warn",
59
- "paper/leading-zero": "warn",
60
- },
61
- },
62
48
  ],
63
49
  });
64
50
 
@@ -85,8 +71,6 @@ describe("the fixtures: messy fires on each rule, clean is silent", () => {
85
71
  expect(count("paper/section-word")).toBe(3);
86
72
  expect(count("paper/leading-zero")).toBe(2);
87
73
  expect(count("paper/figure-ref-style")).toBe(1);
88
- expect(count("bib/reachable-entry")).toBe(2);
89
- expect(ids(res, "bib/reachable-entry").map((m) => m.line)).toEqual([3, 8]);
90
74
  });
91
75
 
92
76
  it("clean-paper — nothing", async () => {
@@ -121,6 +105,13 @@ describe("paper/section-word", () => {
121
105
  expect(ids(await lint(src), "paper/section-word")).toEqual([]);
122
106
  });
123
107
 
108
+ it("🔴 silent on a bibliography whose file name stands on the line after \\begin{filecontents*}", async () => {
109
+ const src =
110
+ "\\documentclass{acmart}\n\\begin{filecontents*}\n{refs.bib}\n@misc{k, note = {see §5}, url = {https://x.org}}\n\\end{filecontents*}\n" +
111
+ "\\begin{document}\nSection~\\ref{a}.\n\\end{document}\n";
112
+ expect(ids(await lint(src), "paper/section-word")).toEqual([]);
113
+ });
114
+
124
115
  it("🔴 a cleveref name definition is not prose: silent, and --fix leaves it as written", async () => {
125
116
  const src =
126
117
  "\\documentclass{article}\n\\usepackage{cleveref}\n\\crefname{section}{§}{§§}\n\\Crefname{section}{§}{§§}\n" +
@@ -164,14 +155,6 @@ describe("paper/section-word", () => {
164
155
  "\\documentclass{article}\n\\crefname{section}{§}{§§}\n\\begin{document}\nSee §5.\n\\end{document}\n";
165
156
  expect(ids(await lint(src), "paper/section-word")).toHaveLength(1);
166
157
  });
167
-
168
- it("markdown: `§5` → `Section 5`, code untouched", async () => {
169
- const src = "See §5 and `§3` here.\n";
170
- expect(ids(await lint(src, "paper.md"), "paper/section-word")).toHaveLength(
171
- 1,
172
- );
173
- expect(await fixed(src, "paper.md")).toBe("See Section 5 and `§3` here.\n");
174
- });
175
158
  });
176
159
 
177
160
  describe("paper/leading-zero", () => {
@@ -237,17 +220,6 @@ describe("paper/leading-zero", () => {
237
220
  doc("At $p < 0.05$ and $d=0.21$; cell & 0.037 \\\\ and .5 or 0.25."),
238
221
  );
239
222
  });
240
-
241
- it("markdown: prose and a table cell are fixed, code is not", async () => {
242
- const src =
243
- "Significant at p<.05.\n\n| task | p |\n| --- | --- |\n| bugfix | .002 |\n\nRun `.25`.\n";
244
- expect(ids(await lint(src, "paper.md"), "paper/leading-zero")).toHaveLength(
245
- 2,
246
- );
247
- expect(await fixed(src, "paper.md")).toBe(
248
- "Significant at p<0.05.\n\n| task | p |\n| --- | --- |\n| bugfix | 0.002 |\n\nRun `.25`.\n",
249
- );
250
- });
251
223
  });
252
224
 
253
225
  describe("a finding after a heading written over several lines", () => {
@@ -290,15 +262,11 @@ describe("paper/leading-zero — a number after ¶ or § is not a decimal (#102)
290
262
  expect(await fixed(doc(CITE))).toBe(doc(CITE));
291
263
  });
292
264
 
293
- it("…while a real decimal still fixes, in markdown too, even beside a cited paragraph", async () => {
265
+ it("…while a real decimal still fixes, even beside a cited paragraph", async () => {
294
266
  const body = `${CITE}\n\nWe use a threshold of .05 throughout.`;
295
267
  expect(await fixed(doc(body))).toBe(
296
268
  doc(`${CITE}\n\nWe use a threshold of 0.05 throughout.`),
297
269
  );
298
- const md = "See ¶¶.42 and .44.\n\nSignificant at p<.05.\n";
299
- expect(await fixed(md, "paper.md")).toBe(
300
- "See ¶¶.42 and .44.\n\nSignificant at p<0.05.\n",
301
- );
302
270
  });
303
271
  });
304
272
 
@@ -330,69 +298,7 @@ describe("paper/figure-ref-style", () => {
330
298
  });
331
299
  });
332
300
 
333
- describe("bib/reachable-entry", () => {
334
- const bibDoc = (entries) =>
335
- `\\documentclass{acmart}\n\\begin{filecontents*}{refs.bib}\n${entries}\n\\end{filecontents*}\n\\begin{document}\nx\n\\end{document}\n`;
336
-
337
- it("a doi, a url or an arXiv id each make an entry reachable", async () => {
338
- const res = await lint(
339
- bibDoc(
340
- "@article{a, doi = {10.1/x}}\n@misc{b, url = {https://x.org}}\n@misc{c, note = {arXiv:2310.05736}}\n@string{v = {x}}",
341
- ),
342
- );
343
- expect(ids(res, "bib/reachable-entry")).toEqual([]);
344
- });
345
-
346
- it("reports on the entry itself, naming its key", async () => {
347
- const res = await lint(
348
- bibDoc("@misc{ok, url = {https://x.org}}\n@article{lost, title = {x}}"),
349
- );
350
- const [m] = ids(res, "bib/reachable-entry");
351
- expect(m.line).toBe(4);
352
- expect(m.column).toBe(1);
353
- expect(m.message).toMatch(/`lost`/);
354
- });
355
-
356
- it("an entry with no key is reported as `?`, not skipped", async () => {
357
- const [m] = ids(
358
- await lint(bibDoc("@article{\n title = {x}}")),
359
- "bib/reachable-entry",
360
- );
361
- expect(m.message).toMatch(/`\?`/);
362
- });
363
-
364
- it("a source code with only `text`, or with neither text nor raw", () => {
365
- const run = (sourceCode) => {
366
- const out = [];
367
- bib.rules["reachable-entry"]
368
- .create({
369
- sourceCode: {
370
- getLocFromIndex: (i) => ({ line: 1, column: i }),
371
- ...sourceCode,
372
- },
373
- report: (r) => out.push(r.data.key),
374
- })
375
- ["root:exit"]();
376
- return out;
377
- };
378
- expect(run({ text: bibDoc("@article{t, title = {x}}") })).toEqual(["t"]);
379
- expect(run({})).toEqual([]);
380
- });
381
- });
382
-
383
301
  describe("the escape hatch — a disable directive in a `%` comment", () => {
384
- it("🔴 inside the filecontents bibliography, it silences exactly the next entry", async () => {
385
- const res = await lint(
386
- `\\documentclass{acmart}\n\\begin{filecontents*}{refs.bib}\n` +
387
- "% eslint-disable-next-line bib/reachable-entry -- an invited talk, no recording exists\n" +
388
- "@misc{talk, title = {x}}\n@misc{lost, title = {y}}\n" +
389
- "\\end{filecontents*}\n\\begin{document}\nx\n\\end{document}\n",
390
- );
391
- const found = ids(res, "bib/reachable-entry");
392
- expect(found).toHaveLength(1);
393
- expect(found[0].message).toMatch(/`lost`/);
394
- });
395
-
396
302
  it("in the body: next-line, same-line, and a disable/enable region", async () => {
397
303
  const src = doc(
398
304
  [
@@ -467,48 +373,6 @@ describe("where visible text comes from — the walk's other paths", () => {
467
373
  });
468
374
  });
469
375
 
470
- describe("markdown: offsets that cannot be followed, and rules that are LaTeX-only", () => {
471
- const mdLint = async (text, rules) => {
472
- const [res] = await new ESLint({
473
- cwd: FIX,
474
- overrideConfigFile: true,
475
- overrideConfig: [
476
- {
477
- files: ["**/*.md"],
478
- plugins: { markdown, paper: typography },
479
- language: "markdown/gfm",
480
- rules,
481
- },
482
- ],
483
- }).lintText(text, { filePath: join(FIX, "inline", "paper.md") });
484
- expect(res.messages.filter((m) => m.fatal)).toEqual([]);
485
- return res;
486
- };
487
-
488
- it("`§` before prose is reported without a fix", async () => {
489
- const src = "As § above.\n";
490
- const res = await mdLint(src, { "paper/section-word": "warn" });
491
- expect(res.messages.map((m) => [m.ruleId, m.column, m.fix])).toEqual([
492
- ["paper/section-word", 4, undefined],
493
- ]);
494
- });
495
-
496
- it("a sign or a dot written as a character reference has no source offset: nothing is reported", async () => {
497
- const res = await mdLint("See &#167; 5 and &#46;25.\n", {
498
- "paper/section-word": "warn",
499
- "paper/leading-zero": "warn",
500
- });
501
- expect(res.messages).toEqual([]);
502
- });
503
-
504
- it("figure-ref-style is LaTeX-only: silent on markdown", async () => {
505
- const res = await mdLint("Figure~\\ref{a} and Fig.~\\ref{b}.\n", {
506
- "paper/figure-ref-style": "warn",
507
- });
508
- expect(res.messages).toEqual([]);
509
- });
510
- });
511
-
512
376
  describe("a source code the rules did not expect — the defaults hold", () => {
513
377
  const reports = (ruleId, sourceCode) => {
514
378
  const out = [];
@@ -530,12 +394,20 @@ describe("a source code the rules did not expect — the defaults hold", () => {
530
394
  ]);
531
395
  });
532
396
 
533
- it("markdown text nodes without positions: seen, never reported at a guessed place", () => {
534
- const ast = {
535
- type: "root",
536
- children: [{ type: "text", value: "§5 and .25" }],
537
- };
538
- expect(reports("section-word", { text: "§5 and .25", ast })).toEqual([]);
539
- expect(reports("leading-zero", { text: "§5 and .25", ast })).toEqual([]);
540
- });
397
+ it.each(["section-word", "leading-zero", "figure-ref-style"])(
398
+ "%s reads LaTeX only: a source without `raw` (any other language) is not reported",
399
+ (ruleId) => {
400
+ const ast = {
401
+ type: "root",
402
+ children: [
403
+ {
404
+ type: "text",
405
+ value: "§5 and .25",
406
+ position: { start: { offset: 0 }, end: { offset: 10 } },
407
+ },
408
+ ],
409
+ };
410
+ expect(reports(ruleId, { text: "§5 and .25", ast })).toEqual([]);
411
+ },
412
+ );
541
413
  });
@@ -7,8 +7,8 @@
7
7
  * paper/figure-ref-style `Fig.~\ref` beside `Figure~\ref` in one document → the majority form
8
8
  *
9
9
  * The fourth check of the old `paper/typography`, an unreachable bibliography entry, is
10
- * `bib/reachable-entry` (bib-reachable-entry.mjs): it is about the bibliography, not the prose,
11
- * and it has no fix.
10
+ * `bib/reachable-entry` (src/bib-rules.ts): it is about the bibliography, not the prose, and it has
11
+ * no fix.
12
12
  *
13
13
  * ── PROVENANCE: every one is a real review finding, not an invented style ───────────────
14
14
  * From HotCRP #20 (2026-08-23). Reviewer B listed two as literal to-dos:
@@ -41,11 +41,9 @@
41
41
  * IEEE Editorial Style Manual ("0.25, not .25") require the zero. A regex over the raw source
42
42
  * found 22 decimals on a real corpus and none was a defect: every one sat in markup (an option,
43
43
  * a column spec, a comment, a listing, a tikz coordinate). So this walks the TREE and keeps only
44
- * what the reader sees as a number:
45
- * LaTeX prose, inline and display math, table cells, and the text arguments of a known
46
- * set of text macros. Not: any other macro's arguments, environment arguments,
47
- * comments, verbatim/listings/`\lstinline`, tikz, the preamble, the bibliography.
48
- * Markdown text nodes, table cells included. Not: code, inline code, html, front matter.
44
+ * what the reader sees as a number: prose, inline and display math, table cells, and the text
45
+ * arguments of a known set of text macros. Not: any other macro's arguments, environment
46
+ * arguments, comments, verbatim/listings/`\lstinline`, tikz, the preamble, the bibliography.
49
47
  * Each character of a run keeps its source offset, so the finding points at the dot and the fix
50
48
  * inserts the zero there. The arXiv lookbehind stays: in `2310.05736` the dot follows a digit.
51
49
  * A text macro outside the known set (`\hl{.05}`) is read as a parameter and NOT reported — a
@@ -62,7 +60,6 @@
62
60
  import assert from "node:assert/strict";
63
61
  import { getParser } from "@unified-latex/unified-latex-util-parse";
64
62
  import { takeWhile } from "remeda";
65
- import type { Nodes } from "mdast";
66
63
  import {
67
64
  texToMdast,
68
65
  type TexArg,
@@ -78,7 +75,7 @@ import type {
78
75
  } from "./rule-context.ts";
79
76
 
80
77
  /** Where a `.bib` embedded in a `filecontents` environment sits in the file. */
81
- export interface BibRange {
78
+ interface BibRange {
82
79
  readonly start: number;
83
80
  readonly end: number;
84
81
  readonly bodyStart: number;
@@ -94,10 +91,17 @@ interface Run {
94
91
  offs: (number | null)[];
95
92
  }
96
93
 
97
- /** The inline bibliography — a `filecontents` block writing a `.bib` — with its offsets. */
98
- export function bibRange(text: string): BibRange | null {
94
+ /**
95
+ * The inline bibliography — a `filecontents` block writing a `.bib`, its name on the `\begin` line or
96
+ * after white space on a later one, as TeX takes it — with its offsets: the span the markup rules
97
+ * skip. Which bibliography TeX reads is the build's record (`_build/sources.json`,
98
+ * docs/design/paper-sources.md §1);
99
+ * this range only says which bytes are not prose, and moves onto the one projection of live text
100
+ * with the rest of `skippedRanges` (Q3, docs/design/paper-sources.md §10).
101
+ */
102
+ function bibRange(text: string): BibRange | null {
99
103
  const m =
100
- /\\begin\{filecontents\*?\}(?:\[[^\]]*\])?\{[^}]*\.bib\}\r?\n([\s\S]*?)\\end\{filecontents\*?\}/d.exec(
104
+ /\\begin\{filecontents\*?\}\s*(?:\[[^\]]*\]\s*)?\{[^}]*\.bib\}[^\n]*\n([\s\S]*?)\\end\{filecontents\*?\}/d.exec(
101
105
  text,
102
106
  );
103
107
  if (!m) return null;
@@ -108,7 +112,7 @@ export function bibRange(text: string): BibRange | null {
108
112
  return { start: m.index, end: m.index + whole.length, bodyStart, body };
109
113
  }
110
114
 
111
- /** A node of a file's projection, as far as its place goes: mdast's, or the `.tex` language's. */
115
+ /** A node of the `.tex` language's projection, as far as its place goes. */
112
116
  interface ProjectedNode {
113
117
  readonly type: string;
114
118
  readonly position?:
@@ -372,66 +376,6 @@ export function texVisibleRuns(
372
376
  return texRuns(document ? document.content : root, false, raw, []);
373
377
  }
374
378
 
375
- // Markdown node types that are not prose.
376
- const MD_HIDDEN = new Set([
377
- "code",
378
- "inlineCode",
379
- "html",
380
- "yaml",
381
- "toml",
382
- "math",
383
- "inlineMath",
384
- ]);
385
- /**
386
- * The source offset of each character of a text node's value. The value is the source with
387
- * markdown's escapes (`\*`) and character references (`&lt;`) resolved, so the two are walked
388
- * side by side; a character whose source cannot be followed gets null (reported, not fixed).
389
- */
390
- function mdOffsets(value: string, src: string, start: number) {
391
- const offs: (number | null)[] = [];
392
- let j = start;
393
- for (const ch of value) {
394
- if (src[j] === ch) offs.push(j++);
395
- else if (src[j] === "\\" && src[j + 1] === ch) {
396
- offs.push(j + 1);
397
- j += 2;
398
- } else if (src[j] === "&" && /^&#?\w{1,8};/.test(src.slice(j, j + 10))) {
399
- offs.push(null);
400
- j = src.indexOf(";", j) + 1;
401
- } else {
402
- offs.push(null);
403
- j++;
404
- }
405
- }
406
- return offs;
407
- }
408
-
409
- function mdVisibleRuns(
410
- node: Nodes | undefined,
411
- src: string,
412
- out: Run[] = [],
413
- ): Run[] {
414
- if (!node || MD_HIDDEN.has(node.type)) return out;
415
- if (node.type === "text") {
416
- const start = node.position?.start.offset;
417
- out.push({
418
- text: node.value,
419
- offs:
420
- typeof start === "number"
421
- ? mdOffsets(node.value, src, start)
422
- : Array.from(node.value, () => null),
423
- });
424
- }
425
- for (const c of "children" in node ? node.children : [])
426
- mdVisibleRuns(c, src, out);
427
- return out;
428
- }
429
-
430
- const visibleRuns = (sourceCode: RuleSourceCode): Run[] =>
431
- isTex(sourceCode)
432
- ? texVisibleRuns(sourceCode.raw)
433
- : mdVisibleRuns(sourceCode.ast, sourceCode.text);
434
-
435
379
  /** A report at `[from, to)` of the file. */
436
380
  const at = (
437
381
  context: RuleContext,
@@ -455,36 +399,22 @@ const SECTION_GLYPH = /§/g;
455
399
 
456
400
  function sectionWord(context: RuleContext) {
457
401
  const sc = context.sourceCode;
458
- if (isTex(sc)) {
459
- const seen = new Set();
460
- for (const m of markupMatches(sc, SECTION_TEX)) {
461
- seen.add(m.index);
462
- const word = m[3] ? "Section~" : "Section ";
463
- const range: [number, number] = [m.index, m.index + m[0].length];
464
- at(context, range[0], range[1], {
465
- messageId: "sign",
466
- fix: (f) => f.replaceTextRange(range, word),
467
- });
468
- }
469
- // A glyph before neither a reference nor a number: reported, not rewritten — there is no
470
- // single right word for a doubled sign or a sign before prose.
471
- for (const m of markupMatches(sc, SECTION_GLYPH))
472
- if (!seen.has(m.index))
473
- at(context, m.index, m.index + 1, { messageId: "sign" });
474
- return;
402
+ if (!isTex(sc)) return;
403
+ const seen = new Set();
404
+ for (const m of markupMatches(sc, SECTION_TEX)) {
405
+ seen.add(m.index);
406
+ const word = m[3] ? "Section~" : "Section ";
407
+ const range: [number, number] = [m.index, m.index + m[0].length];
408
+ at(context, range[0], range[1], {
409
+ messageId: "sign",
410
+ fix: (f) => f.replaceTextRange(range, word),
411
+ });
475
412
  }
476
- for (const r of visibleRuns(sc))
477
- for (const m of r.text.matchAll(/§([ \t]*)(\d)?/g)) {
478
- const from = r.offs[m.index];
479
- if (from === null || from === undefined) continue;
480
- // The sign and the space after it; the number stays.
481
- const [, gap = ""] = m;
482
- const range: [number, number] = [from, from + 1 + gap.length];
483
- at(context, range[0], range[1], {
484
- messageId: "sign",
485
- ...(m[2] ? { fix: (f) => f.replaceTextRange(range, "Section ") } : {}),
486
- });
487
- }
413
+ // A glyph before neither a reference nor a number: reported, not rewritten — there is no
414
+ // single right word for a doubled sign or a sign before prose.
415
+ for (const m of markupMatches(sc, SECTION_GLYPH))
416
+ if (!seen.has(m.index))
417
+ at(context, m.index, m.index + 1, { messageId: "sign" });
488
418
  }
489
419
 
490
420
  // ── paper/leading-zero ─────────────────────────────────────────────────────────────────
@@ -508,8 +438,9 @@ const paragraphBefore = (src: string, i: number) => {
508
438
 
509
439
  function leadingZero(context: RuleContext) {
510
440
  const sc = context.sourceCode;
511
- const src = isTex(sc) ? sc.raw : sc.text;
512
- for (const r of visibleRuns(sc))
441
+ if (!isTex(sc)) return;
442
+ const src = sc.raw;
443
+ for (const r of texVisibleRuns(src))
513
444
  for (const m of r.text.matchAll(BARE_DECIMAL)) {
514
445
  const dot = r.offs[m.index];
515
446
  if (dot === null || dot === undefined) continue;
@@ -170,7 +170,7 @@ for (const bad of ["", null, 0, false, [], {}, 42])
170
170
  const names = Object.keys(sets).sort();
171
171
  assert.deepEqual(
172
172
  names,
173
- ["all", "md", "pdfFacts", "refFacts", "status", "tex", "venue"],
173
+ ["all", "pdfFacts", "refFacts", "status", "tex", "venue"],
174
174
  "the glob set changed — a consumer's config enumerates these by name, and a renamed or " +
175
175
  "dropped key is a block that silently lints nothing",
176
176
  );
@@ -41,7 +41,7 @@
41
41
  *
42
42
  * A FILENAME MARKER, no root at all (`**\/paper.tex`).
43
43
  * Measured on the first consumer: `paper.tex` ×4 with one outside the corpus,
44
- * `PIPELINE-STATUS.md` ×6 with two of them test fixtures, `paper.md` ×3 with two outside.
44
+ * `PIPELINE-STATUS.md` ×6 with two of them test fixtures.
45
45
  * The guard that would consume this is a `deny` at `error` severity; a denial on writing
46
46
  * a fixture gets the hook switched off the same day.
47
47
  *
@@ -134,15 +134,13 @@ export function papersRoot(
134
134
  * The glob sets a consumer declares its blocks on, derived from one root.
135
135
  *
136
136
  * 🔴 THE SHAPES LIVE HERE, NOT IN THE CONSUMER, and that is the half that keeps a rule from
137
- * going blind later. `paper.md` + `draft.md`, `_build/*.facts.json`, `PIPELINE-STATUS.md` are
137
+ * going blind later. `paper.tex`, `_build/*.facts.json`, `PIPELINE-STATUS.md` are
138
138
  * this pipeline's CONVENTION; if a rule starts reading a new file, the glob for it must arrive
139
139
  * with the rule, in the same package, rather than as a line every consumer has to add by hand
140
140
  * to keep up. A hand-maintained list of paths is the thing that rots, and rots silently.
141
141
  */
142
142
  export function paperFiles(root: string) {
143
143
  return {
144
- /** Markdown drafts — the two names a paper's prose is allowed to have. */
145
- md: [`${root}/*/paper.md`, `${root}/*/draft.md`],
146
144
  /** The LaTeX source of a paper (not its figures, not its vendored drafts). */
147
145
  tex: [`${root}/*/paper.tex`],
148
146
  /** The per-paper stage ledger. */
@@ -35,8 +35,8 @@
35
35
  * (`284:will be released`, `284:Upon acceptance` + `285:will be made publicly available`).
36
36
  *
37
37
  * 🔴 AND ONE DISAGREEMENT, WHICH IS NOT IN THE LOGIC BUT IN THE INPUT — and it is the whole
38
- * gain. The old implementation picked its file by looking for `paper.md` / `draft.md` and
39
- * otherwise taking the FIRST `.tex` in `readdirSync` order. Measured 2026-09-07:
38
+ * gain. The old implementation picked its file by taking the FIRST `.tex` in `readdirSync`
39
+ * order. Measured 2026-09-07:
40
40
  * paper A → a 536-BYTE file of nine `\def`s with venue numbers. The check, added
41
41
  * specifically BECAUSE of paper A, was not reading paper A;
42
42
  * paper B → a four-month-old draft, one of seven `.tex` files in that directory;
@@ -3,27 +3,11 @@
3
3
  * how the prose is written.
4
4
  *
5
5
  * ── WHY ITS OWN NAMESPACE, AND NOT `paper/*` ────────────────────────────────
6
- * In the source corpus the `paper/*` family is declared on BOTH `paper.md` and `paper.tex`,
7
- * and that parity is pinned by an assertion: "a rule added for markdown must not silently
8
- * fail to reach LaTeX (or the other way round)". The subject here is the BUILD MODE
9
- * (`\documentclass[review]`, `printacmref=false`), and in markdown it does not exist: a draft
10
- * has no mechanical sign saying "this build is the final one". Writing
11
- * `paper/future-promise: "off"` on markdown would be an exemption justified by "there is no
12
- * subject", and that same parity assertion requires the rule to be ALIVE on the other side —
13
- * i.e. the machinery exists but is built for the opposite direction (off on `.tex`, alive on
14
- * `.md`).
15
- *
16
- * Hence a namespace of its own, the same way an external word-counter gets one: it has a word
17
- * count markdown does not have, we have a preamble markdown does not have. Where the config
18
- * enumerates non-`paper/*` rules on `.tex`, the list is CLOSED and `tex/future-promise` is
19
- * named explicitly, so that a new namespace arrives by decision rather than as a side effect
20
- * of editing a config.
21
- *
22
- * ⚠️ THE MEASUREMENT THIS DECISION RESTS ON (source corpus, 2026-09-06 → 2026-09-07): had the
23
- * rule been declared on `paper.md` it would produce ZERO there TODAY — not one match of
24
- * `FUTURE_PROMISE_RE` in the markdown draft of the one paper that has one. So the choice is
25
- * about semantics, not noise: markdown has no input distinguishing a draft from a shipped
26
- * build, and the rule would fire on a draft's honest promise.
6
+ * The subject here is the BUILD MODE (`\documentclass[review]`, `printacmref=false`), not the
7
+ * prose the `paper/*` rules read. Hence a namespace of its own, the same way an external
8
+ * word-counter gets one. Where the config enumerates non-`paper/*` rules on `.tex`, the list is
9
+ * CLOSED and `tex/future-promise` is named explicitly, so that a new namespace arrives by
10
+ * decision rather than as a side effect of editing a config.
27
11
  *
28
12
  * ── THE RULE READS THE FILE FROM DISK, NOT THE PROJECTION. This is load-bearing ──
29
13
  * The LaTeX language in `latex-language.mjs` hands rules a PROJECTION of `.tex` into a
@@ -37,9 +21,8 @@
37
21
  * run (and in CI) there is no such difference.
38
22
  *
39
23
  * ── WHAT THE MOVE TO A LINT RULE FIXED BESIDES THE FORM (measured 2026-09-07) ──
40
- * 🔴 The previous implementation, a script, picked its input like this: look for `paper.md` /
41
- * `draft.md`, and failing that take `readdirSync(dir).find((f) => f.endsWith(".tex"))` — the
42
- * FIRST `.tex` in directory order. Measured across the three papers of the source corpus:
24
+ * 🔴 The previous implementation, a script, picked its input like this:
25
+ * `readdirSync(dir).find((f) => f.endsWith(".tex"))` — the FIRST `.tex` in directory order. Measured across the three papers of the source corpus:
43
26
  * paper A → a 536-BYTE file holding nine `\def`s with venue numbers. The check, added
44
27
  * specifically BECAUSE of paper A, was not reading paper A;
45
28
  * paper B → a four-month-old draft, one of SEVEN `.tex` files in that directory (the real
@@ -121,7 +104,7 @@ const FRONTMATTER_OVERRIDES = [
121
104
  // is looking.
122
105
  //
123
106
  // The scope is DELIBERATELY NARROW, because a noisy gate is a silenced gate:
124
- // - `.tex` only (see the file header: markdown has no sign of "this build is final");
107
+ // - `.tex` only: the build mode is read from the preamble;
125
108
  // - a review-mode build is EXEMPT, and that is the whole point: during peer review
126
109
  // "at camera-ready" is a true and ordinary promise. The defect is only a promise that
127
110
  // outlived its own delivery;
package/eslint.config.mjs CHANGED
@@ -520,7 +520,6 @@ const TYPESCRIPT_ONLY = [
520
520
  "test/e2e/**/*.{js,mjs,cjs}",
521
521
  "test/fixtures/layers/src/**/*.{js,mjs,cjs}",
522
522
  "fixtures/accepted-papers/**/*.{js,mjs,cjs}",
523
- "fixtures/real-markdown-paper/**/*.{js,mjs,cjs}",
524
523
  "skills/paper-pipeline/scripts/fixtures/**/*.{js,mjs,cjs}",
525
524
  "skills/plan-paper-timeline/fixtures/**/*.{js,mjs,cjs}",
526
525
  ];
@@ -799,6 +798,28 @@ export default [
799
798
  "no-restricted-imports": ["error", { paths: ONE_COLLECTION_LIBRARY }],
800
799
  },
801
800
  },
801
+ // 🔴 ONE PLACE READS BIBLIOGRAPHIES. `@retorquere/bibtex-parser` is known to its adapter only;
802
+ // every other file — the app, the domain, another adapter, a skill's script — reads a `.bib` through
803
+ // the `BibReader` port, and a path a person gave through `bibliographyAt` (src/paper-sources.ts).
804
+ // Three readers of `.bib` text grew here before there was one. Its own rule id, so no other
805
+ // `no-restricted-imports` block can replace it (test/eslint-bib-parser.test.ts).
806
+ {
807
+ files: ["**/*.{ts,mts,mjs,js,cjs}"],
808
+ ignores: ["src/adapters/bibtex/**"],
809
+ plugins: { n },
810
+ rules: {
811
+ "n/no-restricted-import": [
812
+ "error",
813
+ [
814
+ {
815
+ name: ["@retorquere/bibtex-parser", "@retorquere/bibtex-parser/**"],
816
+ message:
817
+ "The bibtex parser is src/adapters/bibtex/'s: read a .bib through the BibReader port (src/ports/bib-reader.ts), or a path through bibliographyAt (src/paper-sources.ts).",
818
+ },
819
+ ],
820
+ ],
821
+ },
822
+ },
802
823
  // 🔴 THE LINTER DOES NOT READ A SKILL. Skills are consumers of the package, not its storage: the
803
824
  // venue presets once lived in a skill and were located through a skill's script, so renaming the
804
825
  // skill broke `paperlint lint` (#133). The package's own code — src/, eslint-rules/, lib/ — may
@@ -3,7 +3,7 @@
3
3
  % rule is judged against what a real build of the class measures. It conforms: the LLM Usage
4
4
  % Statement closes it, and not one rule has anything to say.
5
5
  \documentclass[conference,compsoc]{IEEEtran}
6
- \begin{filecontents*}{refs.bib}
6
+ \begin{filecontents*}[overwrite]{refs.bib}
7
7
  @article{knuth84,
8
8
  title = {Literate Programming},
9
9
  author = {Knuth, Donald E.},
@@ -1,7 +1,7 @@
1
1
  % B: the name in an acknowledgments paragraph.
2
2
  % The declared identity is invented: "Ada Example", handle "adaexample" (paperlint.json).
3
3
  \documentclass[conference,compsoc]{IEEEtran}
4
- \begin{filecontents*}{refs.bib}
4
+ \begin{filecontents*}[overwrite]{refs.bib}
5
5
  @article{knuth84,
6
6
  title = {Literate Programming},
7
7
  author = {Knuth, Donald E.},
@@ -1,7 +1,7 @@
1
1
  % A: the name in the author block.
2
2
  % The declared identity is invented: "Ada Example", handle "adaexample" (paperlint.json).
3
3
  \documentclass[conference,compsoc]{IEEEtran}
4
- \begin{filecontents*}{refs.bib}
4
+ \begin{filecontents*}[overwrite]{refs.bib}
5
5
  @article{knuth84,
6
6
  title = {Literate Programming},
7
7
  author = {Knuth, Donald E.},