paperlint 8.0.0 → 9.0.1

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 (393) hide show
  1. package/.github/CLAUDE.md +22 -0
  2. package/.github/CLAUDE.md.spec.ts +20 -0
  3. package/CLAUDE.md +59 -531
  4. package/CONTRIBUTING.md +7 -6
  5. package/README.md +4 -3
  6. package/dist/CLAUDE.md.spec.d.ts +3 -0
  7. package/dist/CLAUDE.md.spec.d.ts.map +1 -0
  8. package/dist/CLAUDE.md.spec.js +62 -0
  9. package/dist/CLAUDE.md.spec.js.map +1 -0
  10. package/dist/adapters/bibtex/index.d.ts +3 -0
  11. package/dist/adapters/bibtex/index.d.ts.map +1 -0
  12. package/dist/adapters/bibtex/index.js +103 -0
  13. package/dist/adapters/bibtex/index.js.map +1 -0
  14. package/dist/adapters/latex/filecontents.d.ts +33 -0
  15. package/dist/adapters/latex/filecontents.d.ts.map +1 -0
  16. package/dist/adapters/latex/filecontents.js +65 -0
  17. package/dist/adapters/latex/filecontents.js.map +1 -0
  18. package/dist/adapters/latex/includes.d.ts +3 -3
  19. package/dist/adapters/latex/includes.d.ts.map +1 -1
  20. package/dist/adapters/latex/includes.js +18 -10
  21. package/dist/adapters/latex/includes.js.map +1 -1
  22. package/dist/adapters/latex/index.d.ts.map +1 -1
  23. package/dist/adapters/latex/index.js +2 -0
  24. package/dist/adapters/latex/index.js.map +1 -1
  25. package/dist/adapters/latex/nodes.d.ts +12 -0
  26. package/dist/adapters/latex/nodes.d.ts.map +1 -1
  27. package/dist/adapters/latex/nodes.js +39 -0
  28. package/dist/adapters/latex/nodes.js.map +1 -1
  29. package/dist/adapters/latex/parse.d.ts +7 -1
  30. package/dist/adapters/latex/parse.d.ts.map +1 -1
  31. package/dist/adapters/latex/parse.js +55 -6
  32. package/dist/adapters/latex/parse.js.map +1 -1
  33. package/dist/adapters/memory/index.d.ts +3 -0
  34. package/dist/adapters/memory/index.d.ts.map +1 -1
  35. package/dist/adapters/memory/index.js +9 -0
  36. package/dist/adapters/memory/index.js.map +1 -1
  37. package/dist/adapters/references/index.d.ts.map +1 -1
  38. package/dist/adapters/references/index.js +27 -24
  39. package/dist/adapters/references/index.js.map +1 -1
  40. package/dist/adapters/sources-record/index.d.ts +9 -0
  41. package/dist/adapters/sources-record/index.d.ts.map +1 -0
  42. package/dist/adapters/sources-record/index.js +68 -0
  43. package/dist/adapters/sources-record/index.js.map +1 -0
  44. package/dist/adapters/tex-output/blg.d.ts +9 -0
  45. package/dist/adapters/tex-output/blg.d.ts.map +1 -0
  46. package/dist/adapters/tex-output/blg.js +20 -0
  47. package/dist/adapters/tex-output/blg.js.map +1 -0
  48. package/dist/adapters/tex-output/fls.d.ts +8 -0
  49. package/dist/adapters/tex-output/fls.d.ts.map +1 -0
  50. package/dist/adapters/tex-output/fls.js +16 -0
  51. package/dist/adapters/tex-output/fls.js.map +1 -0
  52. package/dist/adapters/tex-output/index.d.ts +7 -0
  53. package/dist/adapters/tex-output/index.d.ts.map +1 -0
  54. package/dist/adapters/tex-output/index.js +7 -0
  55. package/dist/adapters/tex-output/index.js.map +1 -0
  56. package/dist/bib-rules.d.ts +47 -0
  57. package/dist/bib-rules.d.ts.map +1 -0
  58. package/dist/bib-rules.js +127 -0
  59. package/dist/bib-rules.js.map +1 -0
  60. package/dist/build.d.ts +29 -1
  61. package/dist/build.d.ts.map +1 -1
  62. package/dist/build.js +41 -25
  63. package/dist/build.js.map +1 -1
  64. package/dist/claim-provenance.d.ts +2 -2
  65. package/dist/cli.d.ts +6 -1
  66. package/dist/cli.d.ts.map +1 -1
  67. package/dist/cli.js +30 -26
  68. package/dist/cli.js.map +1 -1
  69. package/dist/domain/paper-source.d.ts +1 -10
  70. package/dist/domain/paper-source.d.ts.map +1 -1
  71. package/dist/domain/paper-source.js +34 -24
  72. package/dist/domain/paper-source.js.map +1 -1
  73. package/dist/domain/paper-sources.d.ts +72 -0
  74. package/dist/domain/paper-sources.d.ts.map +1 -0
  75. package/dist/domain/paper-sources.js +18 -0
  76. package/dist/domain/paper-sources.js.map +1 -0
  77. package/dist/domain/sources-record.d.ts +102 -0
  78. package/dist/domain/sources-record.d.ts.map +1 -0
  79. package/dist/domain/sources-record.js +38 -0
  80. package/dist/domain/sources-record.js.map +1 -0
  81. package/dist/domain/tex-run.d.ts +25 -0
  82. package/dist/domain/tex-run.d.ts.map +1 -0
  83. package/dist/domain/tex-run.js +79 -0
  84. package/dist/domain/tex-run.js.map +1 -0
  85. package/dist/eslint-rules/CLAUDE.md.spec.d.ts +3 -0
  86. package/dist/eslint-rules/CLAUDE.md.spec.d.ts.map +1 -0
  87. package/dist/eslint-rules/CLAUDE.md.spec.js +39 -0
  88. package/dist/eslint-rules/CLAUDE.md.spec.js.map +1 -0
  89. package/dist/eslint-rules/install-path-literals.d.ts +1 -1
  90. package/dist/eslint-rules/install-path-literals.js +4 -4
  91. package/dist/eslint-rules/install-path-literals.js.map +1 -1
  92. package/dist/eslint-rules/paper-context.d.ts.map +1 -1
  93. package/dist/eslint-rules/paper-context.js +4 -3
  94. package/dist/eslint-rules/paper-context.js.map +1 -1
  95. package/dist/eslint-rules/paper-typography.d.ts +0 -9
  96. package/dist/eslint-rules/paper-typography.d.ts.map +1 -1
  97. package/dist/eslint-rules/paper-typography.js +12 -5
  98. package/dist/eslint-rules/paper-typography.js.map +1 -1
  99. package/dist/eslint-rules/pdf-last-page-balance.js +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/agent-cli-version.js +1 -1
  108. package/dist/lib/agent-cli-version.js.map +1 -1
  109. package/dist/package-dirs.d.ts.map +1 -1
  110. package/dist/package-dirs.js +3 -2
  111. package/dist/package-dirs.js.map +1 -1
  112. package/dist/paper-includes.d.ts +4 -7
  113. package/dist/paper-includes.d.ts.map +1 -1
  114. package/dist/paper-includes.js +29 -21
  115. package/dist/paper-includes.js.map +1 -1
  116. package/dist/paper-prose.d.ts +2 -2
  117. package/dist/paper-record.d.ts +25 -0
  118. package/dist/paper-record.d.ts.map +1 -0
  119. package/dist/paper-record.js +39 -0
  120. package/dist/paper-record.js.map +1 -0
  121. package/dist/paper-sources.d.ts +66 -0
  122. package/dist/paper-sources.d.ts.map +1 -0
  123. package/dist/paper-sources.js +92 -0
  124. package/dist/paper-sources.js.map +1 -0
  125. package/dist/ports/bib-reader.d.ts +18 -0
  126. package/dist/ports/bib-reader.d.ts.map +1 -0
  127. package/dist/ports/bib-reader.js +2 -0
  128. package/dist/ports/bib-reader.js.map +1 -0
  129. package/dist/ports/check-references.d.ts +9 -4
  130. package/dist/ports/check-references.d.ts.map +1 -1
  131. package/dist/ports/latex.d.ts +15 -0
  132. package/dist/ports/latex.d.ts.map +1 -1
  133. package/dist/ports/sources-codec.d.ts +11 -0
  134. package/dist/ports/sources-codec.d.ts.map +1 -0
  135. package/dist/ports/sources-codec.js +2 -0
  136. package/dist/ports/sources-codec.js.map +1 -0
  137. package/dist/ports/tex-output.d.ts +17 -0
  138. package/dist/ports/tex-output.d.ts.map +1 -0
  139. package/dist/ports/tex-output.js +2 -0
  140. package/dist/ports/tex-output.js.map +1 -0
  141. package/dist/recorded-bibliography.d.ts +89 -0
  142. package/dist/recorded-bibliography.d.ts.map +1 -0
  143. package/dist/recorded-bibliography.js +136 -0
  144. package/dist/recorded-bibliography.js.map +1 -0
  145. package/dist/reference-rules.d.ts +5 -5
  146. package/dist/reference-rules.d.ts.map +1 -1
  147. package/dist/reference-rules.js +150 -77
  148. package/dist/reference-rules.js.map +1 -1
  149. package/dist/references.d.ts +71 -16
  150. package/dist/references.d.ts.map +1 -1
  151. package/dist/references.js +110 -26
  152. package/dist/references.js.map +1 -1
  153. package/dist/register-bands.d.ts +3 -3
  154. package/dist/register-bands.d.ts.map +1 -1
  155. package/dist/register-bands.js.map +1 -1
  156. package/dist/register.d.ts +2 -2
  157. package/dist/sources-record.d.ts +38 -0
  158. package/dist/sources-record.d.ts.map +1 -0
  159. package/dist/sources-record.js +123 -0
  160. package/dist/sources-record.js.map +1 -0
  161. package/dist/sources-rules.d.ts +44 -0
  162. package/dist/sources-rules.d.ts.map +1 -0
  163. package/dist/sources-rules.js +76 -0
  164. package/dist/sources-rules.js.map +1 -0
  165. package/dist/tex-paper.d.ts +10 -15
  166. package/dist/tex-paper.d.ts.map +1 -1
  167. package/dist/tex-paper.js +24 -40
  168. package/dist/tex-paper.js.map +1 -1
  169. package/dist/tex-requirements.js +1 -1
  170. package/dist/tex-venue-rules.d.ts +3 -3
  171. package/dist/tex-venue-rules.d.ts.map +1 -1
  172. package/dist/tex-venue-rules.js.map +1 -1
  173. package/dist/toolchain.js +1 -1
  174. package/docs/configuration.md +12 -4
  175. package/docs/design/paper-sources.md +197 -0
  176. package/docs/e2e.md +1 -1
  177. package/docs/incidents.md +5 -5
  178. package/docs/package-shape-options.md +1 -1
  179. package/docs/references.md +34 -8
  180. package/docs/rules/bib/reachable-entry.md +64 -0
  181. package/docs/rules/paper/sources-fresh.md +81 -0
  182. package/docs/rules.md +62 -57
  183. package/eslint-rules/CLAUDE.md +44 -0
  184. package/eslint-rules/CLAUDE.md.spec.ts +43 -0
  185. package/eslint-rules/install-path-literals.harness.mjs +1 -1
  186. package/eslint-rules/install-path-literals.ts +4 -4
  187. package/eslint-rules/paper-context.ts +4 -3
  188. package/eslint-rules/paper-typography.test.mjs +12 -72
  189. package/eslint-rules/paper-typography.ts +13 -6
  190. package/eslint-rules/pdf-last-page-balance.ts +1 -1
  191. package/eslint.config.mjs +28 -3
  192. package/fixtures/build-e2e/aidc/paper.tex +1 -1
  193. package/fixtures/build-e2e/anon-ack/paper.tex +1 -1
  194. package/fixtures/build-e2e/anon-author/paper.tex +1 -1
  195. package/fixtures/build-e2e/anon-bib/paper.tex +1 -1
  196. package/fixtures/build-e2e/anon-href/paper.tex +1 -1
  197. package/fixtures/build-e2e/anon-macro/paper.tex +1 -1
  198. package/fixtures/build-e2e/anon-pdfauthor/paper.tex +1 -1
  199. package/fixtures/paper-sources/p1/paper.tex +30 -0
  200. package/fixtures/paper-sources/p1/refs.bib +6 -0
  201. package/fixtures/paper-sources/p1/sections/dead.tex +6 -0
  202. package/fixtures/paper-sources/p1/sections/intro.tex +7 -0
  203. package/fixtures/paper-sources/p1/sections/parked.tex +6 -0
  204. package/fixtures/paper-sources/p1/tex-truth.json +37 -0
  205. package/fixtures/paper-sources/p1/versions/2026-01-01-review.tex +4 -0
  206. package/fixtures/paper-sources/v1-stale/paper.tex +14 -0
  207. package/fixtures/paper-sources/v1-stale/refs.bib +6 -0
  208. package/fixtures/paper-sources/v1-stale/tex-truth.json +32 -0
  209. package/fixtures/paper-sources/v10-ifthenelse/anon.bib +1 -0
  210. package/fixtures/paper-sources/v10-ifthenelse/paper.tex +9 -0
  211. package/fixtures/paper-sources/v10-ifthenelse/real.bib +1 -0
  212. package/fixtures/paper-sources/v10-ifthenelse/tex-truth.json +28 -0
  213. package/fixtures/paper-sources/v11-two-blocks/paper.tex +12 -0
  214. package/fixtures/paper-sources/v11-two-blocks/tex-truth.json +30 -0
  215. package/fixtures/paper-sources/v13-percent-text/paper.tex +6 -0
  216. package/fixtures/paper-sources/v13-percent-text/refs.bib +3 -0
  217. package/fixtures/paper-sources/v13-percent-text/tex-truth.json +30 -0
  218. package/fixtures/paper-sources/v15-after-end/paper.tex +6 -0
  219. package/fixtures/paper-sources/v15-after-end/refs.bib +1 -0
  220. package/fixtures/paper-sources/v15-after-end/tex-truth.json +17 -0
  221. package/fixtures/paper-sources/v16-included-block/bibblock.tex +3 -0
  222. package/fixtures/paper-sources/v16-included-block/paper.tex +7 -0
  223. package/fixtures/paper-sources/v16-included-block/refs.bib +1 -0
  224. package/fixtures/paper-sources/v16-included-block/tex-truth.json +35 -0
  225. package/fixtures/paper-sources/v17-include-order/bibsetup.tex +1 -0
  226. package/fixtures/paper-sources/v17-include-order/first.bib +1 -0
  227. package/fixtures/paper-sources/v17-include-order/later.bib +1 -0
  228. package/fixtures/paper-sources/v17-include-order/paper.tex +7 -0
  229. package/fixtures/paper-sources/v17-include-order/tex-truth.json +40 -0
  230. package/fixtures/paper-sources/v2-overwrite/paper.tex +14 -0
  231. package/fixtures/paper-sources/v2-overwrite/refs.bib +6 -0
  232. package/fixtures/paper-sources/v2-overwrite/tex-truth.json +30 -0
  233. package/fixtures/paper-sources/v22-parked-thebibliography/paper.tex +7 -0
  234. package/fixtures/paper-sources/v22-parked-thebibliography/tex-truth.json +17 -0
  235. package/fixtures/paper-sources/v23-parked-include/old.bib +1 -0
  236. package/fixtures/paper-sources/v23-parked-include/paper.tex +7 -0
  237. package/fixtures/paper-sources/v23-parked-include/parked.tex +1 -0
  238. package/fixtures/paper-sources/v23-parked-include/refs.bib +1 -0
  239. package/fixtures/paper-sources/v23-parked-include/tex-truth.json +28 -0
  240. package/fixtures/paper-sources/v24-quoted-fields/paper.tex +6 -0
  241. package/fixtures/paper-sources/v24-quoted-fields/refs.bib +5 -0
  242. package/fixtures/paper-sources/v24-quoted-fields/tex-truth.json +32 -0
  243. package/fixtures/paper-sources/v25-string-differs/paper.tex +10 -0
  244. package/fixtures/paper-sources/v25-string-differs/refs.bib +2 -0
  245. package/fixtures/paper-sources/v25-string-differs/tex-truth.json +32 -0
  246. package/fixtures/paper-sources/v26-comment-in-names/one.bib +1 -0
  247. package/fixtures/paper-sources/v26-comment-in-names/paper.tex +7 -0
  248. package/fixtures/paper-sources/v26-comment-in-names/tex-truth.json +32 -0
  249. package/fixtures/paper-sources/v26-comment-in-names/two.bib +1 -0
  250. package/fixtures/paper-sources/v27-shared-string/abbreviations.bib +2 -0
  251. package/fixtures/paper-sources/v27-shared-string/paper.tex +6 -0
  252. package/fixtures/paper-sources/v27-shared-string/references.bib +6 -0
  253. package/fixtures/paper-sources/v27-shared-string/tex-truth.json +31 -0
  254. package/fixtures/paper-sources/v28-shared-string-block/abbreviations.bib +2 -0
  255. package/fixtures/paper-sources/v28-shared-string-block/paper.tex +14 -0
  256. package/fixtures/paper-sources/v28-shared-string-block/tex-truth.json +33 -0
  257. package/fixtures/paper-sources/v29-generated-input/paper.tex +9 -0
  258. package/fixtures/paper-sources/v29-generated-input/tex-truth.json +24 -0
  259. package/fixtures/paper-sources/v3-declared/paper.bib +6 -0
  260. package/fixtures/paper-sources/v3-declared/paper.tex +6 -0
  261. package/fixtures/paper-sources/v3-declared/tex-truth.json +28 -0
  262. package/fixtures/paper-sources/v30-name-next-line/paper.tex +14 -0
  263. package/fixtures/paper-sources/v30-name-next-line/tex-truth.json +30 -0
  264. package/fixtures/paper-sources/v4-commented/paper.tex +14 -0
  265. package/fixtures/paper-sources/v4-commented/refs.bib +6 -0
  266. package/fixtures/paper-sources/v4-commented/tex-truth.json +28 -0
  267. package/fixtures/paper-sources/v5-percent-entry/paper.tex +20 -0
  268. package/fixtures/paper-sources/v5-percent-entry/tex-truth.json +37 -0
  269. package/fixtures/paper-sources/v6-unclosed/paper.tex +6 -0
  270. package/fixtures/paper-sources/v6-unclosed/refs.bib +5 -0
  271. package/fixtures/paper-sources/v6-unclosed/tex-truth.json +37 -0
  272. package/fixtures/paper-sources/v8-jobname/paper.tex +9 -0
  273. package/fixtures/paper-sources/v8-jobname/tex-truth.json +30 -0
  274. package/fixtures/paper-sources/v9-two-bibliographies/one.bib +1 -0
  275. package/fixtures/paper-sources/v9-two-bibliographies/paper.tex +7 -0
  276. package/fixtures/paper-sources/v9-two-bibliographies/tex-truth.json +35 -0
  277. package/fixtures/paper-sources/v9-two-bibliographies/two.bib +1 -0
  278. package/fixtures/paper-typography/clean-paper/paper.tex +1 -1
  279. package/fixtures/paper-typography/messy-paper/paper.tex +1 -1
  280. package/hooks/CLAUDE.md +34 -0
  281. package/hooks/CLAUDE.md.spec.ts +30 -0
  282. package/lib/agent-cli-version.ts +1 -1
  283. package/package.json +3 -11
  284. package/plugin/CLAUDE.md +22 -0
  285. package/plugin/CLAUDE.md.spec.ts +22 -0
  286. package/scripts/check.test.ts +23 -1
  287. package/scripts/check.ts +7 -3
  288. package/scripts/coverage-config.test.ts +21 -11
  289. package/scripts/layer-legacy-frozen.test.ts +41 -2
  290. package/scripts/layer-legacy-frozen.ts +31 -2
  291. package/skills/CLAUDE.md +19 -0
  292. package/skills/CLAUDE.md.spec.ts +18 -0
  293. package/skills/grade-paper-writing/SKILL.md +4 -3
  294. package/skills/grade-paper-writing/SKILL.md.spec.ts +3 -2
  295. package/skills/grade-paper-writing/prose-lint.mjs +11 -2
  296. package/skills/grade-paper-writing/prose-lint.test.ts +52 -4
  297. package/skills/paper-pipeline/scripts/extract-ref-facts.harness.mjs +83 -33
  298. package/skills/paper-pipeline/scripts/extract-ref-facts.mjs +67 -115
  299. package/skills/paper-pipeline/scripts/extract-ref-facts.test.mjs +128 -121
  300. package/skills/plan-paper-timeline/plan-paper-timeline.effects.harness.mjs +21 -102
  301. package/skills/verify-citations/SKILL.md +7 -4
  302. package/skills/verify-citations/SKILL.md.spec.ts +6 -3
  303. package/skills/verify-citations/scripts/bib-authors.check.test.mjs +17 -19
  304. package/skills/verify-citations/scripts/bib-authors.cli.test.ts +129 -46
  305. package/skills/verify-citations/scripts/bib-authors.d.mts +22 -2
  306. package/skills/verify-citations/scripts/bib-authors.mjs +57 -111
  307. package/skills/verify-citations/scripts/verify-cites.cli.test.ts +16 -0
  308. package/skills/verify-citations/scripts/verify-cites.d.mts +19 -3
  309. package/skills/verify-citations/scripts/verify-cites.mjs +42 -108
  310. package/skills/verify-citations/scripts/verify-cites.test.mjs +23 -36
  311. package/src/CLAUDE.md +23 -1
  312. package/src/CLAUDE.md.spec.ts +72 -0
  313. package/src/adapters/bibtex/index.test.ts +163 -0
  314. package/src/adapters/bibtex/index.ts +155 -0
  315. package/src/adapters/latex/filecontents.ts +114 -0
  316. package/src/adapters/latex/includes.ts +18 -10
  317. package/src/adapters/latex/index.test.ts +73 -0
  318. package/src/adapters/latex/index.ts +2 -0
  319. package/src/adapters/latex/nodes.ts +64 -0
  320. package/src/adapters/latex/parse.test.ts +38 -0
  321. package/src/adapters/latex/parse.ts +67 -6
  322. package/src/adapters/memory/index.ts +17 -0
  323. package/src/adapters/references/index.test.ts +75 -21
  324. package/src/adapters/references/index.ts +37 -32
  325. package/src/adapters/sources-record/index.test.ts +87 -0
  326. package/src/adapters/sources-record/index.ts +82 -0
  327. package/src/adapters/tex-output/blg.ts +27 -0
  328. package/src/adapters/tex-output/fls.ts +24 -0
  329. package/src/adapters/tex-output/index.test.ts +171 -0
  330. package/src/adapters/tex-output/index.ts +12 -0
  331. package/src/bib-rules.test.ts +356 -0
  332. package/src/bib-rules.ts +228 -0
  333. package/src/bibliography-at.test.ts +286 -0
  334. package/src/build.test.ts +210 -0
  335. package/src/build.ts +101 -36
  336. package/src/claim-provenance.test.ts +33 -7
  337. package/src/claim-provenance.ts +2 -2
  338. package/src/cli.harness.mjs +20 -10
  339. package/src/cli.test.ts +14 -10
  340. package/src/cli.ts +37 -28
  341. package/src/domain/paper-source.test.ts +58 -20
  342. package/src/domain/paper-source.ts +38 -42
  343. package/src/domain/paper-sources.ts +94 -0
  344. package/src/domain/sources-record.test.ts +71 -0
  345. package/src/domain/sources-record.ts +136 -0
  346. package/src/domain/tex-run.test.ts +228 -0
  347. package/src/domain/tex-run.ts +139 -0
  348. package/src/heading-case.test.ts +24 -10
  349. package/src/latex-log.ts +20 -0
  350. package/src/latex-loop.harness.ts +1 -0
  351. package/src/latex-loop.ts +2 -0
  352. package/src/linted-set.test.ts +9 -1
  353. package/src/new-paper.harness.mjs +3 -2
  354. package/src/new-paper.test.ts +9 -5
  355. package/src/package-dirs.ts +3 -2
  356. package/src/paper-includes.test.ts +157 -38
  357. package/src/paper-includes.ts +39 -35
  358. package/src/paper-prose.test.ts +34 -5
  359. package/src/paper-prose.ts +2 -2
  360. package/src/paper-record.test.ts +173 -0
  361. package/src/paper-record.ts +66 -0
  362. package/src/paper-settings-commands.test.ts +5 -2
  363. package/src/paper-sources.ts +151 -0
  364. package/src/ports/bib-reader.ts +27 -0
  365. package/src/ports/check-references.ts +7 -3
  366. package/src/ports/latex.ts +19 -0
  367. package/src/ports/sources-codec.ts +11 -0
  368. package/src/ports/tex-output.ts +17 -0
  369. package/src/recorded-bibliography.ts +297 -0
  370. package/src/reference-rules.ts +217 -104
  371. package/src/references.test.ts +520 -177
  372. package/src/references.ts +185 -37
  373. package/src/register-bands.test.ts +5 -1
  374. package/src/register-bands.ts +3 -5
  375. package/src/register.test.ts +6 -1
  376. package/src/register.ts +2 -2
  377. package/src/rule-cases.test.ts +68 -12
  378. package/src/rule-docs.test.ts +0 -1
  379. package/src/sources-record.test.ts +308 -0
  380. package/src/sources-record.ts +231 -0
  381. package/src/sources-rules.test.ts +152 -0
  382. package/src/sources-rules.ts +117 -0
  383. package/src/tex-paper.test.ts +148 -78
  384. package/src/tex-paper.ts +31 -61
  385. package/src/tex-requirements.ts +1 -1
  386. package/src/tex-venue-rules.test.ts +36 -8
  387. package/src/tex-venue-rules.ts +3 -5
  388. package/src/toolchain.ts +1 -1
  389. package/dist/eslint-rules/bib-reachable-entry.d.ts +0 -22
  390. package/dist/eslint-rules/bib-reachable-entry.d.ts.map +0 -1
  391. package/dist/eslint-rules/bib-reachable-entry.js +0 -75
  392. package/dist/eslint-rules/bib-reachable-entry.js.map +0 -1
  393. package/eslint-rules/bib-reachable-entry.ts +0 -76
@@ -43,122 +43,60 @@
43
43
  * Usage: node bib-authors.mjs <paper-dir-or-.bib-or-.tex> [--json]
44
44
  * Exit: 0 = no author-set/order differences 1 = differences found 2 = usage/IO error
45
45
  */
46
- import { readFileSync, existsSync, readdirSync } from "node:fs";
47
- import { join, extname } from "node:path";
46
+ import { resolve } from "node:path";
47
+ import { latexReader } from "#src/adapters/latex/index";
48
+ import { nodeFiles } from "#src/adapters/node/index";
49
+ import { sourcesCodec } from "#src/adapters/sources-record/index";
50
+ import { absolutePath } from "#src/domain/paths";
51
+ import { bibReader } from "#src/adapters/bibtex/index";
52
+ import { bibliographyAt, bibliographyUnreadWhy } from "#src/paper-sources";
48
53
  import { isMain } from "../../paper-pipeline/scripts/consumer.mjs";
49
54
 
50
55
  const DBLP = "https://dblp.org/search/publ/api";
51
56
 
52
- /* ---------- input: a .bib, a .tex with filecontents, or a paper dir ---------- */
57
+ /* ---------- input: the bibliography TeX reads, or a .bib named ---------- */
53
58
 
54
- function bibTextFrom(target) {
55
- let file = target;
56
- if (!existsSync(file)) die(`no such path: ${file}`);
57
- if (!extname(file)) {
58
- const names = readdirSync(file);
59
- // 🔴 THE CANONICAL NAME FIRST, then the only candidate, and never "whichever
60
- // turns up first" (review #189). It used to be `names.find(f => f.endsWith(".tex"))`,
61
- // that is, the first in directory order. Measured: one of the papers in the corpus
62
- // has FIVE drafts lying around (paper-CONSTRUCTIVE-…, paper-FOLDED-…, paper-SAFE-…),
63
- // while PIPELINE-STATUS.md names `paper.tex` as the submitted source. The author
64
- // check went off into a stale draft and printed a verdict about the WRONG
65
- // bibliography — a gate that checks the wrong file is worse than a missing one,
66
- // because it says "checked".
67
- const pick = (ext) => {
68
- const canon = names.find((f) => f === `paper${ext}`);
69
- if (canon) return canon;
70
- const all = names.filter((f) => f.endsWith(ext)).sort();
71
- if (all.length === 1) return all[0];
72
- if (all.length > 1) {
73
- // We do not fail: a new check here would set up one more surface, and its
74
- // home is decided by the ladder in the `CLAUDE.md` next to the papers, not by
75
- // this script (the `frozen-checks` ratchet is what guards that). It is enough
76
- // to SAY it out loud and to choose DETERMINISTICALLY — the header below prints
77
- // which file was checked anyway, so the reader sees the choice.
78
- console.error(
79
- `⚠️ ${file} holds several ${ext} files and no canonical paper${ext}: ` +
80
- `${all.join(", ")} — took ${all[0]}. If that is the wrong one, name the file explicitly.`,
81
- );
82
- return all[0];
83
- }
84
- return undefined;
85
- };
86
- const found = pick(".bib") || pick(".tex");
87
- if (!found) die(`no .bib or .tex in ${file}`);
88
- file = join(file, found);
89
- }
90
- const text = readFileSync(file, "utf-8");
91
- if (file.endsWith(".bib")) return { text, file };
92
- // Only a .bib and a .tex are read: any other file would fall through to the refs.bib beside it,
93
- // and the report would name a file nobody asked about.
94
- if (!file.endsWith(".tex"))
95
- die(`${file} is not a .bib or a .tex — give the paper's .bib or paper.tex`);
96
- // A .tex may carry the bibliography inline via filecontents — that is how our papers do it.
97
- const m = text.match(
98
- /\\begin\{filecontents\*?\}(?:\[[^\]]*\])?\{[^}]*\.bib\}\r?\n([\s\S]*?)\\end\{filecontents\*?\}/,
99
- );
100
- if (m) return { text: m[1], file };
101
- const sibling = join(file, "..", "refs.bib");
102
- if (existsSync(sibling))
103
- return { text: readFileSync(sibling, "utf-8"), file: sibling };
104
- die(
105
- `no bibliography found in ${file} (no filecontents block, no refs.bib beside it)`,
106
- );
107
- }
59
+ const DEPS = {
60
+ files: nodeFiles,
61
+ codec: sourcesCodec,
62
+ latex: latexReader,
63
+ bib: bibReader,
64
+ };
108
65
 
109
- /* ---------- a deliberately small bib reader ----------
110
- * Only three fields are needed (type, key, author, title, booktitle/journal), and a full
111
- * BibTeX grammar would be a second thing to maintain. Brace-depth counting is enough and
112
- * is exercised by the colocated test. */
113
- export function parseBib(text) {
114
- const out = [];
115
- const re = /@(\w+)\s*\{\s*([^,\s]+)\s*,/g;
116
- let m;
117
- while ((m = re.exec(text))) {
118
- const [, type, key] = m;
119
- let i = re.lastIndex,
120
- depth = 1;
121
- while (i < text.length && depth > 0) {
122
- if (text[i] === "{") depth++;
123
- else if (text[i] === "}") depth--;
124
- i++;
125
- }
126
- const body = text.slice(re.lastIndex, i - 1);
127
- out.push({ type: type.toLowerCase(), key, ...fields(body) });
128
- }
129
- return out;
66
+ /**
67
+ * The texts to read: a `.bib` as named; for a paper directory or a `.tex`, the databases the last
68
+ * build's bibtex opened (`_build/sources.json`) — `bibliographyAt` (src/paper-sources.ts) answers, and
69
+ * refuses in its words, "run `npx paperlint build` first" among them.
70
+ */
71
+ function bibliographyFrom(target) {
72
+ const r = bibliographyAt(absolutePath(resolve(target)), DEPS);
73
+ if (!r.ok) die(bibliographyUnreadWhy(r.error));
74
+ const { texts } = r.value;
75
+ return { files: [...new Set(texts.map((x) => x.path))], texts };
130
76
  }
131
77
 
132
- function fields(body) {
133
- const get = (name) => {
134
- const r = new RegExp(`(?:^|[,\\s])${name}\\s*=\\s*`, "i");
135
- const at = body.search(r);
136
- if (at === -1) return "";
137
- let i = body.indexOf("=", at) + 1;
138
- while (/\s/.test(body[i])) i++;
139
- if (body[i] === "{") {
140
- let depth = 1,
141
- j = i + 1;
142
- while (j < body.length && depth > 0) {
143
- if (body[j] === "{") depth++;
144
- else if (body[j] === "}") depth--;
145
- j++;
146
- }
147
- return body.slice(i + 1, j - 1);
148
- }
149
- if (body[i] === '"') {
150
- const j = body.indexOf('"', i + 1);
151
- return body.slice(i + 1, j);
152
- }
153
- const j = body.indexOf(",", i);
154
- return body.slice(i, j === -1 ? undefined : j);
155
- };
156
- return {
157
- author: get("author"),
158
- title: get("title"),
159
- booktitle: get("booktitle"),
160
- journal: get("journal"),
161
- };
78
+ /* ---------- entries: as the bibtex reader read them ---------- */
79
+
80
+ /** A name of a name list as BibTeX writes it, family part first: `von Last, First`, or the literal. */
81
+ const writtenName = (n) =>
82
+ n.name ??
83
+ [[n.prefix, n.lastName].filter(Boolean).join(" "), n.firstName]
84
+ .filter(Boolean)
85
+ .join(", ");
86
+
87
+ /**
88
+ * The entries this check reads, from the bibtex reader's (src/ports/bib-reader.ts): type, key, the
89
+ * author list written out (`and others` kept), title, booktitle and journal; "" for a field it lacks.
90
+ */
91
+ export function authorEntries(entries) {
92
+ return entries.map((e) => ({
93
+ type: e.type,
94
+ key: e.key,
95
+ author: (e.names.author ?? []).map(writtenName).join(" and "),
96
+ title: e.fields.title ?? "",
97
+ booktitle: e.fields.booktitle ?? "",
98
+ journal: e.fields.journal ?? "",
99
+ }));
162
100
  }
163
101
 
164
102
  /* ---------- normalisation: surnames only, in order ---------- */
@@ -353,14 +291,22 @@ async function main() {
353
291
  if (!args[0])
354
292
  die("usage: bib-authors.mjs <paper-dir|file.bib|file.tex> [--json]");
355
293
 
356
- const { text, file } = bibTextFrom(args[0]);
357
- const parsed = parseBib(text);
294
+ const { files, texts } = bibliographyFrom(args[0]);
295
+ const file = files.join(", ");
296
+ const parsed = texts.flatMap((t) => authorEntries(t.entries));
358
297
  const entries = parsed.filter((e) => e.title && e.author);
359
298
  const { findings, skipped, unchecked } = await checkAuthors(parsed);
360
299
  if (asJson) {
361
300
  console.log(
362
301
  JSON.stringify(
363
- { file, entries: entries.length, findings, skipped, unchecked },
302
+ {
303
+ file: files[0],
304
+ files,
305
+ entries: entries.length,
306
+ findings,
307
+ skipped,
308
+ unchecked,
309
+ },
364
310
  null,
365
311
  2,
366
312
  ),
@@ -10,6 +10,8 @@ import { dirname, join } from "node:path";
10
10
  import { fileURLToPath } from "node:url";
11
11
  import { test } from "vitest";
12
12
  import { z } from "zod";
13
+ import { absolutePath } from "#src/domain/paths";
14
+ import { bibliographyUnreadWhy } from "#src/paper-sources";
13
15
  import {
14
16
  runNode,
15
17
  useTempDir,
@@ -92,6 +94,20 @@ test("an input that cannot be read: exit 2, named", () => {
92
94
  );
93
95
  });
94
96
 
97
+ test("a .bib that is not there: exit 2, in bibliographyAt's words", () => {
98
+ // The path → bibliography vocabulary is tested once, beside bibliographyAt (src/paper-sources.ts).
99
+ const gone = join(root, "gone.bib");
100
+ const r = offline([gone]);
101
+ assert.deepEqual(
102
+ { status: r.status, stdout: r.stdout, stderr: r.stderr },
103
+ {
104
+ status: 2,
105
+ stdout: "",
106
+ stderr: `error: could not read input — ${bibliographyUnreadWhy({ kind: "missing", path: absolutePath(gone) })}\n`,
107
+ },
108
+ );
109
+ });
110
+
95
111
  test("offline over a JSON list: a citation with no id is unresolvable by name, a short SHA is flagged", () => {
96
112
  const r = parsed(offline([join(root, "cites.json")]));
97
113
  assert.deepEqual(
@@ -5,14 +5,30 @@
5
5
  // Only what `src/` imports is declared. The `.mjs` is not typechecked, so a declaration nobody
6
6
  // compiles against would drift without anyone noticing.
7
7
 
8
- /** One entry as `parseBib` returns it: the BibTeX key as `id`, plus the fields it recognised. */
8
+ /** One citation as `citationsOf` returns it: the BibTeX key as `id`, plus the fields it recognised. */
9
9
  export interface Citation {
10
10
  readonly id: string;
11
11
  readonly [field: string]: unknown;
12
12
  }
13
13
 
14
- /** Every entry of a BibTeX text, `@comment`/`@string`/`@preamble` skipped. */
15
- export declare function parseBib(text: string): Citation[];
14
+ /** A name of a name list, in its parts (`BibName`). */
15
+ export interface ReadName {
16
+ readonly lastName?: string;
17
+ readonly firstName?: string;
18
+ readonly prefix?: string;
19
+ readonly suffix?: string;
20
+ readonly name?: string;
21
+ }
22
+
23
+ /** An entry as the bibtex reader reads it (`BibEntry`, src/domain/paper-sources.ts), as far as citations use it. */
24
+ export interface ReadEntry {
25
+ readonly key: string;
26
+ readonly fields: Readonly<Record<string, string>>;
27
+ readonly names: Readonly<Record<string, readonly ReadName[]>>;
28
+ }
29
+
30
+ /** The citations of a bibliography's entries, as the bibtex reader read them. */
31
+ export declare function citationsOf(entries: readonly ReadEntry[]): Citation[];
16
32
 
17
33
  /** The title reduced to what identifies it: case, braces and punctuation removed. */
18
34
  export declare function titleIdentity(title: string): string;
@@ -47,7 +47,13 @@
47
47
 
48
48
  import { mkdirSync, readFileSync, writeFileSync, existsSync } from "node:fs";
49
49
  import { homedir } from "node:os";
50
- import { dirname, join, extname } from "node:path";
50
+ import { dirname, join, extname, resolve } from "node:path";
51
+ import { bibReader } from "#src/adapters/bibtex/index";
52
+ import { latexReader } from "#src/adapters/latex/index";
53
+ import { nodeFiles } from "#src/adapters/node/index";
54
+ import { sourcesCodec } from "#src/adapters/sources-record/index";
55
+ import { absolutePath } from "#src/domain/paths";
56
+ import { bibliographyAt, bibliographyUnreadWhy } from "#src/paper-sources";
51
57
  import { createHash } from "node:crypto";
52
58
  import {
53
59
  consumerContactEmail,
@@ -588,116 +594,32 @@ export function reduceVerdict(citation, evidence, commitFlags = []) {
588
594
  }
589
595
 
590
596
  // ─────────────────────────────────────────────────────────────────────────────
591
- // .bib PARSER — small, best-effort. Extracts @article/@inproceedings/... entries
592
- // and (loosely) \bibitem blocks. Not a full BibTeX grammar; enough to seed cites.
597
+ // CITATIONS — from the entries the bibtex reader read (src/ports/bib-reader.ts).
593
598
  // ─────────────────────────────────────────────────────────────────────────────
594
599
 
600
+ /** A name of a name list, written out: `First von Last, Jr.`, or the literal it was. */
601
+ const writtenName = (n) =>
602
+ n.name ??
603
+ [n.firstName, n.prefix, n.lastName, n.suffix].filter(Boolean).join(" ");
604
+
595
605
  /**
596
- * field(body, name) — extract a BibTeX field value from an entry body.
597
- * (a) anchors `name` so `title` does NOT also match `booktitle` (S4a);
598
- * (b) reads BRACE-delimited values with balanced-brace scanning, so a protected
599
- * acronym `{Evaluating {LLM}-based ...}` is captured whole, not truncated at
600
- * the inner `}` (S4b);
601
- * also handles "quoted" and bare (numeric) values.
606
+ * The citations of a bibliography's entries, as the bibtex reader read them: the key as `id`, and
607
+ * the doi, title, year, authors and arXiv eprint each entry has.
602
608
  */
603
- function bibField(body, name) {
604
- const re = new RegExp(`(?<![a-zA-Z])${name}\\s*=\\s*`, "i");
605
- const fm = re.exec(body);
606
- if (!fm) return undefined;
607
- let i = fm.index + fm[0].length;
608
- const ch = body[i];
609
- if (ch === "{") {
610
- let depth = 0;
611
- let j = i;
612
- for (; j < body.length; j++) {
613
- if (body[j] === "{") depth++;
614
- else if (body[j] === "}") {
615
- depth--;
616
- if (depth === 0) break;
617
- }
618
- }
619
- return body
620
- .slice(i + 1, j)
621
- .replace(/\s+/g, " ")
622
- .trim();
623
- }
624
- if (ch === '"') {
625
- let j = i + 1;
626
- while (j < body.length && body[j] !== '"') j++;
627
- return body
628
- .slice(i + 1, j)
629
- .replace(/\s+/g, " ")
630
- .trim();
631
- }
632
- // bare value (e.g. year = 2017,) — up to the next comma / newline / close brace
633
- let j = i;
634
- while (j < body.length && !/[,\n}]/.test(body[j])) j++;
635
- const v = body.slice(i, j).replace(/\s+/g, " ").trim();
636
- return v || undefined;
637
- }
638
-
639
- export function parseBib(text) {
640
- const cites = [];
641
- // @type{key, — then read the entry body with BALANCED-BRACE scanning so that
642
- // one-line entries and indented / same-line closing braces (`\n }` or `...}`)
643
- // are captured, not dropped by a `\n}`-at-column-0 requirement (S4c).
644
- const headRe = /@(\w+)\s*\{\s*([^,\s}]+)\s*,/g;
645
- let m;
646
- while ((m = headRe.exec(text)) !== null) {
647
- const type = m[1].toLowerCase();
648
- if (type === "comment" || type === "string" || type === "preamble")
649
- continue;
650
- const key = m[2].trim();
651
- // Balanced scan from just after the key's comma to the entry's closing brace.
652
- let depth = 1; // the entry's opening `{` was already consumed by headRe
653
- let j = headRe.lastIndex;
654
- for (; j < text.length && depth > 0; j++) {
655
- if (text[j] === "{") depth++;
656
- else if (text[j] === "}") depth--;
657
- }
658
- const body = text.slice(headRe.lastIndex, j - 1);
659
- headRe.lastIndex = j; // resume scanning AFTER this entry
660
-
661
- const c = { id: key };
662
- const doi = bibField(body, "doi");
663
- const title = bibField(body, "title");
664
- const year = bibField(body, "year");
665
- const author = bibField(body, "author");
666
- const eprint = bibField(body, "eprint");
667
- const archive =
668
- bibField(body, "archiveprefix") || bibField(body, "eprinttype");
669
- if (doi) c.doi = normalizeDoi(doi);
670
- if (title) c.title = title.replace(/[{}]/g, "");
671
- if (year) c.year = year;
672
- if (author) c.authors = author;
673
- if (eprint && (!archive || /arxiv/i.test(archive)))
674
- c.arxiv = normalizeArxiv(eprint);
675
- cites.push(c);
676
- }
677
- if (cites.length > 0) return cites;
678
-
679
- // Fallback: \bibitem — grab arXiv ids / DOIs out of each block, best-effort.
680
- const itemRe =
681
- /\\bibitem(?:\[[^\]]*\])?\{([^}]+)\}([\s\S]*?)(?=\\bibitem|\\end\{thebibliography\}|$)/g;
682
- while ((m = itemRe.exec(text)) !== null) {
683
- const key = m[1].trim();
684
- const block = m[2];
685
- const c = { id: key };
686
- const doiM = block.match(/10\.\d{4,9}\/[-._;()/:A-Z0-9]+/i);
687
- const arxivM = block.match(/arXiv:\s*(\d{4}\.\d{4,5})/i);
688
- const yearM = block.match(/\b(19|20)\d{2}\b/);
689
- if (doiM) c.doi = normalizeDoi(doiM[0]);
690
- if (arxivM) c.arxiv = normalizeArxiv(arxivM[1]);
691
- if (yearM) c.year = yearM[0];
692
- // crude title: longest quoted or {\em ...} run
693
- const titleM =
694
- block.match(/``([^']{6,}?)''/) ||
695
- block.match(/\\emph\{([^}]{6,}?)\}/) ||
696
- block.match(/\{\\em\s+([^}]{6,}?)\}/);
697
- if (titleM) c.title = titleM[1].replace(/\s+/g, " ").trim();
698
- cites.push(c);
699
- }
700
- return cites;
609
+ export function citationsOf(entries) {
610
+ return entries.map((e) => {
611
+ const f = e.fields;
612
+ const c = { id: e.key };
613
+ if (f.doi) c.doi = normalizeDoi(f.doi);
614
+ if (f.title) c.title = f.title.replace(/\s+/g, " ").trim();
615
+ if (f.year) c.year = f.year;
616
+ const authors = (e.names.author ?? []).map(writtenName).join(" and ");
617
+ if (authors) c.authors = authors;
618
+ const archive = f.archiveprefix || f.eprinttype;
619
+ if (f.eprint && (!archive || /arxiv/i.test(archive)))
620
+ c.arxiv = normalizeArxiv(f.eprint);
621
+ return c;
622
+ });
701
623
  }
702
624
 
703
625
  // ─────────────────────────────────────────────────────────────────────────────
@@ -1212,11 +1134,23 @@ export async function verifyCitationLive(
1212
1134
  // CLI
1213
1135
  // ─────────────────────────────────────────────────────────────────────────────
1214
1136
 
1137
+ /** What `bibliographyAt` needs to read a `.bib` (src/paper-sources.ts, the one answer for a path). */
1138
+ const DEPS = {
1139
+ files: nodeFiles,
1140
+ codec: sourcesCodec,
1141
+ latex: latexReader,
1142
+ bib: bibReader,
1143
+ };
1144
+
1215
1145
  function loadInput(argv) {
1216
1146
  const path = argv.find((a) => !a.startsWith("-"));
1147
+ if (path && extname(path).toLowerCase() === ".bib") {
1148
+ const r = bibliographyAt(absolutePath(resolve(path)), DEPS);
1149
+ if (!r.ok) throw new Error(bibliographyUnreadWhy(r.error));
1150
+ return citationsOf(r.value.texts.flatMap((t) => t.entries));
1151
+ }
1217
1152
  const raw =
1218
1153
  path && path !== "-" ? readFileSync(path, "utf8") : readFileSync(0, "utf8"); // stdin
1219
- if (path && extname(path).toLowerCase() === ".bib") return parseBib(raw);
1220
1154
  const parsed = JSON.parse(raw);
1221
1155
  return Array.isArray(parsed) ? parsed : [parsed];
1222
1156
  }
@@ -24,7 +24,7 @@ import {
24
24
  checkNvd,
25
25
  checkCommit,
26
26
  reduceVerdict,
27
- parseBib,
27
+ citationsOf,
28
28
  parseArxivFeed,
29
29
  normalizeDoi,
30
30
  normalizeArxiv,
@@ -33,6 +33,12 @@ import {
33
33
  titleIdentity,
34
34
  isValidCveId,
35
35
  } from "./verify-cites.mjs";
36
+ import { bibReader } from "#src/adapters/bibtex/index";
37
+ import { absolutePath } from "#src/domain/paths";
38
+
39
+ /** The citations of a BibTeX text, read by the one bibtex reader. */
40
+ const citationsIn = (bib) =>
41
+ citationsOf(bibReader.readFile(absolutePath("/p/refs.bib"), bib).entries);
36
42
 
37
43
  /** One vitest case per check, named by it. */
38
44
  function ok(cond, name) {
@@ -377,7 +383,7 @@ console.log("\n(extra) .bib parser best-effort extraction:");
377
383
  eprint={1512.03385}
378
384
  }
379
385
  `;
380
- const cites = parseBib(bib);
386
+ const cites = citationsIn(bib);
381
387
  eq(cites.length, 2, "parsed 2 entries");
382
388
  eq(cites[0].doi, "10.5555/3295222.3295349", "entry 1 doi");
383
389
  eq(cites[1].arxiv, "1512.03385", "entry 2 arxiv eprint");
@@ -724,7 +730,7 @@ console.log("\n[S4] .bib parser edge cases:");
724
730
  title = {The Real Paper Title},
725
731
  year = {2021}
726
732
  }`;
727
- const a = parseBib(bibA)[0];
733
+ const a = citationsIn(bibA)[0];
728
734
  eq(a.title, "The Real Paper Title", "title anchored — not booktitle");
729
735
 
730
736
  // (b) brace-protected acronym must not truncate at the inner brace.
@@ -733,14 +739,14 @@ console.log("\n[S4] .bib parser edge cases:");
733
739
  year = {2026}
734
740
  }`;
735
741
  eq(
736
- parseBib(bibB)[0].title,
742
+ citationsIn(bibB)[0].title,
737
743
  "Evaluating LLM-based Systems for Code Review",
738
744
  "brace-protected acronym kept whole",
739
745
  );
740
746
 
741
747
  // (c1) one-line entry (no \n} at column 0).
742
748
  const bibC1 = `@article{k3, title = {A One Line Entry}, year = {2020}, doi = {10.1/abc}}`;
743
- const c1 = parseBib(bibC1)[0];
749
+ const c1 = citationsIn(bibC1)[0];
744
750
  eq(c1.title, "A One Line Entry", "one-line entry title parsed");
745
751
  eq(c1.doi, "10.1/abc", "one-line entry doi parsed");
746
752
 
@@ -750,14 +756,14 @@ console.log("\n[S4] .bib parser edge cases:");
750
756
  year = {2019}
751
757
  }`;
752
758
  eq(
753
- parseBib(bibC2)[0].title,
759
+ citationsIn(bibC2)[0].title,
754
760
  "Indented Closing Brace",
755
761
  "indented-closing-brace entry parsed",
756
762
  );
757
763
 
758
764
  // all four parse together, in sequence (balanced-brace scan resumes correctly).
759
765
  eq(
760
- parseBib(bibA + "\n" + bibB + "\n" + bibC1 + "\n" + bibC2).length,
766
+ citationsIn(bibA + "\n" + bibB + "\n" + bibC1 + "\n" + bibC2).length,
761
767
  4,
762
768
  "all 4 entries parsed in sequence",
763
769
  );
@@ -796,15 +802,6 @@ console.log("\n[S4] .bib parser edge cases:");
796
802
  });
797
803
  eq(n.doi, "10.1/y", "normalizeIdentifiers cleans doi");
798
804
  eq(n.arxiv, "1234.5678", "normalizeIdentifiers cleans arxiv");
799
- // the exact live-reproduced break: a \bibitem DOI with a swallowed sentence period.
800
- const bib = `\\begin{thebibliography}{9}
801
- \\bibitem{ccs23} A. Author. Title. In Proc. CCS. doi:10.1145/3576915.3623218. 2023.
802
- \\end{thebibliography}`;
803
- eq(
804
- parseBib(bib)[0].doi,
805
- "10.1145/3576915.3623218",
806
- "bibitem DOI trailing period not swallowed",
807
- );
808
805
  }
809
806
 
810
807
  // ── Edges of the pure layer: the defensive arms, each with the input that reaches it ──────────
@@ -916,32 +913,22 @@ test("reduceVerdict: an arXiv id missed only by a non-authoritative source is un
916
913
  });
917
914
  });
918
915
 
919
- test("parseBib: @comment/@string/@preamble are skipped, a bare value is read, an empty one is absent", () => {
916
+ test("citationsOf: @comment/@string/@preamble are no citations; a quoted and a bare value are read; authors written out", () => {
920
917
  const bib = [
921
918
  "@comment{c, not an entry}",
922
- "@string{s, x = {y}}",
923
- '@article{k1, title = "Quoted Title", year = 2017, eprint = {1706.03762}, archiveprefix = {arXiv}}',
924
- "@misc{k2, title = {T}, year = ,}",
925
- ].join("\n");
926
- assert.deepEqual(parseBib(bib), [
927
- { id: "k1", title: "Quoted Title", year: "2017", arxiv: "1706.03762" },
928
- { id: "k2", title: "T" },
929
- ]);
930
- });
931
-
932
- test("parseBib's \\bibitem fallback reads an arXiv id and an \\emph title", () => {
933
- const text = [
934
- "\\begin{thebibliography}{9}",
935
- "\\bibitem{a} A. Author. \\emph{A Long Enough Title}. arXiv:1706.03762, 2017.",
936
- "\\end{thebibliography}",
919
+ "@string{s = {y}}",
920
+ '@article{k1, title = "Quoted Title", year = 2017, eprint = {1706.03762}, archiveprefix = {arXiv}, author = {Vaswani, Ashish and von Neumann, John}}',
921
+ "@misc{k2, title = {T}, eprint = {x}, archiveprefix = {HAL}}",
937
922
  ].join("\n");
938
- assert.deepEqual(parseBib(text), [
923
+ assert.deepEqual(citationsIn(bib), [
939
924
  {
940
- id: "a",
941
- arxiv: "1706.03762",
925
+ id: "k1",
926
+ title: "Quoted Title",
942
927
  year: "2017",
943
- title: "A Long Enough Title",
928
+ authors: "Ashish Vaswani and John von Neumann",
929
+ arxiv: "1706.03762",
944
930
  },
931
+ { id: "k2", title: "T" },
945
932
  ]);
946
933
  });
947
934
 
package/src/CLAUDE.md CHANGED
@@ -1,9 +1,15 @@
1
- # CLAUDE.md — `src/`
1
+ <!-- vigiles:sha256:f51051aae2ef09f8 compiled from src/CLAUDE.md.spec.ts -->
2
+
3
+ # CLAUDE.md
4
+
5
+ ## `src/`
2
6
 
3
7
  The package's TypeScript: the CLI, the build, the toolchain installer, and the measuring that feeds
4
8
  the facts file. Loaded when working under `src/`; the repository-wide rules are in the root
5
9
  `CLAUDE.md`.
6
10
 
11
+ ## The two layer axes
12
+
7
13
  **`src/` is hexagonal on two axes, and the linter enforces both** (`eslint.config.mjs`; fixtures in
8
14
  `test/fixtures/layers/`, run by `test/eslint-layers.test.ts`; issue #76).
9
15
 
@@ -49,3 +55,19 @@ against `process.cwd()`, so lint started from any other directory classifies not
49
55
  `checkAllOrigins: true`: without it no `module:` policy is evaluated, so axis B and the library bans
50
56
  report nothing — dropping it turns the fixtures `src/domain/reads-disk.ts` and
51
57
  `src/adapters/one/disk.ts` clean (measured).
58
+
59
+ ## Readers and commands
60
+
61
+ `@retorquere/bibtex-parser` is the one `.bib` reader, behind `src/ports/bib-reader.ts`.
62
+ Rejected on 2026-09-27, so not proposed again: a separate `paperlint refs` command; the slowness it
63
+ would have moved was a cache thrown away after every build (`docs/references.md`).
64
+
65
+ ## Rules
66
+
67
+ ### Layers
68
+
69
+ **Enforced by:** `eslint/boundaries/dependencies`
70
+
71
+ ### Effect Globals
72
+
73
+ **Enforced by:** `eslint/no-restricted-globals`