paperlint 8.0.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 (356) 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 +40 -24
  57. package/dist/build.js.map +1 -1
  58. package/dist/claim-provenance.d.ts +2 -2
  59. package/dist/cli.d.ts +6 -1
  60. package/dist/cli.d.ts.map +1 -1
  61. package/dist/cli.js +30 -26
  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/paper-context.d.ts.map +1 -1
  80. package/dist/eslint-rules/paper-context.js +4 -3
  81. package/dist/eslint-rules/paper-context.js.map +1 -1
  82. package/dist/eslint-rules/paper-typography.d.ts +0 -9
  83. package/dist/eslint-rules/paper-typography.d.ts.map +1 -1
  84. package/dist/eslint-rules/paper-typography.js +12 -5
  85. package/dist/eslint-rules/paper-typography.js.map +1 -1
  86. package/dist/latex-log.d.ts +10 -0
  87. package/dist/latex-log.d.ts.map +1 -1
  88. package/dist/latex-log.js +10 -0
  89. package/dist/latex-log.js.map +1 -1
  90. package/dist/latex-loop.d.ts +2 -0
  91. package/dist/latex-loop.d.ts.map +1 -1
  92. package/dist/latex-loop.js.map +1 -1
  93. package/dist/package-dirs.d.ts.map +1 -1
  94. package/dist/package-dirs.js +3 -2
  95. package/dist/package-dirs.js.map +1 -1
  96. package/dist/paper-includes.d.ts +4 -7
  97. package/dist/paper-includes.d.ts.map +1 -1
  98. package/dist/paper-includes.js +29 -21
  99. package/dist/paper-includes.js.map +1 -1
  100. package/dist/paper-prose.d.ts +2 -2
  101. package/dist/paper-record.d.ts +25 -0
  102. package/dist/paper-record.d.ts.map +1 -0
  103. package/dist/paper-record.js +39 -0
  104. package/dist/paper-record.js.map +1 -0
  105. package/dist/paper-sources.d.ts +66 -0
  106. package/dist/paper-sources.d.ts.map +1 -0
  107. package/dist/paper-sources.js +92 -0
  108. package/dist/paper-sources.js.map +1 -0
  109. package/dist/ports/bib-reader.d.ts +18 -0
  110. package/dist/ports/bib-reader.d.ts.map +1 -0
  111. package/dist/ports/bib-reader.js +2 -0
  112. package/dist/ports/bib-reader.js.map +1 -0
  113. package/dist/ports/check-references.d.ts +9 -4
  114. package/dist/ports/check-references.d.ts.map +1 -1
  115. package/dist/ports/latex.d.ts +15 -0
  116. package/dist/ports/latex.d.ts.map +1 -1
  117. package/dist/ports/sources-codec.d.ts +11 -0
  118. package/dist/ports/sources-codec.d.ts.map +1 -0
  119. package/dist/ports/sources-codec.js +2 -0
  120. package/dist/ports/sources-codec.js.map +1 -0
  121. package/dist/ports/tex-output.d.ts +17 -0
  122. package/dist/ports/tex-output.d.ts.map +1 -0
  123. package/dist/ports/tex-output.js +2 -0
  124. package/dist/ports/tex-output.js.map +1 -0
  125. package/dist/recorded-bibliography.d.ts +89 -0
  126. package/dist/recorded-bibliography.d.ts.map +1 -0
  127. package/dist/recorded-bibliography.js +136 -0
  128. package/dist/recorded-bibliography.js.map +1 -0
  129. package/dist/reference-rules.d.ts +5 -5
  130. package/dist/reference-rules.d.ts.map +1 -1
  131. package/dist/reference-rules.js +150 -77
  132. package/dist/reference-rules.js.map +1 -1
  133. package/dist/references.d.ts +71 -16
  134. package/dist/references.d.ts.map +1 -1
  135. package/dist/references.js +110 -26
  136. package/dist/references.js.map +1 -1
  137. package/dist/register-bands.d.ts +3 -3
  138. package/dist/register-bands.d.ts.map +1 -1
  139. package/dist/register-bands.js.map +1 -1
  140. package/dist/register.d.ts +2 -2
  141. package/dist/sources-record.d.ts +38 -0
  142. package/dist/sources-record.d.ts.map +1 -0
  143. package/dist/sources-record.js +123 -0
  144. package/dist/sources-record.js.map +1 -0
  145. package/dist/sources-rules.d.ts +44 -0
  146. package/dist/sources-rules.d.ts.map +1 -0
  147. package/dist/sources-rules.js +76 -0
  148. package/dist/sources-rules.js.map +1 -0
  149. package/dist/tex-paper.d.ts +10 -15
  150. package/dist/tex-paper.d.ts.map +1 -1
  151. package/dist/tex-paper.js +24 -40
  152. package/dist/tex-paper.js.map +1 -1
  153. package/dist/tex-venue-rules.d.ts +3 -3
  154. package/dist/tex-venue-rules.d.ts.map +1 -1
  155. package/dist/tex-venue-rules.js.map +1 -1
  156. package/docs/configuration.md +12 -4
  157. package/docs/design/paper-sources.md +197 -0
  158. package/docs/package-shape-options.md +1 -1
  159. package/docs/references.md +34 -8
  160. package/docs/rules/bib/reachable-entry.md +64 -0
  161. package/docs/rules/paper/sources-fresh.md +81 -0
  162. package/docs/rules.md +62 -57
  163. package/eslint-rules/paper-context.ts +4 -3
  164. package/eslint-rules/paper-typography.test.mjs +12 -72
  165. package/eslint-rules/paper-typography.ts +13 -6
  166. package/eslint.config.mjs +22 -0
  167. package/fixtures/build-e2e/aidc/paper.tex +1 -1
  168. package/fixtures/build-e2e/anon-ack/paper.tex +1 -1
  169. package/fixtures/build-e2e/anon-author/paper.tex +1 -1
  170. package/fixtures/build-e2e/anon-bib/paper.tex +1 -1
  171. package/fixtures/build-e2e/anon-href/paper.tex +1 -1
  172. package/fixtures/build-e2e/anon-macro/paper.tex +1 -1
  173. package/fixtures/build-e2e/anon-pdfauthor/paper.tex +1 -1
  174. package/fixtures/paper-sources/p1/paper.tex +30 -0
  175. package/fixtures/paper-sources/p1/refs.bib +6 -0
  176. package/fixtures/paper-sources/p1/sections/dead.tex +6 -0
  177. package/fixtures/paper-sources/p1/sections/intro.tex +7 -0
  178. package/fixtures/paper-sources/p1/sections/parked.tex +6 -0
  179. package/fixtures/paper-sources/p1/tex-truth.json +37 -0
  180. package/fixtures/paper-sources/p1/versions/2026-01-01-review.tex +4 -0
  181. package/fixtures/paper-sources/v1-stale/paper.tex +14 -0
  182. package/fixtures/paper-sources/v1-stale/refs.bib +6 -0
  183. package/fixtures/paper-sources/v1-stale/tex-truth.json +32 -0
  184. package/fixtures/paper-sources/v10-ifthenelse/anon.bib +1 -0
  185. package/fixtures/paper-sources/v10-ifthenelse/paper.tex +9 -0
  186. package/fixtures/paper-sources/v10-ifthenelse/real.bib +1 -0
  187. package/fixtures/paper-sources/v10-ifthenelse/tex-truth.json +28 -0
  188. package/fixtures/paper-sources/v11-two-blocks/paper.tex +12 -0
  189. package/fixtures/paper-sources/v11-two-blocks/tex-truth.json +30 -0
  190. package/fixtures/paper-sources/v13-percent-text/paper.tex +6 -0
  191. package/fixtures/paper-sources/v13-percent-text/refs.bib +3 -0
  192. package/fixtures/paper-sources/v13-percent-text/tex-truth.json +30 -0
  193. package/fixtures/paper-sources/v15-after-end/paper.tex +6 -0
  194. package/fixtures/paper-sources/v15-after-end/refs.bib +1 -0
  195. package/fixtures/paper-sources/v15-after-end/tex-truth.json +17 -0
  196. package/fixtures/paper-sources/v16-included-block/bibblock.tex +3 -0
  197. package/fixtures/paper-sources/v16-included-block/paper.tex +7 -0
  198. package/fixtures/paper-sources/v16-included-block/refs.bib +1 -0
  199. package/fixtures/paper-sources/v16-included-block/tex-truth.json +35 -0
  200. package/fixtures/paper-sources/v17-include-order/bibsetup.tex +1 -0
  201. package/fixtures/paper-sources/v17-include-order/first.bib +1 -0
  202. package/fixtures/paper-sources/v17-include-order/later.bib +1 -0
  203. package/fixtures/paper-sources/v17-include-order/paper.tex +7 -0
  204. package/fixtures/paper-sources/v17-include-order/tex-truth.json +40 -0
  205. package/fixtures/paper-sources/v2-overwrite/paper.tex +14 -0
  206. package/fixtures/paper-sources/v2-overwrite/refs.bib +6 -0
  207. package/fixtures/paper-sources/v2-overwrite/tex-truth.json +30 -0
  208. package/fixtures/paper-sources/v22-parked-thebibliography/paper.tex +7 -0
  209. package/fixtures/paper-sources/v22-parked-thebibliography/tex-truth.json +17 -0
  210. package/fixtures/paper-sources/v23-parked-include/old.bib +1 -0
  211. package/fixtures/paper-sources/v23-parked-include/paper.tex +7 -0
  212. package/fixtures/paper-sources/v23-parked-include/parked.tex +1 -0
  213. package/fixtures/paper-sources/v23-parked-include/refs.bib +1 -0
  214. package/fixtures/paper-sources/v23-parked-include/tex-truth.json +28 -0
  215. package/fixtures/paper-sources/v24-quoted-fields/paper.tex +6 -0
  216. package/fixtures/paper-sources/v24-quoted-fields/refs.bib +5 -0
  217. package/fixtures/paper-sources/v24-quoted-fields/tex-truth.json +32 -0
  218. package/fixtures/paper-sources/v25-string-differs/paper.tex +10 -0
  219. package/fixtures/paper-sources/v25-string-differs/refs.bib +2 -0
  220. package/fixtures/paper-sources/v25-string-differs/tex-truth.json +32 -0
  221. package/fixtures/paper-sources/v26-comment-in-names/one.bib +1 -0
  222. package/fixtures/paper-sources/v26-comment-in-names/paper.tex +7 -0
  223. package/fixtures/paper-sources/v26-comment-in-names/tex-truth.json +32 -0
  224. package/fixtures/paper-sources/v26-comment-in-names/two.bib +1 -0
  225. package/fixtures/paper-sources/v27-shared-string/abbreviations.bib +2 -0
  226. package/fixtures/paper-sources/v27-shared-string/paper.tex +6 -0
  227. package/fixtures/paper-sources/v27-shared-string/references.bib +6 -0
  228. package/fixtures/paper-sources/v27-shared-string/tex-truth.json +31 -0
  229. package/fixtures/paper-sources/v28-shared-string-block/abbreviations.bib +2 -0
  230. package/fixtures/paper-sources/v28-shared-string-block/paper.tex +14 -0
  231. package/fixtures/paper-sources/v28-shared-string-block/tex-truth.json +33 -0
  232. package/fixtures/paper-sources/v29-generated-input/paper.tex +9 -0
  233. package/fixtures/paper-sources/v29-generated-input/tex-truth.json +24 -0
  234. package/fixtures/paper-sources/v3-declared/paper.bib +6 -0
  235. package/fixtures/paper-sources/v3-declared/paper.tex +6 -0
  236. package/fixtures/paper-sources/v3-declared/tex-truth.json +28 -0
  237. package/fixtures/paper-sources/v30-name-next-line/paper.tex +14 -0
  238. package/fixtures/paper-sources/v30-name-next-line/tex-truth.json +30 -0
  239. package/fixtures/paper-sources/v4-commented/paper.tex +14 -0
  240. package/fixtures/paper-sources/v4-commented/refs.bib +6 -0
  241. package/fixtures/paper-sources/v4-commented/tex-truth.json +28 -0
  242. package/fixtures/paper-sources/v5-percent-entry/paper.tex +20 -0
  243. package/fixtures/paper-sources/v5-percent-entry/tex-truth.json +37 -0
  244. package/fixtures/paper-sources/v6-unclosed/paper.tex +6 -0
  245. package/fixtures/paper-sources/v6-unclosed/refs.bib +5 -0
  246. package/fixtures/paper-sources/v6-unclosed/tex-truth.json +37 -0
  247. package/fixtures/paper-sources/v8-jobname/paper.tex +9 -0
  248. package/fixtures/paper-sources/v8-jobname/tex-truth.json +30 -0
  249. package/fixtures/paper-sources/v9-two-bibliographies/one.bib +1 -0
  250. package/fixtures/paper-sources/v9-two-bibliographies/paper.tex +7 -0
  251. package/fixtures/paper-sources/v9-two-bibliographies/tex-truth.json +35 -0
  252. package/fixtures/paper-sources/v9-two-bibliographies/two.bib +1 -0
  253. package/fixtures/paper-typography/clean-paper/paper.tex +1 -1
  254. package/fixtures/paper-typography/messy-paper/paper.tex +1 -1
  255. package/package.json +2 -10
  256. package/scripts/check.test.ts +23 -1
  257. package/scripts/check.ts +7 -3
  258. package/scripts/coverage-config.test.ts +19 -10
  259. package/scripts/layer-legacy-frozen.test.ts +41 -2
  260. package/scripts/layer-legacy-frozen.ts +31 -2
  261. package/skills/grade-paper-writing/SKILL.md +4 -3
  262. package/skills/grade-paper-writing/SKILL.md.spec.ts +3 -2
  263. package/skills/grade-paper-writing/prose-lint.mjs +11 -2
  264. package/skills/grade-paper-writing/prose-lint.test.ts +52 -4
  265. package/skills/paper-pipeline/scripts/extract-ref-facts.harness.mjs +83 -33
  266. package/skills/paper-pipeline/scripts/extract-ref-facts.mjs +67 -115
  267. package/skills/paper-pipeline/scripts/extract-ref-facts.test.mjs +128 -121
  268. package/skills/verify-citations/SKILL.md +7 -4
  269. package/skills/verify-citations/SKILL.md.spec.ts +6 -3
  270. package/skills/verify-citations/scripts/bib-authors.check.test.mjs +17 -19
  271. package/skills/verify-citations/scripts/bib-authors.cli.test.ts +129 -46
  272. package/skills/verify-citations/scripts/bib-authors.d.mts +22 -2
  273. package/skills/verify-citations/scripts/bib-authors.mjs +57 -111
  274. package/skills/verify-citations/scripts/verify-cites.cli.test.ts +16 -0
  275. package/skills/verify-citations/scripts/verify-cites.d.mts +19 -3
  276. package/skills/verify-citations/scripts/verify-cites.mjs +42 -108
  277. package/skills/verify-citations/scripts/verify-cites.test.mjs +23 -36
  278. package/src/adapters/bibtex/index.test.ts +163 -0
  279. package/src/adapters/bibtex/index.ts +155 -0
  280. package/src/adapters/latex/filecontents.ts +114 -0
  281. package/src/adapters/latex/includes.ts +18 -10
  282. package/src/adapters/latex/index.test.ts +73 -0
  283. package/src/adapters/latex/index.ts +2 -0
  284. package/src/adapters/latex/nodes.ts +64 -0
  285. package/src/adapters/latex/parse.test.ts +38 -0
  286. package/src/adapters/latex/parse.ts +67 -6
  287. package/src/adapters/memory/index.ts +17 -0
  288. package/src/adapters/references/index.test.ts +75 -21
  289. package/src/adapters/references/index.ts +37 -32
  290. package/src/adapters/sources-record/index.test.ts +87 -0
  291. package/src/adapters/sources-record/index.ts +82 -0
  292. package/src/adapters/tex-output/blg.ts +27 -0
  293. package/src/adapters/tex-output/fls.ts +24 -0
  294. package/src/adapters/tex-output/index.test.ts +171 -0
  295. package/src/adapters/tex-output/index.ts +12 -0
  296. package/src/bib-rules.test.ts +356 -0
  297. package/src/bib-rules.ts +228 -0
  298. package/src/bibliography-at.test.ts +286 -0
  299. package/src/build.test.ts +210 -0
  300. package/src/build.ts +100 -35
  301. package/src/claim-provenance.test.ts +33 -7
  302. package/src/claim-provenance.ts +2 -2
  303. package/src/cli.harness.mjs +20 -10
  304. package/src/cli.test.ts +14 -10
  305. package/src/cli.ts +37 -28
  306. package/src/domain/paper-source.test.ts +58 -20
  307. package/src/domain/paper-source.ts +38 -42
  308. package/src/domain/paper-sources.ts +94 -0
  309. package/src/domain/sources-record.test.ts +71 -0
  310. package/src/domain/sources-record.ts +136 -0
  311. package/src/domain/tex-run.test.ts +228 -0
  312. package/src/domain/tex-run.ts +139 -0
  313. package/src/heading-case.test.ts +24 -10
  314. package/src/latex-log.ts +20 -0
  315. package/src/latex-loop.harness.ts +1 -0
  316. package/src/latex-loop.ts +2 -0
  317. package/src/linted-set.test.ts +9 -1
  318. package/src/new-paper.harness.mjs +3 -2
  319. package/src/new-paper.test.ts +9 -5
  320. package/src/package-dirs.ts +3 -2
  321. package/src/paper-includes.test.ts +157 -38
  322. package/src/paper-includes.ts +39 -35
  323. package/src/paper-prose.test.ts +34 -5
  324. package/src/paper-prose.ts +2 -2
  325. package/src/paper-record.test.ts +173 -0
  326. package/src/paper-record.ts +66 -0
  327. package/src/paper-settings-commands.test.ts +5 -2
  328. package/src/paper-sources.ts +151 -0
  329. package/src/ports/bib-reader.ts +27 -0
  330. package/src/ports/check-references.ts +7 -3
  331. package/src/ports/latex.ts +19 -0
  332. package/src/ports/sources-codec.ts +11 -0
  333. package/src/ports/tex-output.ts +17 -0
  334. package/src/recorded-bibliography.ts +297 -0
  335. package/src/reference-rules.ts +217 -104
  336. package/src/references.test.ts +520 -177
  337. package/src/references.ts +185 -37
  338. package/src/register-bands.test.ts +5 -1
  339. package/src/register-bands.ts +3 -5
  340. package/src/register.test.ts +6 -1
  341. package/src/register.ts +2 -2
  342. package/src/rule-cases.test.ts +68 -12
  343. package/src/rule-docs.test.ts +0 -1
  344. package/src/sources-record.test.ts +308 -0
  345. package/src/sources-record.ts +231 -0
  346. package/src/sources-rules.test.ts +152 -0
  347. package/src/sources-rules.ts +117 -0
  348. package/src/tex-paper.test.ts +148 -78
  349. package/src/tex-paper.ts +31 -61
  350. package/src/tex-venue-rules.test.ts +36 -8
  351. package/src/tex-venue-rules.ts +3 -5
  352. package/dist/eslint-rules/bib-reachable-entry.d.ts +0 -22
  353. package/dist/eslint-rules/bib-reachable-entry.d.ts.map +0 -1
  354. package/dist/eslint-rules/bib-reachable-entry.js +0 -75
  355. package/dist/eslint-rules/bib-reachable-entry.js.map +0 -1
  356. package/eslint-rules/bib-reachable-entry.ts +0 -76
@@ -4,7 +4,7 @@ description: Verify every citation is a real work with correct metadata, and tha
4
4
  allowed-tools: [Read, Write, Edit, Grep, Glob, Bash, WebSearch, WebFetch, Agent]
5
5
  ---
6
6
 
7
- <!-- vigiles:sha256:faf8d55c8ddb82ed compiled from skills/verify-citations/SKILL.md.spec.ts -->
7
+ <!-- vigiles:sha256:fdb023ed7c613e1b compiled from skills/verify-citations/SKILL.md.spec.ts -->
8
8
 
9
9
  # verify-citations — every cite real, the delta explicit
10
10
 
@@ -165,10 +165,13 @@ heavier than a typo, and it is exactly the class §"any regalia about third part
165
165
 
166
166
  **How to run.**
167
167
  ```
168
- node scripts/bib-authors.mjs <paper-dir> # or a .bib, or a .tex with a filecontents block
168
+ node scripts/bib-authors.mjs <paper-dir> # the databases bibtex opened in the last build; or a .bib, or a .tex
169
169
  node scripts/bib-authors.mjs <paper-dir> --json
170
170
  ```
171
- Exits **1** on any author-set or author-order disagreement, **0** when clean, **2** on usage/IO error.
171
+ Exits **1** on any author-set or author-order disagreement, **0** when clean, **2** on usage/IO error. A paper
172
+ directory or a `.tex` is read through the build's record (`_build/sources.json`): a paper that was never built,
173
+ or changed since, is refused (exit 2) and told to run `npx paperlint build` first — the databases are TeX's answer,
174
+ not a reading of the source. A `.bib` named is read as itself.
172
175
 
173
176
  **Source is DBLP, and that choice is the evidence.** DBLP indexes the proceedings record and the CoRR record
174
177
  **separately**, so a single query returns both lists side by side — the cleanest proof available that the
@@ -244,7 +247,7 @@ It used to be one script, `verify-refs.mjs`; the measurement half stayed a scrip
244
247
  became twelve ESLint rules over the facts it writes.
245
248
 
246
249
  ```
247
- # 1. measure: parse the bibliography, ask CrossRef/arXiv, write _build/refs.facts.json
250
+ # 1. measure: parse the bibliography bibtex opened (build the paper first), ask CrossRef/arXiv, write _build/refs.facts.json
248
251
  node .claude/skills/paper-pipeline/scripts/extract-ref-facts.mjs <paper-dir>
249
252
  node .claude/skills/paper-pipeline/scripts/extract-ref-facts.mjs <paper-dir> --offline # cached responses only
250
253
 
@@ -181,10 +181,13 @@ heavier than a typo, and it is exactly the class §"any regalia about third part
181
181
 
182
182
  **How to run.**
183
183
  \`\`\`
184
- node scripts/bib-authors.mjs <paper-dir> # or a .bib, or a .tex with a filecontents block
184
+ node scripts/bib-authors.mjs <paper-dir> # the databases bibtex opened in the last build; or a .bib, or a .tex
185
185
  node scripts/bib-authors.mjs <paper-dir> --json
186
186
  \`\`\`
187
- Exits **1** on any author-set or author-order disagreement, **0** when clean, **2** on usage/IO error.
187
+ Exits **1** on any author-set or author-order disagreement, **0** when clean, **2** on usage/IO error. A paper
188
+ directory or a \`.tex\` is read through the build's record (\`_build/sources.json\`): a paper that was never built,
189
+ or changed since, is refused (exit 2) and told to run \`npx paperlint build\` first — the databases are TeX's answer,
190
+ not a reading of the source. A \`.bib\` named is read as itself.
188
191
 
189
192
  **Source is DBLP, and that choice is the evidence.** DBLP indexes the proceedings record and the CoRR record
190
193
  **separately**, so a single query returns both lists side by side — the cleanest proof available that the
@@ -260,7 +263,7 @@ It used to be one script, \`verify-refs.mjs\`; the measurement half stayed a scr
260
263
  became twelve ESLint rules over the facts it writes.
261
264
 
262
265
  \`\`\`
263
- # 1. measure: parse the bibliography, ask CrossRef/arXiv, write _build/refs.facts.json
266
+ # 1. measure: parse the bibliography bibtex opened (build the paper first), ask CrossRef/arXiv, write _build/refs.facts.json
264
267
  node .claude/skills/paper-pipeline/scripts/extract-ref-facts.mjs <paper-dir>
265
268
  node .claude/skills/paper-pipeline/scripts/extract-ref-facts.mjs <paper-dir> --offline # cached responses only
266
269
 
@@ -13,9 +13,15 @@ import {
13
13
  checkAuthors,
14
14
  claimsPublished,
15
15
  dblpHits,
16
- parseBib,
16
+ authorEntries,
17
17
  surnames,
18
18
  } from "./bib-authors.mjs";
19
+ import { bibReader } from "#src/adapters/bibtex/index";
20
+ import { absolutePath } from "#src/domain/paths";
21
+
22
+ /** This check's entries of a BibTeX text, read by the one bibtex reader. */
23
+ const entriesIn = (bib) =>
24
+ authorEntries(bibReader.readFile(absolutePath("/p/refs.bib"), bib).entries);
19
25
 
20
26
  afterEach(() => vi.unstubAllGlobals());
21
27
 
@@ -215,35 +221,27 @@ test("dblpHits: no hits is an empty list; 429 is retryable; other errors are not
215
221
  );
216
222
  });
217
223
 
218
- test("parseBib reads quoted and bare field values, and a field that is absent", () => {
224
+ test("authorEntries: names written family first (`and others` kept), a quoted value, a field that is absent", () => {
219
225
  assert.deepEqual(
220
- parseBib(
221
- '@article{k, author = "Ada Lovelace", title = {T}, journal = J, }',
226
+ entriesIn(
227
+ '@article{k, author = "Ada Lovelace and van der Berg, Jan and others", title = {The {BERT} Model}, journal = {J}}\n@misc{untitled, author = {Hopper, Grace}}',
222
228
  ),
223
229
  [
224
230
  {
225
231
  type: "article",
226
232
  key: "k",
227
- author: "Ada Lovelace",
228
- title: "T",
233
+ author: "Lovelace, Ada and van der Berg, Jan and others",
234
+ title: "The BERT Model",
229
235
  booktitle: "",
230
236
  journal: "J",
231
237
  },
232
- ],
233
- );
234
- });
235
-
236
- test("parseBib: braces nested inside a value, and a bare last value with no comma after it", () => {
237
- assert.deepEqual(
238
- parseBib("@article{k, title = {The {BERT} Model}, journal = J}"),
239
- [
240
238
  {
241
- type: "article",
242
- key: "k",
243
- author: "",
244
- title: "The {BERT} Model",
239
+ type: "misc",
240
+ key: "untitled",
241
+ author: "Hopper, Grace",
242
+ title: "",
245
243
  booktitle: "",
246
- journal: "J",
244
+ journal: "",
247
245
  },
248
246
  ],
249
247
  );
@@ -1,7 +1,7 @@
1
1
  /**
2
- * bib-authors.mjs as a process: which file it picks from a paper directory, the bibliography
3
- * sources it reads (.bib, a .tex's filecontents or its sibling refs.bib), the file it refuses, and
4
- * the report and exit code for each outcome. DBLP is a preload that answers
2
+ * bib-authors.mjs as a process: the bibliography it reads (a .bib named, or what the last build's
3
+ * bibtex opened for a paper directory or a .tex — `_build/sources.json`, through `bibliographyAt`), the
4
+ * targets it refuses, and the report and exit code for each outcome. DBLP is a preload that answers
5
5
  * by title from a table (the documented `result.hits.hit[].info` shape) — no network.
6
6
  */
7
7
  import assert from "node:assert/strict";
@@ -15,6 +15,13 @@ import {
15
15
  useTempDir,
16
16
  writeTree,
17
17
  } from "../../../test/support.ts";
18
+ import {
19
+ plantedOnDisk,
20
+ recordOnDisk,
21
+ type Shape,
22
+ } from "../../../test/recorded-fixture.ts";
23
+ import { absolutePath } from "#src/domain/paths";
24
+ import { bibliographyUnreadWhy } from "#src/paper-sources";
18
25
 
19
26
  const SCRIPT = join(dirname(fileURLToPath(import.meta.url)), "bib-authors.mjs");
20
27
  const root = useTempDir("bib-authors-cli-");
@@ -32,6 +39,10 @@ const bib = (
32
39
  ) =>
33
40
  `@inproceedings{${key}, author = {${author}}, title = {${title}}, ${venue}}\n`;
34
41
 
42
+ /** A .tex that declares the database `name`, with `preamble` before its document. */
43
+ const declaring = (name: string, preamble = "") =>
44
+ `\\documentclass{article}\n${preamble}\\begin{document}x\\bibliography{${name}}\\end{document}\n`;
45
+
35
46
  writeTree(root, {
36
47
  // The retry backoff (1.5 s, then 3 s) and the 900 ms courtesy pause are real sleeps in the
37
48
  // script. Under CPU load they made "DBLP down" take 11.7 s once and time out. The preload
@@ -46,24 +57,53 @@ writeTree(root, {
46
57
  " const hit = (answers[q] ?? []).map((authors) => ({ info: { title: q, venue: 'X', year: '2024',\n" +
47
58
  " type: 'Conference and Workshop Papers', authors: { author: authors.map((text) => ({ text })) } } }));\n" +
48
59
  " return new Response(JSON.stringify({ result: { hits: { hit } } }), { status: 200 });\n};\n",
60
+ "findings/paper.tex": declaring("paper"),
49
61
  "findings/paper.bib":
50
62
  bib("dropped", "Ada Lovelace and Grace Hopper", "Dropped Author") +
51
63
  bib("order", "Alan Turing and Ada Lovelace", "Swapped Order"),
52
64
  "findings/other.bib": bib("never", "X", "Never Read"),
53
65
  "twotex/b.tex": "\\documentclass{article}\n",
54
- "twotex/a.tex":
55
- "\\begin{filecontents*}{refs.bib}\n" +
56
- bib("pre", "Ada Lovelace", "A Preprint", "journal = {arXiv preprint}") +
57
- "\\end{filecontents*}\n",
58
- "sibling/paper.tex": "\\documentclass{article}\n",
66
+ "twotex/paper.tex": declaring(
67
+ "refs",
68
+ "\\begin{filecontents*}[overwrite]{refs.bib}\n" +
69
+ bib("pre", "Ada Lovelace", "A Preprint", "journal = {arXiv preprint}") +
70
+ "\\end{filecontents*}\n",
71
+ ),
72
+ // What TeX wrote from that block: the file bibtex opened.
73
+ "twotex/refs.bib": bib(
74
+ "pre",
75
+ "Ada Lovelace",
76
+ "A Preprint",
77
+ "journal = {arXiv preprint}",
78
+ ),
79
+ "sibling/paper.tex": declaring("refs"),
59
80
  "sibling/refs.bib": bib("good", "Ada Lovelace", "All Good"),
60
- "nobib/paper.tex": "\\documentclass{article}\n",
81
+ "etal/paper.tex": declaring("refs"),
61
82
  "etal/notes.txt": "not a bibliography\n",
62
83
  "etal/refs.bib":
63
84
  bib("good", "Ada Lovelace", "All Good") +
64
85
  bib("etal", "Ada Lovelace and others", "Truncated"),
86
+ "down/paper.tex": declaring("refs"),
65
87
  "down/refs.bib": bib("down", "Ada Lovelace", "Service Down"),
66
- "empty/.keep": "",
88
+ });
89
+
90
+ /** The record of a paper whose bibliography is `db`, a file the author keeps. */
91
+ const keeps = (db: string): Shape => ({
92
+ inputs: [
93
+ ["paper.tex", "body"],
94
+ [db, "preamble"],
95
+ ],
96
+ databases: [db],
97
+ });
98
+ recordOnDisk(join(root, "findings"), keeps("paper.bib"));
99
+ recordOnDisk(join(root, "sibling"), keeps("refs.bib"));
100
+ recordOnDisk(join(root, "etal"), keeps("refs.bib"));
101
+ recordOnDisk(join(root, "down"), keeps("refs.bib"));
102
+ // A block TeX wrote into refs.bib: the file is no input, the block is.
103
+ recordOnDisk(join(root, "twotex"), {
104
+ inputs: [["paper.tex", "body"]],
105
+ written: ["refs.bib"],
106
+ databases: ["refs.bib"],
67
107
  });
68
108
 
69
109
  const run = (...args: string[]) => {
@@ -77,14 +117,16 @@ const run = (...args: string[]) => {
77
117
  };
78
118
  };
79
119
 
80
- test("refusals: no argument, no such path, a directory with nothing to read, a .tex with no bibliography", () => {
120
+ test("refusals: no argument; a path with no bibliography to read, in bibliographyAt's words", () => {
121
+ // The path → bibliography vocabulary is tested once, beside bibliographyAt (src/paper-sources.ts).
122
+ // Here: that the command speaks it, and exits 2.
123
+ const notes = join(root, "etal", "notes.txt");
81
124
  assert.deepEqual(
82
- [
83
- run(),
84
- run(join(root, "nope")),
85
- run(join(root, "empty")),
86
- run(join(root, "nobib", "paper.tex")),
87
- ].map(({ status, stdout, stderr }) => ({ status, stdout, stderr })),
125
+ [run(), run(notes)].map(({ status, stdout, stderr }) => ({
126
+ status,
127
+ stdout,
128
+ stderr,
129
+ })),
88
130
  [
89
131
  {
90
132
  status: 2,
@@ -95,24 +137,13 @@ test("refusals: no argument, no such path, a directory with nothing to read, a .
95
137
  {
96
138
  status: 2,
97
139
  stdout: "",
98
- stderr: "bib-authors: no such path: <root>/nope\n",
99
- },
100
- {
101
- status: 2,
102
- stdout: "",
103
- stderr: "bib-authors: no .bib or .tex in <root>/empty\n",
104
- },
105
- {
106
- status: 2,
107
- stdout: "",
108
- stderr:
109
- "bib-authors: no bibliography found in <root>/nobib/paper.tex (no filecontents block, no refs.bib beside it)\n",
140
+ stderr: `bib-authors: ${bibliographyUnreadWhy({ kind: "not-bib-or-tex", path: absolutePath(notes) }).replaceAll(root, "<root>")}\n`,
110
141
  },
111
142
  ],
112
143
  );
113
144
  });
114
145
 
115
- test("a paper directory: the canonical paper.bib is read, and each difference is printed — FAIL, exit 1", () => {
146
+ test("a paper directory: the paper.bib its \\bibliography{paper} declares is read, and each difference is printed — FAIL, exit 1", () => {
116
147
  assert.deepEqual(run(join(root, "findings")), {
117
148
  status: 1,
118
149
  stderr: "",
@@ -135,16 +166,16 @@ test("a paper directory: the canonical paper.bib is read, and each difference is
135
166
  });
136
167
  });
137
168
 
138
- test("two .tex files and no paper.tex: the first by name, said out loud; its filecontents is read", () => {
139
- const r = run(join(root, "twotex"), "--json");
169
+ test("a .tex named is its folder's paper: the database bibtex opened is read from the block that wrote it", () => {
170
+ const r = run(join(root, "twotex", "paper.tex"), "--json");
140
171
  assert.deepEqual(
141
172
  { status: r.status, stderr: r.stderr, report: parseJson(r.stdout) },
142
173
  {
143
174
  status: 0,
144
- stderr:
145
- "⚠️ <root>/twotex holds several .tex files and no canonical paper.tex: a.tex, b.tex — took a.tex. If that is the wrong one, name the file explicitly.\n",
175
+ stderr: "",
146
176
  report: {
147
- file: "<root>/twotex/a.tex",
177
+ file: "<root>/twotex/paper.tex",
178
+ files: ["<root>/twotex/paper.tex"],
148
179
  entries: 1,
149
180
  findings: [],
150
181
  skipped: [],
@@ -154,7 +185,18 @@ test("two .tex files and no paper.tex: the first by name, said out loud; its fil
154
185
  );
155
186
  });
156
187
 
157
- test("a .tex with no filecontents reads the refs.bib beside it — PASS, exit 0", () => {
188
+ test("a .bib named is read as itself", () => {
189
+ const r = run(join(root, "findings", "paper.bib"), "--json");
190
+ const report: unknown = parseJson(r.stdout);
191
+ assert.deepEqual(
192
+ report !== null && typeof report === "object" && "files" in report
193
+ ? report.files
194
+ : report,
195
+ ["<root>/findings/paper.bib"],
196
+ );
197
+ });
198
+
199
+ test("a .tex that declares refs and has no block reads refs.bib — PASS, exit 0", () => {
158
200
  const r = run(join(root, "sibling", "paper.tex"));
159
201
  assert.deepEqual(r, {
160
202
  status: 0,
@@ -164,16 +206,6 @@ test("a .tex with no filecontents reads the refs.bib beside it — PASS, exit 0"
164
206
  });
165
207
  });
166
208
 
167
- test("a file that is neither a .bib nor a .tex is refused — not swapped for the refs.bib beside it", () => {
168
- const r = run(join(root, "etal", "notes.txt"));
169
- assert.deepEqual(r, {
170
- status: 2,
171
- stdout: "",
172
- stderr:
173
- "bib-authors: <root>/etal/notes.txt is not a .bib or a .tex — give the paper's .bib or paper.tex\n",
174
- });
175
- });
176
-
177
209
  test("an `and others` entry is printed as not applicable, not dropped", () => {
178
210
  const r = run(join(root, "etal"));
179
211
  assert.deepEqual(r, {
@@ -198,3 +230,54 @@ test("DBLP down: NOT CHECKED, PARTIAL, exit 2 — never a pass", () => {
198
230
  "PARTIAL: no disagreement among the entries reached, but 1 could not be checked — this is NOT a pass.\n",
199
231
  });
200
232
  });
233
+
234
+ // The planted papers of fixtures/paper-sources, laid out as a build leaves them: the file read is the
235
+ // one bibtex opened (tex-truth.json), or the block that wrote it.
236
+ test.each([
237
+ ["v1-stale", "refs.bib"],
238
+ ["v2-overwrite", "paper.tex"],
239
+ ["v3-declared", "paper.bib"],
240
+ ["v4-commented", "refs.bib"],
241
+ ])("%s: reads %s", (paper, file) => {
242
+ const dir = join(root, "planted", paper);
243
+ plantedOnDisk(dir, paper);
244
+ const r = runNode(SCRIPT, [dir, "--json"], {
245
+ nodeArgs: ["--import", join(root, "dblp.mjs")],
246
+ });
247
+ const report: unknown = parseJson(r.stdout);
248
+ assert.deepEqual(
249
+ report !== null && typeof report === "object" && "files" in report
250
+ ? report.files
251
+ : report,
252
+ [join(dir, file)],
253
+ );
254
+ });
255
+
256
+ test("🔴 a paper that was not built is refused — run `npx paperlint build` first — and so is one changed since", () => {
257
+ const unbuilt = join(root, "unbuilt");
258
+ writeTree(unbuilt, {
259
+ "paper.tex": declaring("refs"),
260
+ "refs.bib": bib("good", "Ada Lovelace", "All Good"),
261
+ });
262
+ assert.deepEqual(
263
+ [run(unbuilt), run(join(unbuilt, "paper.tex"))].map(
264
+ ({ status, stdout, stderr }) => ({ status, stdout, stderr }),
265
+ ),
266
+ Array.from({ length: 2 }, () => ({
267
+ status: 2,
268
+ stdout: "",
269
+ stderr:
270
+ "bib-authors: <root>/unbuilt has not been built — run `npx paperlint build` first, which records the databases bibtex reads\n",
271
+ })),
272
+ );
273
+ recordOnDisk(unbuilt, keeps("refs.bib"));
274
+ writeTree(unbuilt, {
275
+ "refs.bib": bib("good", "Ada Lovelace", "All Good Edited"),
276
+ });
277
+ assert.deepEqual(run(unbuilt), {
278
+ status: 2,
279
+ stdout: "",
280
+ stderr:
281
+ "bib-authors: <root>/unbuilt changed since the last build (refs.bib edited) — run `npx paperlint build` first\n",
282
+ });
283
+ });
@@ -1,7 +1,7 @@
1
1
  // Types for bib-authors.mjs, for the exports `src/` imports — the same arrangement as
2
2
  // `verify-cites.d.mts` beside it. Only what `src/` imports is declared.
3
3
 
4
- /** One entry as `parseBib` returns it; a field the entry lacks is "". */
4
+ /** One entry as `authorEntries` returns it; a field the entry lacks is "". */
5
5
  export interface BibEntry {
6
6
  readonly type: string;
7
7
  readonly key: string;
@@ -11,7 +11,27 @@ export interface BibEntry {
11
11
  readonly journal: string;
12
12
  }
13
13
 
14
- export declare function parseBib(text: string): BibEntry[];
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 (src/domain/paper-sources.ts), as far as this check uses it. */
24
+ export interface ReadEntry {
25
+ readonly type: string;
26
+ readonly key: string;
27
+ readonly fields: Readonly<Record<string, string>>;
28
+ readonly names: Readonly<Record<string, readonly ReadName[]>>;
29
+ }
30
+
31
+ /** The entries this check reads, from the bibtex reader's. */
32
+ export declare function authorEntries(
33
+ entries: readonly ReadEntry[],
34
+ ): BibEntry[];
15
35
 
16
36
  /** True when the author list ends in `and others` — there is nothing complete to compare. */
17
37
  export declare const truncated: (authorField: string) => boolean;
@@ -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(