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
@@ -3,34 +3,32 @@
3
3
  * extract-ref-facts.mjs — parse a paper's bibliography, ask the registries and write FACTS to JSON.
4
4
  * It judges nothing.
5
5
  *
6
- * Usage: node extract-ref-facts.mjs <paper dir|paper.md|refs.bib> [--offline] [--refresh]
6
+ * Usage: node extract-ref-facts.mjs <paper dir|paper.tex|refs.bib> [--offline] [--refresh]
7
7
  * [--cache=PATH] [--out=PATH] [--quiet]
8
- * Exit: 0 — facts written · 1 — nothing to write (no source, no entries).
8
+ * Exit: 0 — facts written · 1 — nothing to write (no source, a source that is neither a .bib nor
9
+ * a .tex, no entries).
9
10
  *
10
11
  * WHY THE SPLIT. Before 2026-08-26 measurement and judgement lived in one script
11
12
  * (`verify-refs.mjs`, 20 emit sites, 8 kinds of findings) — that is, on the fifth rung of the
12
13
  * ladder in `the consumer's papers CLAUDE.md`. The judgement moved into ESLint rules
13
14
  * (`eslint-rules/ref-facts.mjs`): a registry of rules, severity from config, `eslint-disable` with a
14
- * reason, `file:line:col` positions. What stayed here is the plumbing: parsing markup, parsing
15
- * `.bib`, the network, parsing the registries' responses.
15
+ * reason, `file:line:col` positions. What stayed here is the plumbing: parsing `.bib`, the network,
16
+ * parsing the registries' responses.
16
17
  *
17
18
  * The precedent in this same knowledge base is `render-paper/extract-pdf-facts.mjs` +
18
19
  * `eslint-rules/pdf-facts.mjs`. The boundary is exactly the same: the script goes out into the
19
20
  * world, the rule passes the judgement.
20
21
  *
21
- * 🔴 WHY THE RULE CANNOT LIVE OVER THE PAPER ITSELF. Of the three papers in the knowledge base one
22
- * has a `paper.md` (`<paper-a>`); in `<paper-b>` and `<paper-c>` the bibliography is `refs.bib`, and
23
- * no ESLint language plugin parses `.bib`. The common input that can do both is the facts JSON.
24
- * The price is known and written down: a finding is addressed into the facts, not into a line of
25
- * `paper.md`; that is why every entry in the facts carries the `line` of its source, and the rule
26
- * prints it in the message.
22
+ * 🔴 WHY THE RULE CANNOT LIVE OVER THE BIBLIOGRAPHY ITSELF. No ESLint language plugin parses
23
+ * `.bib`, so the rule's input is the facts JSON. The price is known and written down: a finding is
24
+ * addressed into the facts, not into a line of the `.bib`; that is why every entry in the facts
25
+ * carries the `line` of its source, and the rule prints it in the message.
27
26
  *
28
27
  * 🔴 WHAT THIS MOVE FIXED (both legs were DEAD, measured 2026-08-26):
29
28
  *
30
- * 1. `.bib` WAS NEVER OPENED. `resolveSource` walked `paper.md` → `draft.md` →
31
- * `build/custom.bib`; `refs.bib` was not on the list, although the error text promised
32
- * "or .bib". The run: `verify-refs.mjs <papers-root>/<paper-b>` →
33
- * "no paper.md, draft.md or .bib … nothing to check".
29
+ * 1. `.bib` WAS NEVER OPENED. `resolveSource` did not list `refs.bib`, although the error text
30
+ * promised "or .bib". The run: `verify-refs.mjs <papers-root>/<paper-b>` →
31
+ * "… or .bib … nothing to check".
34
32
  * 2. THE `.bib` PARSER DID NOT TAKE THE REAL FILES. The regex `/@\w+\{([^,]+),([\s\S]*?)\n\}/g`
35
33
  * required a `}` on a line of its own, while both of our `refs.bib` close an entry on the line
36
34
  * of the last field (`note={arXiv:2310.06770}}`). A run straight at the file: "parsed 0
@@ -62,23 +60,17 @@
62
60
  * registry's response, not by a comparison. If the cache kept parsed entries, those readers would
63
61
  * stay uncovered while looking covered.
64
62
  */
65
- import {
66
- readFileSync,
67
- writeFileSync,
68
- existsSync,
69
- mkdirSync,
70
- statSync,
71
- } from "node:fs";
63
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs";
72
64
  import { createHash } from "node:crypto";
73
65
  import { join, resolve, dirname, basename } from "node:path";
74
- import { headings as mdHeadings, requireMarkdown } from "#lib/markdown";
66
+ import { latexReader } from "#src/adapters/latex/index";
67
+ import { nodeFiles } from "#src/adapters/node/index";
68
+ import { sourcesCodec } from "#src/adapters/sources-record/index";
69
+ import { absolutePath } from "#src/domain/paths";
70
+ import { bibReader } from "#src/adapters/bibtex/index";
71
+ import { bibliographyAt, bibliographyUnreadWhy } from "#src/paper-sources";
75
72
  import { isMain } from "./consumer.mjs";
76
73
 
77
- // Markup is parsed with a parser (`CLAUDE.md`, 2026-08-11). We fail rather than degrade: without
78
- // the parser the reference list would not be found at all, and the facts would come out empty —
79
- // that is, a clean verdict about a paper that in fact has forty references.
80
- requireMarkdown();
81
-
82
74
  const ROOT = process.env.CLAUDE_PROJECT_DIR || process.cwd();
83
75
  const CROSSREF = "https://api.crossref.org/works/";
84
76
  const ARXIV = "https://export.arxiv.org/api/query?id_list=";
@@ -89,135 +81,12 @@ const UA =
89
81
 
90
82
  export const SCHEMA = 1;
91
83
 
92
- // ── parsing: the reference list in markdown ──────────────────────────────────
93
- //
94
- // The same shape that `repro/md2submission.py` reads, deliberately: that script turns the list into
95
- // the .bib actually uploaded to the venue, so whatever it counts as an entry is an entry.
96
- // `^\d{1,2}. ` opens an entry, an indent continues it, a blank line closes it.
97
-
98
- const REF_OPEN = /^(\d{1,2})\.\s+(.*)$/;
99
- /** Working notes that never reach the PDF; md2submission strips them, so we do too. */
100
- const BRACKET_NOTE = /\[(VERIFY|ANONYMIZ)[^\]]*\]/g;
101
-
102
- export function parseMarkdownRefs(text) {
103
- // The section is cut by the parser. The former `text.split(/^## References\s*$/m)[1]` opened the
104
- // reference list on a `## References` quoted inside a ```-block — and in this repo papers quote
105
- // their own markup in chunks.
106
- const refsHs = mdHeadings(text).filter(
107
- (h) => h.depth === 2 && /^References\s*$/u.test(h.text),
108
- );
109
- if (refsHs.length === 0) return [];
110
- const nl = text.indexOf("\n", refsHs[0].offset);
111
- const body = text.slice(
112
- nl === -1 ? text.length : nl,
113
- refsHs[1] ? refsHs[1].offset : text.length,
114
- );
115
- // The line where the body starts in the source file — so that an entry has an ADDRESS in
116
- // `paper.md` and not just a number in a list. The rule prints it in the message: the facts file
117
- // lives in `_build/`, and without the source line a finding would have to be hunted for by eye.
118
- const bodyLine = text
119
- .slice(0, nl === -1 ? text.length : nl)
120
- .split("\n").length;
121
- // A heading closes the list — appendices come AFTER the bibliography, and without this their
122
- // numbered prose would be parsed as references. The line numbers come from the parser:
123
- // `/^#{1,6}\s/` also counted a hash inside a ```-block as a heading.
124
- const headingLines = new Set(mdHeadings(body).map((h) => h.line));
125
- const entries = [];
126
- let cur = null;
127
- let buf = [];
128
- let curLine = 0;
129
- const flush = () => {
130
- if (cur !== null)
131
- entries.push({
132
- n: cur,
133
- line: curLine,
134
- raw: buf.join(" ").replace(BRACKET_NOTE, "").trim(),
135
- });
136
- cur = null;
137
- buf = [];
138
- };
139
- const lns = body.split("\n");
140
- for (let i = 0; i < lns.length; i++) {
141
- const ln = lns[i];
142
- if (headingLines.has(i)) break;
143
- const m = REF_OPEN.exec(ln);
144
- if (m) {
145
- flush();
146
- cur = Number(m[1]);
147
- curLine = bodyLine + i;
148
- buf = [m[2].trim()];
149
- } else if (cur !== null && ln.trim()) buf.push(ln.trim());
150
- else if (cur !== null) flush();
151
- }
152
- flush();
153
- return entries.map(splitEntry).filter(Boolean);
154
- }
155
-
156
- /** `A, B, C. *Title.* rest` → authors / title / remainder — the shape md2submission produces. */
157
- function splitEntry(e) {
158
- const base = { n: e.n, line: e.line, key: null, raw: e.raw };
159
- const mt = /\*(.+?)\*/.exec(e.raw);
160
- if (!mt)
161
- return {
162
- ...base,
163
- authors: [],
164
- truncated: false,
165
- title: null,
166
- year: yearIn(e.raw),
167
- venue_text: e.raw,
168
- };
169
- const title = mt[1].trim().replace(/\.$/, "");
170
- const rawAuthors = splitAuthors(e.raw.slice(0, mt.index));
171
- const note = e.raw
172
- .slice(mt.index + mt[0].length)
173
- .trim()
174
- .replace(/^,\s*/, "");
175
- const { authors, truncated } = dropEtAl(rawAuthors);
176
- return {
177
- ...base,
178
- authors,
179
- truncated,
180
- title,
181
- year: yearIn(note) ?? yearIn(e.raw),
182
- venue_text: note,
183
- };
184
- }
185
-
186
- function splitAuthors(a) {
187
- const s = a
188
- .trim()
189
- .replace(/\.\s*$/, "")
190
- .trim();
191
- if (!s) return [];
192
- return s
193
- .split(",")
194
- .map((p) => p.trim())
195
- .filter(Boolean);
196
- }
197
-
198
- /**
199
- * `Z. Xiang et al.` — the entry deliberately gives a PREFIX of the author list. That is a fact about
200
- * the entry, not a judgement, so the truncation is removed here and the rule receives an honest
201
- * prefix plus a flag.
202
- */
203
- const ET_AL = /\bet\s+al\.?$/i;
204
- function dropEtAl(authors) {
205
- if (!authors.length || !ET_AL.test(authors[authors.length - 1].trim()))
206
- return { authors, truncated: false };
207
- const out = authors.slice();
208
- out[out.length - 1] = out[out.length - 1].replace(ET_AL, "").trim();
209
- return { authors: out.filter(Boolean), truncated: true };
210
- }
211
-
212
- const yearIn = (s) => (/\b(19|20)\d{2}\b/.exec(s) ?? [null])[0];
213
-
214
84
  // ── parsing: .bib WITH A REAL PARSER ─────────────────────────────────────────
215
85
 
216
86
  /**
217
- * The name order is normalised to "First name Surname" — the same shape in which authors are written
218
- * in markdown. Then the rule has ONE `surname()` function (the last alphabetic token) for both
219
- * sources instead of two branches. Normalising the format is the parser's job; comparing is the
220
- * rule's job.
87
+ * The name order is normalised to "First name Surname". Then the rule has ONE `surname()` function
88
+ * (the last alphabetic token). Normalising the format is the parser's job; comparing is the rule's
89
+ * job.
221
90
  *
222
91
  * 🔴 TWO SHAPES, AND BOTH ARE LISTED EXPLICITLY. The parser returns an institution
223
92
  * (`author={{Adversa AI}}` — the double brace means "one name as a whole, do not split") as
@@ -241,47 +110,15 @@ const joinName = (a) =>
241
110
  .trim();
242
111
 
243
112
  /**
244
- * 🔴 THE .bib PARSER IS AN OPTIONAL DEPENDENCY, AND THE FAILURE MUST BE LOUD AND CARRY THE CURE.
245
- * It weighs 15 MB out of the consumer's 56 MB install (the package itself 9 MB plus the English
246
- * model `wink-eng-lite-web-model` 4 MB and `unicode2latex` 2 MB) — 27% of the weight for a single
247
- * call that only whoever extracts bibliography facts needs. The import here is dynamic anyway, so
248
- * the laziness was already there; the manifest merely stopped lying about it being required.
249
- *
250
- * ⚠️ A silent skip here would be the worst of the options: a missing checker and a passing one look
251
- * the same, and "the bibliography was not checked" reads as "the bibliography is fine". That is why
252
- * the message names the command, not the fact.
113
+ * The facts' entries of one database, as the bibtex reader read them (src/ports/bib-reader.ts): each
114
+ * with its line in the file that holds it, so a finding points where the author edits (the block's
115
+ * lines in paper.tex, or the `.bib`'s).
253
116
  */
254
- export async function parseBib(
255
- text,
256
- { load = () => import("@retorquere/bibtex-parser") } = {},
257
- ) {
258
- let parse;
259
- try {
260
- ({ parse } = await load());
261
- } catch (e) {
262
- if (e?.code !== "ERR_MODULE_NOT_FOUND") throw e;
263
- throw new Error(
264
- "parsing .bib requires @retorquere/bibtex-parser — it is declared OPTIONAL because it " +
265
- "weighs 15 MB and is needed only for bibliography facts.\n" +
266
- " Install: npm i -D @retorquere/bibtex-parser\n" +
267
- " Why not our own regex: measured 26.08 — the regex gave 0 entries on both real " +
268
- "files, four libraries gave the correct 27 and 51.",
269
- );
270
- }
271
- // `sentenceCase: false` — the title is needed as written. Our normalisation folds the case
272
- // anyway, but a fact has to be a fact: a paraphrased title cannot be shown to a human.
273
- const res = parse(text, { sentenceCase: false, verbatimFields: [] });
274
- // The entry's line in the file — found by its key. The parser gives no positions, and a finding
275
- // needs an address; a key in `.bib` is unique by definition of the format, so the search is
276
- // unambiguous.
277
- const lines = text.split("\n");
278
- const lineOfKey = (key) => {
279
- const i = lines.findIndex((l) => l.includes(`{${key},`));
280
- return i === -1 ? 0 : i + 1;
281
- };
282
- return res.entries.map((e, i) => {
283
- const f = e.fields ?? {};
284
- const names = Array.isArray(f.author) ? f.author : [];
117
+ export function factEntries(bib) {
118
+ const lineOf = (at) => bib.text.slice(0, at).split("\n").length;
119
+ return bib.entries.map((e, i) => {
120
+ const f = e.fields;
121
+ const names = e.names.author ?? [];
285
122
  // `and others` is BibTeX's `et al.`. The parser returns it as an author with no first name.
286
123
  const isOthers = (a) => !a.firstName && /^others$/i.test(a.lastName ?? "");
287
124
  const truncated = names.some(isOthers);
@@ -289,29 +126,20 @@ export async function parseBib(
289
126
  .filter((a) => !isOthers(a))
290
127
  .map(joinName)
291
128
  .filter(Boolean);
292
- // A field is a string in today's parser output (list fields such as `keywords` are arrays,
293
- // and none of those is read); an array or a number from another parser version is joined or
294
- // printed rather than dropped.
295
- const str = (v) =>
296
- Array.isArray(v)
297
- ? v.join(" ")
298
- : typeof v === "string"
299
- ? v
300
- : v == null
301
- ? ""
302
- : String(v);
129
+ // Every field the reader gives is text; a field the entry lacks is "".
130
+ const str = (v) => v ?? "";
303
131
  const venueText = [f.booktitle, f.journal, f.note, f.howpublished]
304
132
  .map(str)
305
133
  .filter(Boolean)
306
134
  .join(" ");
307
135
  return {
308
136
  n: i + 1,
309
- line: lineOfKey(e.key),
137
+ line: lineOf(e.span.start),
310
138
  key: e.key,
311
139
  // `raw` is where identifiers are looked for: they hide in `note`, in `journal`
312
140
  // (`arXiv preprint arXiv:2107.03374`), and in a separate `doi` field.
313
141
  raw: [
314
- str(f.author && authors.join(" and ")),
142
+ authors.join(" and "),
315
143
  str(f.title),
316
144
  venueText,
317
145
  str(f.doi),
@@ -527,27 +355,40 @@ export function recordFrom(key, cached) {
527
355
 
528
356
  // ── assembling the facts ─────────────────────────────────────────────────────
529
357
 
530
- /** `paper.md` → `draft.md` → `refs.bib` → `build/custom.bib`. `refs.bib` ADDED 26.08 (defect #1). */
531
- export const SOURCE_ORDER = [
532
- "paper.md",
533
- "draft.md",
534
- "refs.bib",
535
- "build/custom.bib",
536
- ];
358
+ /**
359
+ * WHICH BIBLIOGRAPHY: the one the last build's bibtex opened, as `paperlint build` recorded it
360
+ * (`_build/sources.json`, docs/design/paper-sources.md §1) and `bibliographyAt` reads it
361
+ * (src/paper-sources.ts) — never an order of file names of our own, and never read out of TeX source.
362
+ *
363
+ * a directory the databases bibtex opened for its paper.tex
364
+ * a .tex the same, for that file's folder
365
+ * a .bib that file, as named
366
+ *
367
+ * A paper with no current record is refused: run `npx paperlint build` first. A database TeX wrote from
368
+ * a `filecontents` block is read from the block, where the author edits it.
369
+ */
370
+ const DEPS = {
371
+ files: nodeFiles,
372
+ codec: sourcesCodec,
373
+ latex: latexReader,
374
+ bib: bibReader,
375
+ };
537
376
 
538
- export function resolveSource(target) {
539
- const t = resolve(target);
540
- if (existsSync(t) && statSync(t).isFile()) return t;
541
- for (const c of SOURCE_ORDER) {
542
- const p = join(t, c);
543
- if (existsSync(p)) return p;
544
- }
545
- return null;
377
+ /**
378
+ * The bibliography of `target`: the texts to read, or why there are none — `bibliographyAt`
379
+ * (src/paper-sources.ts) answers, in its words. `paperDir` is where the cache and the facts go.
380
+ */
381
+ export function bibliographyFrom(target) {
382
+ const r = bibliographyAt(absolutePath(resolve(target)), DEPS);
383
+ if (!r.ok) return { ok: false, why: bibliographyUnreadWhy(r.error) };
384
+ const { paperDir, texts } = r.value;
385
+ return { ok: true, paperDir, texts };
546
386
  }
547
387
 
548
- export async function loadEntries(path) {
549
- const text = readFileSync(path, "utf8");
550
- return path.endsWith(".bib") ? parseBib(text) : parseMarkdownRefs(text);
388
+ /** The entries of one text TeX reads, each with its file. */
389
+ export function loadEntries(bib) {
390
+ const file = rel(bib.path);
391
+ return factEntries(bib).map((e) => ({ ...e, file }));
551
392
  }
552
393
 
553
394
  /** Every registry key a list of entries needs. Computed once so the requests go in one batch. */
@@ -559,7 +400,11 @@ export function keysFor(entries) {
559
400
 
560
401
  const rel = (p) => (p.startsWith(ROOT) ? p.slice(ROOT.length + 1) : p);
561
402
 
562
- export function buildFacts({ source, text, entries, cache, cachePath }) {
403
+ /**
404
+ * The facts of a run. `source` is the file the first database was read from (the only one, for a
405
+ * paper with one), `sources` every one; `source_sha256` hashes those files' bytes, in order.
406
+ */
407
+ export function buildFacts({ texts, entries, cache, cachePath }) {
563
408
  const withIds = entries.map((e) => {
564
409
  const ids = extractIds(e);
565
410
  return { ...e, ids, primary: primaryOf(ids), keys: allKeys(ids) };
@@ -570,9 +415,12 @@ export function buildFacts({ source, text, entries, cache, cachePath }) {
570
415
  if (!(k in records)) records[k] = recordFrom(k, cache[k]);
571
416
  return {
572
417
  schema: SCHEMA,
573
- source: rel(source),
574
- source_kind: source.endsWith(".bib") ? "bibtex" : "markdown",
575
- source_sha256: createHash("sha256").update(text).digest("hex"),
418
+ source: rel(texts[0].path),
419
+ sources: [...new Set(texts.map((b) => rel(b.path)))],
420
+ source_kind: "bibtex",
421
+ source_sha256: [...new Set(texts.map((b) => b.text))]
422
+ .reduce((h, t) => h.update(t), createHash("sha256"))
423
+ .digest("hex"),
576
424
  cache: cachePath ? rel(cachePath) : null,
577
425
  generated: new Date().toISOString().slice(0, 10),
578
426
  entries: withIds,
@@ -594,16 +442,12 @@ async function main(argv) {
594
442
  .slice(1)
595
443
  .join("=");
596
444
 
597
- const src = resolveSource(target);
598
- if (!src) {
599
- console.error(
600
- `🛑 no ${SOURCE_ORDER.join(", ")} under ${resolve(target)} — nowhere to take a bibliography from.`,
601
- );
445
+ const found = bibliographyFrom(target);
446
+ if (!found.ok) {
447
+ console.error(`🛑 ${found.why}.`);
602
448
  return 1;
603
449
  }
604
- const paperDir = statSync(resolve(target)).isFile()
605
- ? dirname(src)
606
- : resolve(target);
450
+ const { paperDir, texts } = found;
607
451
  const cachePath = opt("cache")
608
452
  ? resolve(opt("cache"))
609
453
  : join(paperDir, "repro", "refs-cache.json");
@@ -611,13 +455,13 @@ async function main(argv) {
611
455
  ? resolve(opt("out"))
612
456
  : join(paperDir, "_build", "refs.facts.json");
613
457
 
614
- const text = readFileSync(src, "utf8");
615
- const entries = await loadEntries(src);
458
+ const entries = texts.flatMap(loadEntries);
459
+ const names = [...new Set(texts.map((b) => basename(b.path)))].join(", ");
616
460
  if (!entries.length) {
617
461
  // 🔴 Zero entries is a suspect, not a success. Facts with an empty list would look like a
618
462
  // clean bibliography, so we do not write them at all and exit with a non-zero code.
619
463
  console.error(
620
- `🛑 parsed 0 entries out of ${rel(src)}. Silence here would look like a clean bibliography.`,
464
+ `🛑 parsed 0 entries out of ${[...new Set(texts.map((b) => rel(b.path)))].join(", ")}. Silence here would look like a clean bibliography.`,
621
465
  );
622
466
  return 1;
623
467
  }
@@ -644,12 +488,12 @@ async function main(argv) {
644
488
  if (fetched) saveCache(cachePath, cache);
645
489
  }
646
490
 
647
- const facts = buildFacts({ source: src, text, entries, cache, cachePath });
491
+ const facts = buildFacts({ texts, entries, cache, cachePath });
648
492
  mkdirSync(dirname(out), { recursive: true });
649
493
  writeFileSync(out, `${JSON.stringify(facts, null, 2)}\n`);
650
494
  const resolved = Object.values(facts.records).filter((r) => r.found).length;
651
495
  console.log(
652
- `📚 ${basename(src)} → ${rel(out)} (${entries.length} entries, ${Object.keys(facts.records).length} identifiers, ` +
496
+ `📚 ${names} → ${rel(out)} (${entries.length} entries, ${Object.keys(facts.records).length} identifiers, ` +
653
497
  `${resolved} resolved${offline ? ", offline" : fetched ? `, +${fetched} fetched` : ", all from cache"})`,
654
498
  );
655
499
  return 0;