paperlint 7.2.0 → 9.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (474) hide show
  1. package/CLAUDE.md +9 -25
  2. package/CONTRIBUTING.md +5 -4
  3. package/README.md +4 -3
  4. package/dist/adapters/bibtex/index.d.ts +3 -0
  5. package/dist/adapters/bibtex/index.d.ts.map +1 -0
  6. package/dist/adapters/bibtex/index.js +103 -0
  7. package/dist/adapters/bibtex/index.js.map +1 -0
  8. package/dist/adapters/latex/filecontents.d.ts +33 -0
  9. package/dist/adapters/latex/filecontents.d.ts.map +1 -0
  10. package/dist/adapters/latex/filecontents.js +65 -0
  11. package/dist/adapters/latex/filecontents.js.map +1 -0
  12. package/dist/adapters/latex/includes.d.ts +3 -3
  13. package/dist/adapters/latex/includes.d.ts.map +1 -1
  14. package/dist/adapters/latex/includes.js +18 -10
  15. package/dist/adapters/latex/includes.js.map +1 -1
  16. package/dist/adapters/latex/index.d.ts.map +1 -1
  17. package/dist/adapters/latex/index.js +2 -0
  18. package/dist/adapters/latex/index.js.map +1 -1
  19. package/dist/adapters/latex/nodes.d.ts +12 -0
  20. package/dist/adapters/latex/nodes.d.ts.map +1 -1
  21. package/dist/adapters/latex/nodes.js +39 -0
  22. package/dist/adapters/latex/nodes.js.map +1 -1
  23. package/dist/adapters/latex/parse.d.ts +7 -1
  24. package/dist/adapters/latex/parse.d.ts.map +1 -1
  25. package/dist/adapters/latex/parse.js +55 -6
  26. package/dist/adapters/latex/parse.js.map +1 -1
  27. package/dist/adapters/memory/index.d.ts +3 -0
  28. package/dist/adapters/memory/index.d.ts.map +1 -1
  29. package/dist/adapters/memory/index.js +9 -0
  30. package/dist/adapters/memory/index.js.map +1 -1
  31. package/dist/adapters/references/index.d.ts.map +1 -1
  32. package/dist/adapters/references/index.js +27 -24
  33. package/dist/adapters/references/index.js.map +1 -1
  34. package/dist/adapters/sources-record/index.d.ts +9 -0
  35. package/dist/adapters/sources-record/index.d.ts.map +1 -0
  36. package/dist/adapters/sources-record/index.js +68 -0
  37. package/dist/adapters/sources-record/index.js.map +1 -0
  38. package/dist/adapters/tex-output/blg.d.ts +9 -0
  39. package/dist/adapters/tex-output/blg.d.ts.map +1 -0
  40. package/dist/adapters/tex-output/blg.js +20 -0
  41. package/dist/adapters/tex-output/blg.js.map +1 -0
  42. package/dist/adapters/tex-output/fls.d.ts +8 -0
  43. package/dist/adapters/tex-output/fls.d.ts.map +1 -0
  44. package/dist/adapters/tex-output/fls.js +16 -0
  45. package/dist/adapters/tex-output/fls.js.map +1 -0
  46. package/dist/adapters/tex-output/index.d.ts +7 -0
  47. package/dist/adapters/tex-output/index.d.ts.map +1 -0
  48. package/dist/adapters/tex-output/index.js +7 -0
  49. package/dist/adapters/tex-output/index.js.map +1 -0
  50. package/dist/bib-rules.d.ts +47 -0
  51. package/dist/bib-rules.d.ts.map +1 -0
  52. package/dist/bib-rules.js +127 -0
  53. package/dist/bib-rules.js.map +1 -0
  54. package/dist/build.d.ts +29 -1
  55. package/dist/build.d.ts.map +1 -1
  56. package/dist/build.js +41 -30
  57. package/dist/build.js.map +1 -1
  58. package/dist/claim-provenance.d.ts +2 -2
  59. package/dist/cli.d.ts +9 -5
  60. package/dist/cli.d.ts.map +1 -1
  61. package/dist/cli.js +43 -68
  62. package/dist/cli.js.map +1 -1
  63. package/dist/domain/paper-source.d.ts +1 -10
  64. package/dist/domain/paper-source.d.ts.map +1 -1
  65. package/dist/domain/paper-source.js +34 -24
  66. package/dist/domain/paper-source.js.map +1 -1
  67. package/dist/domain/paper-sources.d.ts +72 -0
  68. package/dist/domain/paper-sources.d.ts.map +1 -0
  69. package/dist/domain/paper-sources.js +18 -0
  70. package/dist/domain/paper-sources.js.map +1 -0
  71. package/dist/domain/sources-record.d.ts +102 -0
  72. package/dist/domain/sources-record.d.ts.map +1 -0
  73. package/dist/domain/sources-record.js +38 -0
  74. package/dist/domain/sources-record.js.map +1 -0
  75. package/dist/domain/tex-run.d.ts +25 -0
  76. package/dist/domain/tex-run.d.ts.map +1 -0
  77. package/dist/domain/tex-run.js +79 -0
  78. package/dist/domain/tex-run.js.map +1 -0
  79. package/dist/eslint-rules/latex-language.d.ts.map +1 -1
  80. package/dist/eslint-rules/latex-language.js +10 -13
  81. package/dist/eslint-rules/latex-language.js.map +1 -1
  82. package/dist/eslint-rules/paper-context.d.ts.map +1 -1
  83. package/dist/eslint-rules/paper-context.js +6 -5
  84. package/dist/eslint-rules/paper-context.js.map +1 -1
  85. package/dist/eslint-rules/paper-typography.d.ts +0 -9
  86. package/dist/eslint-rules/paper-typography.d.ts.map +1 -1
  87. package/dist/eslint-rules/paper-typography.js +34 -96
  88. package/dist/eslint-rules/paper-typography.js.map +1 -1
  89. package/dist/eslint-rules/papers.d.ts +1 -3
  90. package/dist/eslint-rules/papers.d.ts.map +1 -1
  91. package/dist/eslint-rules/papers.js +2 -4
  92. package/dist/eslint-rules/papers.js.map +1 -1
  93. package/dist/eslint-rules/tex-build.d.ts.map +1 -1
  94. package/dist/eslint-rules/tex-build.js +8 -25
  95. package/dist/eslint-rules/tex-build.js.map +1 -1
  96. package/dist/init.d.ts +1 -4
  97. package/dist/init.d.ts.map +1 -1
  98. package/dist/init.js +3 -9
  99. package/dist/init.js.map +1 -1
  100. package/dist/latex-log.d.ts +10 -0
  101. package/dist/latex-log.d.ts.map +1 -1
  102. package/dist/latex-log.js +10 -0
  103. package/dist/latex-log.js.map +1 -1
  104. package/dist/latex-loop.d.ts +2 -0
  105. package/dist/latex-loop.d.ts.map +1 -1
  106. package/dist/latex-loop.js.map +1 -1
  107. package/dist/lib/markdown.d.ts +5 -81
  108. package/dist/lib/markdown.d.ts.map +1 -1
  109. package/dist/lib/markdown.js +5 -116
  110. package/dist/lib/markdown.js.map +1 -1
  111. package/dist/lib/paper-config.d.ts.map +1 -1
  112. package/dist/lib/paper-config.js +0 -2
  113. package/dist/lib/paper-config.js.map +1 -1
  114. package/dist/lib/skill-eval-fixture.d.ts +6 -4
  115. package/dist/lib/skill-eval-fixture.d.ts.map +1 -1
  116. package/dist/lib/skill-eval-fixture.js +25 -18
  117. package/dist/lib/skill-eval-fixture.js.map +1 -1
  118. package/dist/new-paper.d.ts +5 -11
  119. package/dist/new-paper.d.ts.map +1 -1
  120. package/dist/new-paper.js +12 -23
  121. package/dist/new-paper.js.map +1 -1
  122. package/dist/package-dirs.d.ts.map +1 -1
  123. package/dist/package-dirs.js +3 -2
  124. package/dist/package-dirs.js.map +1 -1
  125. package/dist/paper-files.d.ts +4 -4
  126. package/dist/paper-includes.d.ts +4 -7
  127. package/dist/paper-includes.d.ts.map +1 -1
  128. package/dist/paper-includes.js +29 -21
  129. package/dist/paper-includes.js.map +1 -1
  130. package/dist/paper-prose.d.ts +17 -0
  131. package/dist/paper-prose.d.ts.map +1 -0
  132. package/dist/paper-prose.js +46 -0
  133. package/dist/paper-prose.js.map +1 -0
  134. package/dist/paper-record.d.ts +25 -0
  135. package/dist/paper-record.d.ts.map +1 -0
  136. package/dist/paper-record.js +39 -0
  137. package/dist/paper-record.js.map +1 -0
  138. package/dist/paper-sources.d.ts +66 -0
  139. package/dist/paper-sources.d.ts.map +1 -0
  140. package/dist/paper-sources.js +92 -0
  141. package/dist/paper-sources.js.map +1 -0
  142. package/dist/ports/bib-reader.d.ts +18 -0
  143. package/dist/ports/bib-reader.d.ts.map +1 -0
  144. package/dist/ports/bib-reader.js +2 -0
  145. package/dist/ports/bib-reader.js.map +1 -0
  146. package/dist/ports/check-references.d.ts +9 -4
  147. package/dist/ports/check-references.d.ts.map +1 -1
  148. package/dist/ports/latex.d.ts +15 -0
  149. package/dist/ports/latex.d.ts.map +1 -1
  150. package/dist/ports/sources-codec.d.ts +11 -0
  151. package/dist/ports/sources-codec.d.ts.map +1 -0
  152. package/dist/ports/sources-codec.js +2 -0
  153. package/dist/ports/sources-codec.js.map +1 -0
  154. package/dist/ports/tex-output.d.ts +17 -0
  155. package/dist/ports/tex-output.d.ts.map +1 -0
  156. package/dist/ports/tex-output.js +2 -0
  157. package/dist/ports/tex-output.js.map +1 -0
  158. package/dist/recorded-bibliography.d.ts +89 -0
  159. package/dist/recorded-bibliography.d.ts.map +1 -0
  160. package/dist/recorded-bibliography.js +136 -0
  161. package/dist/recorded-bibliography.js.map +1 -0
  162. package/dist/reference-rules.d.ts +5 -5
  163. package/dist/reference-rules.d.ts.map +1 -1
  164. package/dist/reference-rules.js +150 -77
  165. package/dist/reference-rules.js.map +1 -1
  166. package/dist/references.d.ts +71 -16
  167. package/dist/references.d.ts.map +1 -1
  168. package/dist/references.js +110 -26
  169. package/dist/references.js.map +1 -1
  170. package/dist/register-bands.d.ts +3 -3
  171. package/dist/register-bands.d.ts.map +1 -1
  172. package/dist/register-bands.js.map +1 -1
  173. package/dist/register.d.ts +2 -2
  174. package/dist/sources-record.d.ts +38 -0
  175. package/dist/sources-record.d.ts.map +1 -0
  176. package/dist/sources-record.js +123 -0
  177. package/dist/sources-record.js.map +1 -0
  178. package/dist/sources-rules.d.ts +44 -0
  179. package/dist/sources-rules.d.ts.map +1 -0
  180. package/dist/sources-rules.js +76 -0
  181. package/dist/sources-rules.js.map +1 -0
  182. package/dist/structure.d.ts.map +1 -1
  183. package/dist/structure.js +4 -3
  184. package/dist/structure.js.map +1 -1
  185. package/dist/tex-paper.d.ts +10 -15
  186. package/dist/tex-paper.d.ts.map +1 -1
  187. package/dist/tex-paper.js +24 -40
  188. package/dist/tex-paper.js.map +1 -1
  189. package/dist/tex-venue-rules.d.ts +3 -3
  190. package/dist/tex-venue-rules.d.ts.map +1 -1
  191. package/dist/tex-venue-rules.js.map +1 -1
  192. package/dist/types.d.ts +0 -2
  193. package/dist/types.d.ts.map +1 -1
  194. package/docs/configuration.md +18 -12
  195. package/docs/design/paper-sources.md +197 -0
  196. package/docs/e2e.md +4 -6
  197. package/docs/package-shape-options.md +1 -1
  198. package/docs/prior-art/paper-folder-scaffolding.md +24 -29
  199. package/docs/references.md +34 -8
  200. package/docs/rules/bib/reachable-entry.md +64 -0
  201. package/docs/rules/paper/sources-fresh.md +81 -0
  202. package/docs/rules/tex/claim-provenance.md +0 -1
  203. package/docs/rules.md +62 -61
  204. package/eslint-rules/latex-language.harness.mjs +3 -3
  205. package/eslint-rules/latex-language.ts +10 -13
  206. package/eslint-rules/paper-context.ts +6 -5
  207. package/eslint-rules/paper-typography.test.mjs +29 -157
  208. package/eslint-rules/paper-typography.ts +35 -104
  209. package/eslint-rules/papers.harness.mjs +1 -1
  210. package/eslint-rules/papers.ts +2 -4
  211. package/eslint-rules/tex-build.harness.mjs +2 -2
  212. package/eslint-rules/tex-build.ts +8 -25
  213. package/eslint.config.mjs +22 -1
  214. package/fixtures/build-e2e/aidc/paper.tex +1 -1
  215. package/fixtures/build-e2e/anon-ack/paper.tex +1 -1
  216. package/fixtures/build-e2e/anon-author/paper.tex +1 -1
  217. package/fixtures/build-e2e/anon-bib/paper.tex +1 -1
  218. package/fixtures/build-e2e/anon-href/paper.tex +1 -1
  219. package/fixtures/build-e2e/anon-macro/paper.tex +1 -1
  220. package/fixtures/build-e2e/anon-pdfauthor/paper.tex +1 -1
  221. package/fixtures/paper-sources/p1/paper.tex +30 -0
  222. package/fixtures/paper-sources/p1/refs.bib +6 -0
  223. package/fixtures/paper-sources/p1/sections/dead.tex +6 -0
  224. package/fixtures/paper-sources/p1/sections/intro.tex +7 -0
  225. package/fixtures/paper-sources/p1/sections/parked.tex +6 -0
  226. package/fixtures/paper-sources/p1/tex-truth.json +37 -0
  227. package/fixtures/paper-sources/p1/versions/2026-01-01-review.tex +4 -0
  228. package/fixtures/paper-sources/v1-stale/paper.tex +14 -0
  229. package/fixtures/paper-sources/v1-stale/refs.bib +6 -0
  230. package/fixtures/paper-sources/v1-stale/tex-truth.json +32 -0
  231. package/fixtures/paper-sources/v10-ifthenelse/anon.bib +1 -0
  232. package/fixtures/paper-sources/v10-ifthenelse/paper.tex +9 -0
  233. package/fixtures/paper-sources/v10-ifthenelse/real.bib +1 -0
  234. package/fixtures/paper-sources/v10-ifthenelse/tex-truth.json +28 -0
  235. package/fixtures/paper-sources/v11-two-blocks/paper.tex +12 -0
  236. package/fixtures/paper-sources/v11-two-blocks/tex-truth.json +30 -0
  237. package/fixtures/paper-sources/v13-percent-text/paper.tex +6 -0
  238. package/fixtures/paper-sources/v13-percent-text/refs.bib +3 -0
  239. package/fixtures/paper-sources/v13-percent-text/tex-truth.json +30 -0
  240. package/fixtures/paper-sources/v15-after-end/paper.tex +6 -0
  241. package/fixtures/paper-sources/v15-after-end/refs.bib +1 -0
  242. package/fixtures/paper-sources/v15-after-end/tex-truth.json +17 -0
  243. package/fixtures/paper-sources/v16-included-block/bibblock.tex +3 -0
  244. package/fixtures/paper-sources/v16-included-block/paper.tex +7 -0
  245. package/fixtures/paper-sources/v16-included-block/refs.bib +1 -0
  246. package/fixtures/paper-sources/v16-included-block/tex-truth.json +35 -0
  247. package/fixtures/paper-sources/v17-include-order/bibsetup.tex +1 -0
  248. package/fixtures/paper-sources/v17-include-order/first.bib +1 -0
  249. package/fixtures/paper-sources/v17-include-order/later.bib +1 -0
  250. package/fixtures/paper-sources/v17-include-order/paper.tex +7 -0
  251. package/fixtures/paper-sources/v17-include-order/tex-truth.json +40 -0
  252. package/fixtures/paper-sources/v2-overwrite/paper.tex +14 -0
  253. package/fixtures/paper-sources/v2-overwrite/refs.bib +6 -0
  254. package/fixtures/paper-sources/v2-overwrite/tex-truth.json +30 -0
  255. package/fixtures/paper-sources/v22-parked-thebibliography/paper.tex +7 -0
  256. package/fixtures/paper-sources/v22-parked-thebibliography/tex-truth.json +17 -0
  257. package/fixtures/paper-sources/v23-parked-include/old.bib +1 -0
  258. package/fixtures/paper-sources/v23-parked-include/paper.tex +7 -0
  259. package/fixtures/paper-sources/v23-parked-include/parked.tex +1 -0
  260. package/fixtures/paper-sources/v23-parked-include/refs.bib +1 -0
  261. package/fixtures/paper-sources/v23-parked-include/tex-truth.json +28 -0
  262. package/fixtures/paper-sources/v24-quoted-fields/paper.tex +6 -0
  263. package/fixtures/paper-sources/v24-quoted-fields/refs.bib +5 -0
  264. package/fixtures/paper-sources/v24-quoted-fields/tex-truth.json +32 -0
  265. package/fixtures/paper-sources/v25-string-differs/paper.tex +10 -0
  266. package/fixtures/paper-sources/v25-string-differs/refs.bib +2 -0
  267. package/fixtures/paper-sources/v25-string-differs/tex-truth.json +32 -0
  268. package/fixtures/paper-sources/v26-comment-in-names/one.bib +1 -0
  269. package/fixtures/paper-sources/v26-comment-in-names/paper.tex +7 -0
  270. package/fixtures/paper-sources/v26-comment-in-names/tex-truth.json +32 -0
  271. package/fixtures/paper-sources/v26-comment-in-names/two.bib +1 -0
  272. package/fixtures/paper-sources/v27-shared-string/abbreviations.bib +2 -0
  273. package/fixtures/paper-sources/v27-shared-string/paper.tex +6 -0
  274. package/fixtures/paper-sources/v27-shared-string/references.bib +6 -0
  275. package/fixtures/paper-sources/v27-shared-string/tex-truth.json +31 -0
  276. package/fixtures/paper-sources/v28-shared-string-block/abbreviations.bib +2 -0
  277. package/fixtures/paper-sources/v28-shared-string-block/paper.tex +14 -0
  278. package/fixtures/paper-sources/v28-shared-string-block/tex-truth.json +33 -0
  279. package/fixtures/paper-sources/v29-generated-input/paper.tex +9 -0
  280. package/fixtures/paper-sources/v29-generated-input/tex-truth.json +24 -0
  281. package/fixtures/paper-sources/v3-declared/paper.bib +6 -0
  282. package/fixtures/paper-sources/v3-declared/paper.tex +6 -0
  283. package/fixtures/paper-sources/v3-declared/tex-truth.json +28 -0
  284. package/fixtures/paper-sources/v30-name-next-line/paper.tex +14 -0
  285. package/fixtures/paper-sources/v30-name-next-line/tex-truth.json +30 -0
  286. package/fixtures/paper-sources/v4-commented/paper.tex +14 -0
  287. package/fixtures/paper-sources/v4-commented/refs.bib +6 -0
  288. package/fixtures/paper-sources/v4-commented/tex-truth.json +28 -0
  289. package/fixtures/paper-sources/v5-percent-entry/paper.tex +20 -0
  290. package/fixtures/paper-sources/v5-percent-entry/tex-truth.json +37 -0
  291. package/fixtures/paper-sources/v6-unclosed/paper.tex +6 -0
  292. package/fixtures/paper-sources/v6-unclosed/refs.bib +5 -0
  293. package/fixtures/paper-sources/v6-unclosed/tex-truth.json +37 -0
  294. package/fixtures/paper-sources/v8-jobname/paper.tex +9 -0
  295. package/fixtures/paper-sources/v8-jobname/tex-truth.json +30 -0
  296. package/fixtures/paper-sources/v9-two-bibliographies/one.bib +1 -0
  297. package/fixtures/paper-sources/v9-two-bibliographies/paper.tex +7 -0
  298. package/fixtures/paper-sources/v9-two-bibliographies/tex-truth.json +35 -0
  299. package/fixtures/paper-sources/v9-two-bibliographies/two.bib +1 -0
  300. package/fixtures/paper-typography/clean-paper/paper.tex +1 -1
  301. package/fixtures/paper-typography/messy-paper/paper.tex +1 -1
  302. package/hooks/hooks.harness.ts +15 -15
  303. package/hooks/paper-edit-guard.hook.mjs +5 -8
  304. package/hooks/paper-edit-guard.test.ts +12 -3
  305. package/hooks/paper-skills-nudge.hook.mjs +2 -3
  306. package/hooks/paper-skills-nudge.test.ts +13 -3
  307. package/hooks/paper-status-gates.hook.mjs +2 -2
  308. package/hooks/paper-status-gates.test.ts +14 -2
  309. package/lib/markdown.test.mjs +5 -60
  310. package/lib/markdown.ts +9 -136
  311. package/lib/paper-config.ts +0 -2
  312. package/lib/skill-checks.test.mjs +4 -1
  313. package/lib/skill-eval-fixture.ts +25 -18
  314. package/package.json +2 -12
  315. package/presets/realm.jsonc +1 -1
  316. package/scripts/check.test.ts +23 -1
  317. package/scripts/check.ts +7 -3
  318. package/scripts/coverage-config.test.ts +19 -10
  319. package/scripts/harness-api.frozen.json +0 -5
  320. package/scripts/layer-legacy-frozen.test.ts +41 -2
  321. package/scripts/layer-legacy-frozen.ts +31 -2
  322. package/skills/cold-read-diff/SKILL.md +3 -3
  323. package/skills/cold-read-diff/SKILL.md.spec.ts +2 -2
  324. package/skills/grade-paper-writing/SKILL.md +15 -22
  325. package/skills/grade-paper-writing/SKILL.md.spec.ts +14 -21
  326. package/skills/grade-paper-writing/__snapshots__/prose-lint.test.ts.snap +40 -26
  327. package/skills/grade-paper-writing/prose-lint.mjs +117 -278
  328. package/skills/grade-paper-writing/prose-lint.test.ts +230 -79
  329. package/skills/paper-adversarial-review/SKILL.md +2 -2
  330. package/skills/paper-adversarial-review/SKILL.md.spec.ts +1 -1
  331. package/skills/paper-pipeline/SKILL.md +9 -12
  332. package/skills/paper-pipeline/SKILL.md.spec.ts +8 -11
  333. package/skills/paper-pipeline/description-language.eval.mjs +25 -15
  334. package/skills/paper-pipeline/framing-vs-vocabulary.eval.mjs +25 -15
  335. package/skills/paper-pipeline/grade-paper-writing-ablation.eval.mjs +25 -15
  336. package/skills/paper-pipeline/pipeline-firing.eval.mjs +27 -16
  337. package/skills/paper-pipeline/pipeline-language.eval.mjs +25 -15
  338. package/skills/paper-pipeline/references/acceptance-gate.md +16 -20
  339. package/skills/paper-pipeline/references/artifact-checklist.md +4 -5
  340. package/skills/paper-pipeline/references/pipeline-status-template.md +1 -1
  341. package/skills/paper-pipeline/scripts/README.md +0 -4
  342. package/skills/paper-pipeline/scripts/extract-ref-facts.harness.mjs +91 -105
  343. package/skills/paper-pipeline/scripts/extract-ref-facts.mjs +85 -241
  344. package/skills/paper-pipeline/scripts/extract-ref-facts.test.mjs +166 -153
  345. package/skills/paper-pipeline/scripts/generated-code.harness.mjs +3 -3
  346. package/skills/paper-status/SKILL.md +2 -2
  347. package/skills/paper-status/SKILL.md.spec.ts +1 -1
  348. package/skills/render-paper/SKILL.md +2 -2
  349. package/skills/render-paper/SKILL.md.spec.ts +1 -1
  350. package/skills/render-paper/extract-pdf-facts.mjs +2 -2
  351. package/skills/tighten-paper/SKILL.md +29 -42
  352. package/skills/tighten-paper/SKILL.md.spec.ts +28 -41
  353. package/skills/verify-citations/SKILL.md +7 -4
  354. package/skills/verify-citations/SKILL.md.spec.ts +6 -3
  355. package/skills/verify-citations/scripts/bib-authors.check.test.mjs +17 -32
  356. package/skills/verify-citations/scripts/bib-authors.cli.test.ts +138 -43
  357. package/skills/verify-citations/scripts/bib-authors.d.mts +22 -2
  358. package/skills/verify-citations/scripts/bib-authors.mjs +54 -193
  359. package/skills/verify-citations/scripts/bib-authors.test.mjs +1 -44
  360. package/skills/verify-citations/scripts/verify-cites.cli.test.ts +16 -0
  361. package/skills/verify-citations/scripts/verify-cites.d.mts +19 -3
  362. package/skills/verify-citations/scripts/verify-cites.mjs +42 -108
  363. package/skills/verify-citations/scripts/verify-cites.test.mjs +23 -36
  364. package/src/adapters/bibtex/index.test.ts +163 -0
  365. package/src/adapters/bibtex/index.ts +155 -0
  366. package/src/adapters/latex/filecontents.ts +114 -0
  367. package/src/adapters/latex/includes.ts +18 -10
  368. package/src/adapters/latex/index.test.ts +73 -0
  369. package/src/adapters/latex/index.ts +2 -0
  370. package/src/adapters/latex/nodes.ts +64 -0
  371. package/src/adapters/latex/parse.test.ts +38 -0
  372. package/src/adapters/latex/parse.ts +67 -6
  373. package/src/adapters/memory/index.ts +17 -0
  374. package/src/adapters/references/index.test.ts +75 -21
  375. package/src/adapters/references/index.ts +37 -32
  376. package/src/adapters/sources-record/index.test.ts +87 -0
  377. package/src/adapters/sources-record/index.ts +82 -0
  378. package/src/adapters/tex-output/blg.ts +27 -0
  379. package/src/adapters/tex-output/fls.ts +24 -0
  380. package/src/adapters/tex-output/index.test.ts +171 -0
  381. package/src/adapters/tex-output/index.ts +12 -0
  382. package/src/bib-rules.test.ts +356 -0
  383. package/src/bib-rules.ts +228 -0
  384. package/src/bibliography-at.test.ts +286 -0
  385. package/src/build.harness.mjs +2 -2
  386. package/src/build.test.ts +211 -1
  387. package/src/build.ts +101 -41
  388. package/src/claim-provenance.test.ts +33 -7
  389. package/src/claim-provenance.ts +2 -2
  390. package/src/cli.harness.mjs +46 -56
  391. package/src/cli.test.ts +44 -48
  392. package/src/cli.ts +51 -82
  393. package/src/domain/paper-source.test.ts +58 -20
  394. package/src/domain/paper-source.ts +38 -42
  395. package/src/domain/paper-sources.ts +94 -0
  396. package/src/domain/sources-record.test.ts +71 -0
  397. package/src/domain/sources-record.ts +136 -0
  398. package/src/domain/tex-run.test.ts +228 -0
  399. package/src/domain/tex-run.ts +139 -0
  400. package/src/folder-venue-rule.test.ts +1 -1
  401. package/src/heading-case.test.ts +24 -10
  402. package/src/init.test.ts +6 -7
  403. package/src/init.ts +3 -22
  404. package/src/latex-log.ts +20 -0
  405. package/src/latex-loop.harness.ts +1 -0
  406. package/src/latex-loop.ts +2 -0
  407. package/src/lint-fix.test.ts +10 -4
  408. package/src/linted-set.test.ts +9 -1
  409. package/src/new-paper.harness.mjs +12 -30
  410. package/src/new-paper.test.ts +15 -11
  411. package/src/new-paper.ts +11 -30
  412. package/src/package-dirs.ts +3 -2
  413. package/src/paper-files.test.ts +5 -4
  414. package/src/paper-files.ts +4 -4
  415. package/src/paper-includes.test.ts +157 -38
  416. package/src/paper-includes.ts +39 -35
  417. package/src/paper-prose.test.ts +78 -0
  418. package/src/paper-prose.ts +67 -0
  419. package/src/paper-record.test.ts +173 -0
  420. package/src/paper-record.ts +66 -0
  421. package/src/paper-settings-commands.test.ts +5 -2
  422. package/src/paper-sources.ts +151 -0
  423. package/src/parse-args.test.ts +0 -1
  424. package/src/ports/bib-reader.ts +27 -0
  425. package/src/ports/check-references.ts +7 -3
  426. package/src/ports/latex.ts +19 -0
  427. package/src/ports/sources-codec.ts +11 -0
  428. package/src/ports/tex-output.ts +17 -0
  429. package/src/recorded-bibliography.ts +297 -0
  430. package/src/reference-rules.ts +217 -104
  431. package/src/references.test.ts +520 -177
  432. package/src/references.ts +185 -37
  433. package/src/register-bands.test.ts +5 -1
  434. package/src/register-bands.ts +3 -5
  435. package/src/register.test.ts +6 -1
  436. package/src/register.ts +2 -2
  437. package/src/rule-cases.test.ts +68 -12
  438. package/src/rule-docs.test.ts +0 -1
  439. package/src/sources-record.test.ts +308 -0
  440. package/src/sources-record.ts +231 -0
  441. package/src/sources-rules.test.ts +152 -0
  442. package/src/sources-rules.ts +117 -0
  443. package/src/structure.harness.ts +5 -11
  444. package/src/structure.ts +4 -3
  445. package/src/tex-paper.test.ts +148 -78
  446. package/src/tex-paper.ts +31 -61
  447. package/src/tex-venue-rules.test.ts +36 -8
  448. package/src/tex-venue-rules.ts +3 -5
  449. package/src/types.ts +0 -2
  450. package/dist/eslint-rules/bib-reachable-entry.d.ts +0 -22
  451. package/dist/eslint-rules/bib-reachable-entry.d.ts.map +0 -1
  452. package/dist/eslint-rules/bib-reachable-entry.js +0 -75
  453. package/dist/eslint-rules/bib-reachable-entry.js.map +0 -1
  454. package/eslint-rules/bib-reachable-entry.ts +0 -76
  455. package/fixtures/real-markdown-paper/baseline.json +0 -22
  456. package/fixtures/real-markdown-paper/baseline.ts +0 -70
  457. package/skills/paper-pipeline/PIPELINE-MAP.md +0 -381
  458. package/skills/paper-pipeline/pipeline-firing.baseline.json +0 -270
  459. package/skills/paper-pipeline/references/sweep-2026-08-09-ideal-pipeline.md +0 -585
  460. package/skills/paper-pipeline/scripts/artifact-coverage.harness.mjs +0 -492
  461. package/skills/paper-pipeline/scripts/artifact-coverage.mjs +0 -397
  462. package/skills/paper-pipeline/scripts/artifact-coverage.test.ts +0 -86
  463. package/skills/paper-pipeline/scripts/check-provenance.mjs +0 -184
  464. package/skills/paper-pipeline/scripts/population-map.harness.mjs +0 -135
  465. package/skills/paper-pipeline/scripts/population-map.mjs +0 -188
  466. package/skills/paper-pipeline/scripts/population-map.test.mjs +0 -154
  467. package/skills/paper-pipeline/scripts/provenance.harness.mjs +0 -266
  468. package/skills/paper-pipeline/scripts/round-diff.harness.ts +0 -907
  469. package/skills/paper-pipeline/scripts/round-diff.mjs +0 -583
  470. package/skills/paper-pipeline/scripts/round-diff.test.mjs +0 -159
  471. package/skills/tighten-paper/__snapshots__/structure.test.ts.snap +0 -133
  472. package/skills/tighten-paper/structure.mjs +0 -364
  473. package/skills/tighten-paper/structure.test.ts +0 -117
  474. package/templates/paper/paper.md +0 -4
@@ -43,200 +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";
48
- import { headings as mdHeadings, requireMarkdown } from "#lib/markdown";
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";
49
53
  import { isMain } from "../../paper-pipeline/scripts/consumer.mjs";
50
54
 
51
55
  const DBLP = "https://dblp.org/search/publ/api";
52
56
 
53
- /* ---------- input: a .bib, a .tex with filecontents, or a paper dir ---------- */
57
+ /* ---------- input: the bibliography TeX reads, or a .bib named ---------- */
58
+
59
+ const DEPS = {
60
+ files: nodeFiles,
61
+ codec: sourcesCodec,
62
+ latex: latexReader,
63
+ bib: bibReader,
64
+ };
54
65
 
55
66
  /**
56
- * Some papers carry no BibTeX at all. `compile-rules-2026` writes its 67 references as a
57
- * hand-numbered markdown list that `repro/md2acl.py` turns into ACL format at build time:
58
- *
59
- * 12. N. F. Liu, K. Lin, J. Hewitt. *Lost in the Middle: How Language Models Use Long
60
- * Contexts.* TACL, 2024. arXiv:2307.03172.
61
- *
62
- * 🔴 Exempting such a paper would be the wrong call, and measurably so: a hand-written author
63
- * list has NO machine-checkable source, so it is the MORE exposed of the two formats, not the
64
- * less. The class this script hunts lives exactly where an entry names a published venue and an
65
- * arXiv id in the same breath — which is most of this list.
66
- *
67
- * Authors are taken only from the text BEFORE the first `*`, on purpose: entries here sometimes
68
- * append a second work ("— and the reanalysis: S. B. Hossain, ...") after the title, and folding
69
- * those names in would invent co-authors that the first work does not have.
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
70
  */
71
- export function parseMarkdownRefs(text) {
72
- // 🔴 The heading comes FROM THE PARSER, not from a regex (the base's rule "parse markdown with a
73
- // parser"). The previous `text.search(/^#+\s*References\s*$/m)` opened the bibliography on a
74
- // `## References` line QUOTED inside a ``` fence — and the papers in this repo quote their own
75
- // markup in chunks. Exactly this assert already stands at a neighbour
76
- // (`paper-pipeline/scripts/extract-ref-facts.harness.mjs`: "a heading inside a ``` block is not a
77
- // heading"), that is, the class was known and was being reproduced here again.
78
- // `requireMarkdown()` — so that a missing markdown-it fails LOUDLY: with an empty parser
79
- // `headings()` returns [], and the gate would report "no references" instead of refusing. That is
80
- // the same trade of one silent failure for another that the rule was written against.
81
- requireMarkdown();
82
- const h = mdHeadings(text).find((x) => /^References\s*$/u.test(x.text));
83
- if (!h) return [];
84
- const body = text.slice(h.offset);
85
- const out = [];
86
- // 🔴 `$(?![\s\S])`, NOT `\Z`. JavaScript has no `\Z` anchor — outside a unicode-mode pattern it
87
- // is an identity escape meaning the LETTER Z, so the first version of this lookahead ended entry
88
- // 1 in the middle of "J. Zhou", at the Z. Measured: 48 of 67 references parsed, and the missing
89
- // 19 looked like ordinary gaps rather than a bug. Same family as the `\b`-over-Cyrillic defect
90
- // already recorded in this repo: an escape that means one thing in Perl/Python and another here.
91
- const re = /^(\d+)\.[ \t]+([\s\S]*?)(?=^\d+\.[ \t]|^#|$(?![\s\S]))/gm;
92
- let m;
93
- while ((m = re.exec(body))) {
94
- const entry = m[2].replace(/\s*\n\s*/g, " ").trim();
95
- const t = entry.match(/\*([^*]+)\*/);
96
- if (!t) {
97
- // No italic title and no author list — in this corpus these are SOFTWARE references
98
- // (npm packages, repos). Nothing for DBLP to disagree with. They are surfaced as
99
- // not-applicable rather than dropped: an entry that silently vanishes between the file
100
- // and the report is indistinguishable from an entry that passed.
101
- out.push({
102
- type: "mdref",
103
- key: `ref${m[1]}`,
104
- unparsed: true,
105
- author: "",
106
- title: "",
107
- booktitle: "",
108
- journal: "",
109
- });
110
- continue;
111
- }
112
- const authorPart = entry
113
- .slice(0, entry.indexOf("*"))
114
- .trim()
115
- .replace(/[.,;]\s*$/, "");
116
- const rest = entry.slice(entry.indexOf(t[0]) + t[0].length);
117
- out.push({
118
- type: "mdref",
119
- key: `ref${m[1]}`,
120
- author: authorPart
121
- .split(/,\s*|\s+and\s+/)
122
- .filter(Boolean)
123
- .join(" and "),
124
- title: t[1].replace(/\.$/, "").trim(),
125
- booktitle: rest.trim(),
126
- journal: "",
127
- });
128
- }
129
- return out;
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 bibTextFrom(target) {
133
- let file = target;
134
- if (!existsSync(file)) die(`no such path: ${file}`);
135
- if (!extname(file)) {
136
- const names = readdirSync(file);
137
- // 🔴 THE CANONICAL NAME FIRST, then the only candidate, and never "whichever
138
- // turns up first" (review #189). It used to be `names.find(f => f.endsWith(".tex"))`,
139
- // that is, the first in directory order. Measured: one of the papers in the corpus
140
- // has FIVE drafts lying around (paper-CONSTRUCTIVE-…, paper-FOLDED-…, paper-SAFE-…),
141
- // while PIPELINE-STATUS.md names `paper.tex` as the submitted source. The author
142
- // check went off into a stale draft and printed a verdict about the WRONG
143
- // bibliography — a gate that checks the wrong file is worse than a missing one,
144
- // because it says "checked".
145
- const pick = (ext) => {
146
- const canon = names.find((f) => f === `paper${ext}`);
147
- if (canon) return canon;
148
- const all = names.filter((f) => f.endsWith(ext)).sort();
149
- if (all.length === 1) return all[0];
150
- if (all.length > 1) {
151
- // We do not fail: a new check here would set up one more surface, and its
152
- // home is decided by the ladder in the `CLAUDE.md` next to the papers, not by
153
- // this script (the `frozen-checks` ratchet is what guards that). It is enough
154
- // to SAY it out loud and to choose DETERMINISTICALLY — the header below prints
155
- // which file was checked anyway, so the reader sees the choice.
156
- console.error(
157
- `⚠️ ${file} holds several ${ext} files and no canonical paper${ext}: ` +
158
- `${all.join(", ")} — took ${all[0]}. If that is the wrong one, name the file explicitly.`,
159
- );
160
- return all[0];
161
- }
162
- return undefined;
163
- };
164
- const found =
165
- pick(".bib") ||
166
- pick(".tex") ||
167
- names.find((f) => f === "paper.md" || f === "draft.md");
168
- if (!found) die(`no .bib, .tex or paper.md in ${file}`);
169
- file = join(file, found);
170
- }
171
- const text = readFileSync(file, "utf-8");
172
- if (file.endsWith(".md")) return { text, file, markdown: true };
173
- if (file.endsWith(".bib")) return { text, file };
174
- // A .tex may carry the bibliography inline via filecontents — that is how our papers do it.
175
- const m = text.match(
176
- /\\begin\{filecontents\*?\}(?:\[[^\]]*\])?\{[^}]*\.bib\}\r?\n([\s\S]*?)\\end\{filecontents\*?\}/,
177
- );
178
- if (m) return { text: m[1], file };
179
- const sibling = join(file, "..", "refs.bib");
180
- if (existsSync(sibling))
181
- return { text: readFileSync(sibling, "utf-8"), file: sibling };
182
- die(
183
- `no bibliography found in ${file} (no filecontents block, no refs.bib beside it)`,
184
- );
185
- }
78
+ /* ---------- entries: as the bibtex reader read them ---------- */
186
79
 
187
- /* ---------- a deliberately small bib reader ----------
188
- * Only three fields are needed (type, key, author, title, booktitle/journal), and a full
189
- * BibTeX grammar would be a second thing to maintain. Brace-depth counting is enough and
190
- * is exercised by the colocated test. */
191
- export function parseBib(text) {
192
- const out = [];
193
- const re = /@(\w+)\s*\{\s*([^,\s]+)\s*,/g;
194
- let m;
195
- while ((m = re.exec(text))) {
196
- const [, type, key] = m;
197
- let i = re.lastIndex,
198
- depth = 1;
199
- while (i < text.length && depth > 0) {
200
- if (text[i] === "{") depth++;
201
- else if (text[i] === "}") depth--;
202
- i++;
203
- }
204
- const body = text.slice(re.lastIndex, i - 1);
205
- out.push({ type: type.toLowerCase(), key, ...fields(body) });
206
- }
207
- return out;
208
- }
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(", ");
209
86
 
210
- function fields(body) {
211
- const get = (name) => {
212
- const r = new RegExp(`(?:^|[,\\s])${name}\\s*=\\s*`, "i");
213
- const at = body.search(r);
214
- if (at === -1) return "";
215
- let i = body.indexOf("=", at) + 1;
216
- while (/\s/.test(body[i])) i++;
217
- if (body[i] === "{") {
218
- let depth = 1,
219
- j = i + 1;
220
- while (j < body.length && depth > 0) {
221
- if (body[j] === "{") depth++;
222
- else if (body[j] === "}") depth--;
223
- j++;
224
- }
225
- return body.slice(i + 1, j - 1);
226
- }
227
- if (body[i] === '"') {
228
- const j = body.indexOf('"', i + 1);
229
- return body.slice(i + 1, j);
230
- }
231
- const j = body.indexOf(",", i);
232
- return body.slice(i, j === -1 ? undefined : j);
233
- };
234
- return {
235
- author: get("author"),
236
- title: get("title"),
237
- booktitle: get("booktitle"),
238
- journal: get("journal"),
239
- };
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
+ }));
240
100
  }
241
101
 
242
102
  /* ---------- normalisation: surnames only, in order ---------- */
@@ -358,13 +218,6 @@ export async function checkAuthors(
358
218
  const matched = []; // compared against the published record, and equal
359
219
  const entries = parsed.filter((e) => e.title && e.author);
360
220
 
361
- for (const e of parsed.filter((x) => x.unparsed)) {
362
- skipped.push({
363
- key: e.key,
364
- why: "no author/title — a reference to software or a dataset, DBLP does not apply",
365
- });
366
- }
367
-
368
221
  for (const e of entries) {
369
222
  if (!claimsPublished(e)) continue; // a preprint entry is allowed to carry preprint metadata
370
223
  if (truncated(e.author)) {
@@ -438,14 +291,22 @@ async function main() {
438
291
  if (!args[0])
439
292
  die("usage: bib-authors.mjs <paper-dir|file.bib|file.tex> [--json]");
440
293
 
441
- const { text, file, markdown } = bibTextFrom(args[0]);
442
- const parsed = markdown ? parseMarkdownRefs(text) : parseBib(text);
294
+ const { files, texts } = bibliographyFrom(args[0]);
295
+ const file = files.join(", ");
296
+ const parsed = texts.flatMap((t) => authorEntries(t.entries));
443
297
  const entries = parsed.filter((e) => e.title && e.author);
444
298
  const { findings, skipped, unchecked } = await checkAuthors(parsed);
445
299
  if (asJson) {
446
300
  console.log(
447
301
  JSON.stringify(
448
- { 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
+ },
449
310
  null,
450
311
  2,
451
312
  ),
@@ -14,12 +14,7 @@
14
14
  */
15
15
  import assert from "node:assert/strict";
16
16
  import { test } from "vitest";
17
- import {
18
- surnames,
19
- compare,
20
- claimsPublished,
21
- parseMarkdownRefs,
22
- } from "./bib-authors.mjs";
17
+ import { surnames, compare, claimsPublished } from "./bib-authors.mjs";
23
18
 
24
19
  const t = test;
25
20
 
@@ -135,41 +130,3 @@ t("a proceedings entry IS in scope", () => {
135
130
  true,
136
131
  );
137
132
  });
138
-
139
- console.log("bib-authors — where the bibliography begins");
140
-
141
- // Both halves. "Silent on a fence" without "finds the real one" is indistinguishable from a broken parse.
142
- t("a real ## References is parsed", () => {
143
- const md = [
144
- "# Paper",
145
- "text",
146
- "",
147
- "## References",
148
- "",
149
- "1. A. Author. *A title*. In NeurIPS, 2023.",
150
- ].join("\n");
151
- const refs = parseMarkdownRefs(md);
152
- assert.equal(refs.length, 1);
153
- assert.match(refs[0].title, /A title/);
154
- });
155
-
156
- t("a ## References inside a ``` fence does NOT open the bibliography", () => {
157
- const md = [
158
- "# Paper",
159
- "",
160
- "The section is written like this:",
161
- "",
162
- "```markdown",
163
- "## References",
164
- "",
165
- "1. Z. Ghost. *Not a reference, but a markup example*. In Nowhere, 2020.",
166
- "```",
167
- "",
168
- "The end.",
169
- ].join("\n");
170
- assert.deepEqual(
171
- parseMarkdownRefs(md),
172
- [],
173
- "quoted markup is not a bibliography — the ^#+ regex counted it as a heading",
174
- );
175
- });
@@ -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
  }