paperlint 7.2.0 → 9.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (474) hide show
  1. package/CLAUDE.md +9 -25
  2. package/CONTRIBUTING.md +5 -4
  3. package/README.md +4 -3
  4. package/dist/adapters/bibtex/index.d.ts +3 -0
  5. package/dist/adapters/bibtex/index.d.ts.map +1 -0
  6. package/dist/adapters/bibtex/index.js +103 -0
  7. package/dist/adapters/bibtex/index.js.map +1 -0
  8. package/dist/adapters/latex/filecontents.d.ts +33 -0
  9. package/dist/adapters/latex/filecontents.d.ts.map +1 -0
  10. package/dist/adapters/latex/filecontents.js +65 -0
  11. package/dist/adapters/latex/filecontents.js.map +1 -0
  12. package/dist/adapters/latex/includes.d.ts +3 -3
  13. package/dist/adapters/latex/includes.d.ts.map +1 -1
  14. package/dist/adapters/latex/includes.js +18 -10
  15. package/dist/adapters/latex/includes.js.map +1 -1
  16. package/dist/adapters/latex/index.d.ts.map +1 -1
  17. package/dist/adapters/latex/index.js +2 -0
  18. package/dist/adapters/latex/index.js.map +1 -1
  19. package/dist/adapters/latex/nodes.d.ts +12 -0
  20. package/dist/adapters/latex/nodes.d.ts.map +1 -1
  21. package/dist/adapters/latex/nodes.js +39 -0
  22. package/dist/adapters/latex/nodes.js.map +1 -1
  23. package/dist/adapters/latex/parse.d.ts +7 -1
  24. package/dist/adapters/latex/parse.d.ts.map +1 -1
  25. package/dist/adapters/latex/parse.js +55 -6
  26. package/dist/adapters/latex/parse.js.map +1 -1
  27. package/dist/adapters/memory/index.d.ts +3 -0
  28. package/dist/adapters/memory/index.d.ts.map +1 -1
  29. package/dist/adapters/memory/index.js +9 -0
  30. package/dist/adapters/memory/index.js.map +1 -1
  31. package/dist/adapters/references/index.d.ts.map +1 -1
  32. package/dist/adapters/references/index.js +27 -24
  33. package/dist/adapters/references/index.js.map +1 -1
  34. package/dist/adapters/sources-record/index.d.ts +9 -0
  35. package/dist/adapters/sources-record/index.d.ts.map +1 -0
  36. package/dist/adapters/sources-record/index.js +68 -0
  37. package/dist/adapters/sources-record/index.js.map +1 -0
  38. package/dist/adapters/tex-output/blg.d.ts +9 -0
  39. package/dist/adapters/tex-output/blg.d.ts.map +1 -0
  40. package/dist/adapters/tex-output/blg.js +20 -0
  41. package/dist/adapters/tex-output/blg.js.map +1 -0
  42. package/dist/adapters/tex-output/fls.d.ts +8 -0
  43. package/dist/adapters/tex-output/fls.d.ts.map +1 -0
  44. package/dist/adapters/tex-output/fls.js +16 -0
  45. package/dist/adapters/tex-output/fls.js.map +1 -0
  46. package/dist/adapters/tex-output/index.d.ts +7 -0
  47. package/dist/adapters/tex-output/index.d.ts.map +1 -0
  48. package/dist/adapters/tex-output/index.js +7 -0
  49. package/dist/adapters/tex-output/index.js.map +1 -0
  50. package/dist/bib-rules.d.ts +47 -0
  51. package/dist/bib-rules.d.ts.map +1 -0
  52. package/dist/bib-rules.js +127 -0
  53. package/dist/bib-rules.js.map +1 -0
  54. package/dist/build.d.ts +29 -1
  55. package/dist/build.d.ts.map +1 -1
  56. package/dist/build.js +41 -30
  57. package/dist/build.js.map +1 -1
  58. package/dist/claim-provenance.d.ts +2 -2
  59. package/dist/cli.d.ts +9 -5
  60. package/dist/cli.d.ts.map +1 -1
  61. package/dist/cli.js +43 -68
  62. package/dist/cli.js.map +1 -1
  63. package/dist/domain/paper-source.d.ts +1 -10
  64. package/dist/domain/paper-source.d.ts.map +1 -1
  65. package/dist/domain/paper-source.js +34 -24
  66. package/dist/domain/paper-source.js.map +1 -1
  67. package/dist/domain/paper-sources.d.ts +72 -0
  68. package/dist/domain/paper-sources.d.ts.map +1 -0
  69. package/dist/domain/paper-sources.js +18 -0
  70. package/dist/domain/paper-sources.js.map +1 -0
  71. package/dist/domain/sources-record.d.ts +102 -0
  72. package/dist/domain/sources-record.d.ts.map +1 -0
  73. package/dist/domain/sources-record.js +38 -0
  74. package/dist/domain/sources-record.js.map +1 -0
  75. package/dist/domain/tex-run.d.ts +25 -0
  76. package/dist/domain/tex-run.d.ts.map +1 -0
  77. package/dist/domain/tex-run.js +79 -0
  78. package/dist/domain/tex-run.js.map +1 -0
  79. package/dist/eslint-rules/latex-language.d.ts.map +1 -1
  80. package/dist/eslint-rules/latex-language.js +10 -13
  81. package/dist/eslint-rules/latex-language.js.map +1 -1
  82. package/dist/eslint-rules/paper-context.d.ts.map +1 -1
  83. package/dist/eslint-rules/paper-context.js +6 -5
  84. package/dist/eslint-rules/paper-context.js.map +1 -1
  85. package/dist/eslint-rules/paper-typography.d.ts +0 -9
  86. package/dist/eslint-rules/paper-typography.d.ts.map +1 -1
  87. package/dist/eslint-rules/paper-typography.js +34 -96
  88. package/dist/eslint-rules/paper-typography.js.map +1 -1
  89. package/dist/eslint-rules/papers.d.ts +1 -3
  90. package/dist/eslint-rules/papers.d.ts.map +1 -1
  91. package/dist/eslint-rules/papers.js +2 -4
  92. package/dist/eslint-rules/papers.js.map +1 -1
  93. package/dist/eslint-rules/tex-build.d.ts.map +1 -1
  94. package/dist/eslint-rules/tex-build.js +8 -25
  95. package/dist/eslint-rules/tex-build.js.map +1 -1
  96. package/dist/init.d.ts +1 -4
  97. package/dist/init.d.ts.map +1 -1
  98. package/dist/init.js +3 -9
  99. package/dist/init.js.map +1 -1
  100. package/dist/latex-log.d.ts +10 -0
  101. package/dist/latex-log.d.ts.map +1 -1
  102. package/dist/latex-log.js +10 -0
  103. package/dist/latex-log.js.map +1 -1
  104. package/dist/latex-loop.d.ts +2 -0
  105. package/dist/latex-loop.d.ts.map +1 -1
  106. package/dist/latex-loop.js.map +1 -1
  107. package/dist/lib/markdown.d.ts +5 -81
  108. package/dist/lib/markdown.d.ts.map +1 -1
  109. package/dist/lib/markdown.js +5 -116
  110. package/dist/lib/markdown.js.map +1 -1
  111. package/dist/lib/paper-config.d.ts.map +1 -1
  112. package/dist/lib/paper-config.js +0 -2
  113. package/dist/lib/paper-config.js.map +1 -1
  114. package/dist/lib/skill-eval-fixture.d.ts +6 -4
  115. package/dist/lib/skill-eval-fixture.d.ts.map +1 -1
  116. package/dist/lib/skill-eval-fixture.js +25 -18
  117. package/dist/lib/skill-eval-fixture.js.map +1 -1
  118. package/dist/new-paper.d.ts +5 -11
  119. package/dist/new-paper.d.ts.map +1 -1
  120. package/dist/new-paper.js +12 -23
  121. package/dist/new-paper.js.map +1 -1
  122. package/dist/package-dirs.d.ts.map +1 -1
  123. package/dist/package-dirs.js +3 -2
  124. package/dist/package-dirs.js.map +1 -1
  125. package/dist/paper-files.d.ts +4 -4
  126. package/dist/paper-includes.d.ts +4 -7
  127. package/dist/paper-includes.d.ts.map +1 -1
  128. package/dist/paper-includes.js +29 -21
  129. package/dist/paper-includes.js.map +1 -1
  130. package/dist/paper-prose.d.ts +17 -0
  131. package/dist/paper-prose.d.ts.map +1 -0
  132. package/dist/paper-prose.js +46 -0
  133. package/dist/paper-prose.js.map +1 -0
  134. package/dist/paper-record.d.ts +25 -0
  135. package/dist/paper-record.d.ts.map +1 -0
  136. package/dist/paper-record.js +39 -0
  137. package/dist/paper-record.js.map +1 -0
  138. package/dist/paper-sources.d.ts +66 -0
  139. package/dist/paper-sources.d.ts.map +1 -0
  140. package/dist/paper-sources.js +92 -0
  141. package/dist/paper-sources.js.map +1 -0
  142. package/dist/ports/bib-reader.d.ts +18 -0
  143. package/dist/ports/bib-reader.d.ts.map +1 -0
  144. package/dist/ports/bib-reader.js +2 -0
  145. package/dist/ports/bib-reader.js.map +1 -0
  146. package/dist/ports/check-references.d.ts +9 -4
  147. package/dist/ports/check-references.d.ts.map +1 -1
  148. package/dist/ports/latex.d.ts +15 -0
  149. package/dist/ports/latex.d.ts.map +1 -1
  150. package/dist/ports/sources-codec.d.ts +11 -0
  151. package/dist/ports/sources-codec.d.ts.map +1 -0
  152. package/dist/ports/sources-codec.js +2 -0
  153. package/dist/ports/sources-codec.js.map +1 -0
  154. package/dist/ports/tex-output.d.ts +17 -0
  155. package/dist/ports/tex-output.d.ts.map +1 -0
  156. package/dist/ports/tex-output.js +2 -0
  157. package/dist/ports/tex-output.js.map +1 -0
  158. package/dist/recorded-bibliography.d.ts +89 -0
  159. package/dist/recorded-bibliography.d.ts.map +1 -0
  160. package/dist/recorded-bibliography.js +136 -0
  161. package/dist/recorded-bibliography.js.map +1 -0
  162. package/dist/reference-rules.d.ts +5 -5
  163. package/dist/reference-rules.d.ts.map +1 -1
  164. package/dist/reference-rules.js +150 -77
  165. package/dist/reference-rules.js.map +1 -1
  166. package/dist/references.d.ts +71 -16
  167. package/dist/references.d.ts.map +1 -1
  168. package/dist/references.js +110 -26
  169. package/dist/references.js.map +1 -1
  170. package/dist/register-bands.d.ts +3 -3
  171. package/dist/register-bands.d.ts.map +1 -1
  172. package/dist/register-bands.js.map +1 -1
  173. package/dist/register.d.ts +2 -2
  174. package/dist/sources-record.d.ts +38 -0
  175. package/dist/sources-record.d.ts.map +1 -0
  176. package/dist/sources-record.js +123 -0
  177. package/dist/sources-record.js.map +1 -0
  178. package/dist/sources-rules.d.ts +44 -0
  179. package/dist/sources-rules.d.ts.map +1 -0
  180. package/dist/sources-rules.js +76 -0
  181. package/dist/sources-rules.js.map +1 -0
  182. package/dist/structure.d.ts.map +1 -1
  183. package/dist/structure.js +4 -3
  184. package/dist/structure.js.map +1 -1
  185. package/dist/tex-paper.d.ts +10 -15
  186. package/dist/tex-paper.d.ts.map +1 -1
  187. package/dist/tex-paper.js +24 -40
  188. package/dist/tex-paper.js.map +1 -1
  189. package/dist/tex-venue-rules.d.ts +3 -3
  190. package/dist/tex-venue-rules.d.ts.map +1 -1
  191. package/dist/tex-venue-rules.js.map +1 -1
  192. package/dist/types.d.ts +0 -2
  193. package/dist/types.d.ts.map +1 -1
  194. package/docs/configuration.md +18 -12
  195. package/docs/design/paper-sources.md +197 -0
  196. package/docs/e2e.md +4 -6
  197. package/docs/package-shape-options.md +1 -1
  198. package/docs/prior-art/paper-folder-scaffolding.md +24 -29
  199. package/docs/references.md +34 -8
  200. package/docs/rules/bib/reachable-entry.md +64 -0
  201. package/docs/rules/paper/sources-fresh.md +81 -0
  202. package/docs/rules/tex/claim-provenance.md +0 -1
  203. package/docs/rules.md +62 -61
  204. package/eslint-rules/latex-language.harness.mjs +3 -3
  205. package/eslint-rules/latex-language.ts +10 -13
  206. package/eslint-rules/paper-context.ts +6 -5
  207. package/eslint-rules/paper-typography.test.mjs +29 -157
  208. package/eslint-rules/paper-typography.ts +35 -104
  209. package/eslint-rules/papers.harness.mjs +1 -1
  210. package/eslint-rules/papers.ts +2 -4
  211. package/eslint-rules/tex-build.harness.mjs +2 -2
  212. package/eslint-rules/tex-build.ts +8 -25
  213. package/eslint.config.mjs +22 -1
  214. package/fixtures/build-e2e/aidc/paper.tex +1 -1
  215. package/fixtures/build-e2e/anon-ack/paper.tex +1 -1
  216. package/fixtures/build-e2e/anon-author/paper.tex +1 -1
  217. package/fixtures/build-e2e/anon-bib/paper.tex +1 -1
  218. package/fixtures/build-e2e/anon-href/paper.tex +1 -1
  219. package/fixtures/build-e2e/anon-macro/paper.tex +1 -1
  220. package/fixtures/build-e2e/anon-pdfauthor/paper.tex +1 -1
  221. package/fixtures/paper-sources/p1/paper.tex +30 -0
  222. package/fixtures/paper-sources/p1/refs.bib +6 -0
  223. package/fixtures/paper-sources/p1/sections/dead.tex +6 -0
  224. package/fixtures/paper-sources/p1/sections/intro.tex +7 -0
  225. package/fixtures/paper-sources/p1/sections/parked.tex +6 -0
  226. package/fixtures/paper-sources/p1/tex-truth.json +37 -0
  227. package/fixtures/paper-sources/p1/versions/2026-01-01-review.tex +4 -0
  228. package/fixtures/paper-sources/v1-stale/paper.tex +14 -0
  229. package/fixtures/paper-sources/v1-stale/refs.bib +6 -0
  230. package/fixtures/paper-sources/v1-stale/tex-truth.json +32 -0
  231. package/fixtures/paper-sources/v10-ifthenelse/anon.bib +1 -0
  232. package/fixtures/paper-sources/v10-ifthenelse/paper.tex +9 -0
  233. package/fixtures/paper-sources/v10-ifthenelse/real.bib +1 -0
  234. package/fixtures/paper-sources/v10-ifthenelse/tex-truth.json +28 -0
  235. package/fixtures/paper-sources/v11-two-blocks/paper.tex +12 -0
  236. package/fixtures/paper-sources/v11-two-blocks/tex-truth.json +30 -0
  237. package/fixtures/paper-sources/v13-percent-text/paper.tex +6 -0
  238. package/fixtures/paper-sources/v13-percent-text/refs.bib +3 -0
  239. package/fixtures/paper-sources/v13-percent-text/tex-truth.json +30 -0
  240. package/fixtures/paper-sources/v15-after-end/paper.tex +6 -0
  241. package/fixtures/paper-sources/v15-after-end/refs.bib +1 -0
  242. package/fixtures/paper-sources/v15-after-end/tex-truth.json +17 -0
  243. package/fixtures/paper-sources/v16-included-block/bibblock.tex +3 -0
  244. package/fixtures/paper-sources/v16-included-block/paper.tex +7 -0
  245. package/fixtures/paper-sources/v16-included-block/refs.bib +1 -0
  246. package/fixtures/paper-sources/v16-included-block/tex-truth.json +35 -0
  247. package/fixtures/paper-sources/v17-include-order/bibsetup.tex +1 -0
  248. package/fixtures/paper-sources/v17-include-order/first.bib +1 -0
  249. package/fixtures/paper-sources/v17-include-order/later.bib +1 -0
  250. package/fixtures/paper-sources/v17-include-order/paper.tex +7 -0
  251. package/fixtures/paper-sources/v17-include-order/tex-truth.json +40 -0
  252. package/fixtures/paper-sources/v2-overwrite/paper.tex +14 -0
  253. package/fixtures/paper-sources/v2-overwrite/refs.bib +6 -0
  254. package/fixtures/paper-sources/v2-overwrite/tex-truth.json +30 -0
  255. package/fixtures/paper-sources/v22-parked-thebibliography/paper.tex +7 -0
  256. package/fixtures/paper-sources/v22-parked-thebibliography/tex-truth.json +17 -0
  257. package/fixtures/paper-sources/v23-parked-include/old.bib +1 -0
  258. package/fixtures/paper-sources/v23-parked-include/paper.tex +7 -0
  259. package/fixtures/paper-sources/v23-parked-include/parked.tex +1 -0
  260. package/fixtures/paper-sources/v23-parked-include/refs.bib +1 -0
  261. package/fixtures/paper-sources/v23-parked-include/tex-truth.json +28 -0
  262. package/fixtures/paper-sources/v24-quoted-fields/paper.tex +6 -0
  263. package/fixtures/paper-sources/v24-quoted-fields/refs.bib +5 -0
  264. package/fixtures/paper-sources/v24-quoted-fields/tex-truth.json +32 -0
  265. package/fixtures/paper-sources/v25-string-differs/paper.tex +10 -0
  266. package/fixtures/paper-sources/v25-string-differs/refs.bib +2 -0
  267. package/fixtures/paper-sources/v25-string-differs/tex-truth.json +32 -0
  268. package/fixtures/paper-sources/v26-comment-in-names/one.bib +1 -0
  269. package/fixtures/paper-sources/v26-comment-in-names/paper.tex +7 -0
  270. package/fixtures/paper-sources/v26-comment-in-names/tex-truth.json +32 -0
  271. package/fixtures/paper-sources/v26-comment-in-names/two.bib +1 -0
  272. package/fixtures/paper-sources/v27-shared-string/abbreviations.bib +2 -0
  273. package/fixtures/paper-sources/v27-shared-string/paper.tex +6 -0
  274. package/fixtures/paper-sources/v27-shared-string/references.bib +6 -0
  275. package/fixtures/paper-sources/v27-shared-string/tex-truth.json +31 -0
  276. package/fixtures/paper-sources/v28-shared-string-block/abbreviations.bib +2 -0
  277. package/fixtures/paper-sources/v28-shared-string-block/paper.tex +14 -0
  278. package/fixtures/paper-sources/v28-shared-string-block/tex-truth.json +33 -0
  279. package/fixtures/paper-sources/v29-generated-input/paper.tex +9 -0
  280. package/fixtures/paper-sources/v29-generated-input/tex-truth.json +24 -0
  281. package/fixtures/paper-sources/v3-declared/paper.bib +6 -0
  282. package/fixtures/paper-sources/v3-declared/paper.tex +6 -0
  283. package/fixtures/paper-sources/v3-declared/tex-truth.json +28 -0
  284. package/fixtures/paper-sources/v30-name-next-line/paper.tex +14 -0
  285. package/fixtures/paper-sources/v30-name-next-line/tex-truth.json +30 -0
  286. package/fixtures/paper-sources/v4-commented/paper.tex +14 -0
  287. package/fixtures/paper-sources/v4-commented/refs.bib +6 -0
  288. package/fixtures/paper-sources/v4-commented/tex-truth.json +28 -0
  289. package/fixtures/paper-sources/v5-percent-entry/paper.tex +20 -0
  290. package/fixtures/paper-sources/v5-percent-entry/tex-truth.json +37 -0
  291. package/fixtures/paper-sources/v6-unclosed/paper.tex +6 -0
  292. package/fixtures/paper-sources/v6-unclosed/refs.bib +5 -0
  293. package/fixtures/paper-sources/v6-unclosed/tex-truth.json +37 -0
  294. package/fixtures/paper-sources/v8-jobname/paper.tex +9 -0
  295. package/fixtures/paper-sources/v8-jobname/tex-truth.json +30 -0
  296. package/fixtures/paper-sources/v9-two-bibliographies/one.bib +1 -0
  297. package/fixtures/paper-sources/v9-two-bibliographies/paper.tex +7 -0
  298. package/fixtures/paper-sources/v9-two-bibliographies/tex-truth.json +35 -0
  299. package/fixtures/paper-sources/v9-two-bibliographies/two.bib +1 -0
  300. package/fixtures/paper-typography/clean-paper/paper.tex +1 -1
  301. package/fixtures/paper-typography/messy-paper/paper.tex +1 -1
  302. package/hooks/hooks.harness.ts +15 -15
  303. package/hooks/paper-edit-guard.hook.mjs +5 -8
  304. package/hooks/paper-edit-guard.test.ts +12 -3
  305. package/hooks/paper-skills-nudge.hook.mjs +2 -3
  306. package/hooks/paper-skills-nudge.test.ts +13 -3
  307. package/hooks/paper-status-gates.hook.mjs +2 -2
  308. package/hooks/paper-status-gates.test.ts +14 -2
  309. package/lib/markdown.test.mjs +5 -60
  310. package/lib/markdown.ts +9 -136
  311. package/lib/paper-config.ts +0 -2
  312. package/lib/skill-checks.test.mjs +4 -1
  313. package/lib/skill-eval-fixture.ts +25 -18
  314. package/package.json +2 -12
  315. package/presets/realm.jsonc +1 -1
  316. package/scripts/check.test.ts +23 -1
  317. package/scripts/check.ts +7 -3
  318. package/scripts/coverage-config.test.ts +19 -10
  319. package/scripts/harness-api.frozen.json +0 -5
  320. package/scripts/layer-legacy-frozen.test.ts +41 -2
  321. package/scripts/layer-legacy-frozen.ts +31 -2
  322. package/skills/cold-read-diff/SKILL.md +3 -3
  323. package/skills/cold-read-diff/SKILL.md.spec.ts +2 -2
  324. package/skills/grade-paper-writing/SKILL.md +15 -22
  325. package/skills/grade-paper-writing/SKILL.md.spec.ts +14 -21
  326. package/skills/grade-paper-writing/__snapshots__/prose-lint.test.ts.snap +40 -26
  327. package/skills/grade-paper-writing/prose-lint.mjs +117 -278
  328. package/skills/grade-paper-writing/prose-lint.test.ts +230 -79
  329. package/skills/paper-adversarial-review/SKILL.md +2 -2
  330. package/skills/paper-adversarial-review/SKILL.md.spec.ts +1 -1
  331. package/skills/paper-pipeline/SKILL.md +9 -12
  332. package/skills/paper-pipeline/SKILL.md.spec.ts +8 -11
  333. package/skills/paper-pipeline/description-language.eval.mjs +25 -15
  334. package/skills/paper-pipeline/framing-vs-vocabulary.eval.mjs +25 -15
  335. package/skills/paper-pipeline/grade-paper-writing-ablation.eval.mjs +25 -15
  336. package/skills/paper-pipeline/pipeline-firing.eval.mjs +27 -16
  337. package/skills/paper-pipeline/pipeline-language.eval.mjs +25 -15
  338. package/skills/paper-pipeline/references/acceptance-gate.md +16 -20
  339. package/skills/paper-pipeline/references/artifact-checklist.md +4 -5
  340. package/skills/paper-pipeline/references/pipeline-status-template.md +1 -1
  341. package/skills/paper-pipeline/scripts/README.md +0 -4
  342. package/skills/paper-pipeline/scripts/extract-ref-facts.harness.mjs +91 -105
  343. package/skills/paper-pipeline/scripts/extract-ref-facts.mjs +85 -241
  344. package/skills/paper-pipeline/scripts/extract-ref-facts.test.mjs +166 -153
  345. package/skills/paper-pipeline/scripts/generated-code.harness.mjs +3 -3
  346. package/skills/paper-status/SKILL.md +2 -2
  347. package/skills/paper-status/SKILL.md.spec.ts +1 -1
  348. package/skills/render-paper/SKILL.md +2 -2
  349. package/skills/render-paper/SKILL.md.spec.ts +1 -1
  350. package/skills/render-paper/extract-pdf-facts.mjs +2 -2
  351. package/skills/tighten-paper/SKILL.md +29 -42
  352. package/skills/tighten-paper/SKILL.md.spec.ts +28 -41
  353. package/skills/verify-citations/SKILL.md +7 -4
  354. package/skills/verify-citations/SKILL.md.spec.ts +6 -3
  355. package/skills/verify-citations/scripts/bib-authors.check.test.mjs +17 -32
  356. package/skills/verify-citations/scripts/bib-authors.cli.test.ts +138 -43
  357. package/skills/verify-citations/scripts/bib-authors.d.mts +22 -2
  358. package/skills/verify-citations/scripts/bib-authors.mjs +54 -193
  359. package/skills/verify-citations/scripts/bib-authors.test.mjs +1 -44
  360. package/skills/verify-citations/scripts/verify-cites.cli.test.ts +16 -0
  361. package/skills/verify-citations/scripts/verify-cites.d.mts +19 -3
  362. package/skills/verify-citations/scripts/verify-cites.mjs +42 -108
  363. package/skills/verify-citations/scripts/verify-cites.test.mjs +23 -36
  364. package/src/adapters/bibtex/index.test.ts +163 -0
  365. package/src/adapters/bibtex/index.ts +155 -0
  366. package/src/adapters/latex/filecontents.ts +114 -0
  367. package/src/adapters/latex/includes.ts +18 -10
  368. package/src/adapters/latex/index.test.ts +73 -0
  369. package/src/adapters/latex/index.ts +2 -0
  370. package/src/adapters/latex/nodes.ts +64 -0
  371. package/src/adapters/latex/parse.test.ts +38 -0
  372. package/src/adapters/latex/parse.ts +67 -6
  373. package/src/adapters/memory/index.ts +17 -0
  374. package/src/adapters/references/index.test.ts +75 -21
  375. package/src/adapters/references/index.ts +37 -32
  376. package/src/adapters/sources-record/index.test.ts +87 -0
  377. package/src/adapters/sources-record/index.ts +82 -0
  378. package/src/adapters/tex-output/blg.ts +27 -0
  379. package/src/adapters/tex-output/fls.ts +24 -0
  380. package/src/adapters/tex-output/index.test.ts +171 -0
  381. package/src/adapters/tex-output/index.ts +12 -0
  382. package/src/bib-rules.test.ts +356 -0
  383. package/src/bib-rules.ts +228 -0
  384. package/src/bibliography-at.test.ts +286 -0
  385. package/src/build.harness.mjs +2 -2
  386. package/src/build.test.ts +211 -1
  387. package/src/build.ts +101 -41
  388. package/src/claim-provenance.test.ts +33 -7
  389. package/src/claim-provenance.ts +2 -2
  390. package/src/cli.harness.mjs +46 -56
  391. package/src/cli.test.ts +44 -48
  392. package/src/cli.ts +51 -82
  393. package/src/domain/paper-source.test.ts +58 -20
  394. package/src/domain/paper-source.ts +38 -42
  395. package/src/domain/paper-sources.ts +94 -0
  396. package/src/domain/sources-record.test.ts +71 -0
  397. package/src/domain/sources-record.ts +136 -0
  398. package/src/domain/tex-run.test.ts +228 -0
  399. package/src/domain/tex-run.ts +139 -0
  400. package/src/folder-venue-rule.test.ts +1 -1
  401. package/src/heading-case.test.ts +24 -10
  402. package/src/init.test.ts +6 -7
  403. package/src/init.ts +3 -22
  404. package/src/latex-log.ts +20 -0
  405. package/src/latex-loop.harness.ts +1 -0
  406. package/src/latex-loop.ts +2 -0
  407. package/src/lint-fix.test.ts +10 -4
  408. package/src/linted-set.test.ts +9 -1
  409. package/src/new-paper.harness.mjs +12 -30
  410. package/src/new-paper.test.ts +15 -11
  411. package/src/new-paper.ts +11 -30
  412. package/src/package-dirs.ts +3 -2
  413. package/src/paper-files.test.ts +5 -4
  414. package/src/paper-files.ts +4 -4
  415. package/src/paper-includes.test.ts +157 -38
  416. package/src/paper-includes.ts +39 -35
  417. package/src/paper-prose.test.ts +78 -0
  418. package/src/paper-prose.ts +67 -0
  419. package/src/paper-record.test.ts +173 -0
  420. package/src/paper-record.ts +66 -0
  421. package/src/paper-settings-commands.test.ts +5 -2
  422. package/src/paper-sources.ts +151 -0
  423. package/src/parse-args.test.ts +0 -1
  424. package/src/ports/bib-reader.ts +27 -0
  425. package/src/ports/check-references.ts +7 -3
  426. package/src/ports/latex.ts +19 -0
  427. package/src/ports/sources-codec.ts +11 -0
  428. package/src/ports/tex-output.ts +17 -0
  429. package/src/recorded-bibliography.ts +297 -0
  430. package/src/reference-rules.ts +217 -104
  431. package/src/references.test.ts +520 -177
  432. package/src/references.ts +185 -37
  433. package/src/register-bands.test.ts +5 -1
  434. package/src/register-bands.ts +3 -5
  435. package/src/register.test.ts +6 -1
  436. package/src/register.ts +2 -2
  437. package/src/rule-cases.test.ts +68 -12
  438. package/src/rule-docs.test.ts +0 -1
  439. package/src/sources-record.test.ts +308 -0
  440. package/src/sources-record.ts +231 -0
  441. package/src/sources-rules.test.ts +152 -0
  442. package/src/sources-rules.ts +117 -0
  443. package/src/structure.harness.ts +5 -11
  444. package/src/structure.ts +4 -3
  445. package/src/tex-paper.test.ts +148 -78
  446. package/src/tex-paper.ts +31 -61
  447. package/src/tex-venue-rules.test.ts +36 -8
  448. package/src/tex-venue-rules.ts +3 -5
  449. package/src/types.ts +0 -2
  450. package/dist/eslint-rules/bib-reachable-entry.d.ts +0 -22
  451. package/dist/eslint-rules/bib-reachable-entry.d.ts.map +0 -1
  452. package/dist/eslint-rules/bib-reachable-entry.js +0 -75
  453. package/dist/eslint-rules/bib-reachable-entry.js.map +0 -1
  454. package/eslint-rules/bib-reachable-entry.ts +0 -76
  455. package/fixtures/real-markdown-paper/baseline.json +0 -22
  456. package/fixtures/real-markdown-paper/baseline.ts +0 -70
  457. package/skills/paper-pipeline/PIPELINE-MAP.md +0 -381
  458. package/skills/paper-pipeline/pipeline-firing.baseline.json +0 -270
  459. package/skills/paper-pipeline/references/sweep-2026-08-09-ideal-pipeline.md +0 -585
  460. package/skills/paper-pipeline/scripts/artifact-coverage.harness.mjs +0 -492
  461. package/skills/paper-pipeline/scripts/artifact-coverage.mjs +0 -397
  462. package/skills/paper-pipeline/scripts/artifact-coverage.test.ts +0 -86
  463. package/skills/paper-pipeline/scripts/check-provenance.mjs +0 -184
  464. package/skills/paper-pipeline/scripts/population-map.harness.mjs +0 -135
  465. package/skills/paper-pipeline/scripts/population-map.mjs +0 -188
  466. package/skills/paper-pipeline/scripts/population-map.test.mjs +0 -154
  467. package/skills/paper-pipeline/scripts/provenance.harness.mjs +0 -266
  468. package/skills/paper-pipeline/scripts/round-diff.harness.ts +0 -907
  469. package/skills/paper-pipeline/scripts/round-diff.mjs +0 -583
  470. package/skills/paper-pipeline/scripts/round-diff.test.mjs +0 -159
  471. package/skills/tighten-paper/__snapshots__/structure.test.ts.snap +0 -133
  472. package/skills/tighten-paper/structure.mjs +0 -364
  473. package/skills/tighten-paper/structure.test.ts +0 -117
  474. package/templates/paper/paper.md +0 -4
@@ -1,6 +1,6 @@
1
1
  /**
2
- * extract-ref-facts.mjs beyond its harness: the edges of each reader (markdown list, .bib, CrossRef,
3
- * arXiv, cache) and the command itself — sources, flags, the network loop (with `fetch` stubbed in
2
+ * extract-ref-facts.mjs beyond its harness: the edges of each reader (.bib, CrossRef, arXiv,
3
+ * cache) and the command itself — sources, flags, the network loop (with `fetch` stubbed in
4
4
  * the child process), and the refusals.
5
5
  */
6
6
  import assert from "node:assert/strict";
@@ -9,18 +9,18 @@ import { dirname, join } from "node:path";
9
9
  import { fileURLToPath } from "node:url";
10
10
  import { test } from "vitest";
11
11
  import { runNode, useTempDir, writeTree } from "../../../test/support.ts";
12
+ import { plantedOnDisk, recordOnDisk } from "../../../test/recorded-fixture.ts";
13
+ import { bibliographyUnreadWhy } from "#src/paper-sources";
12
14
  import {
15
+ bibliographyFrom,
13
16
  buildFacts,
14
17
  extractIds,
15
18
  loadCache,
16
19
  loadEntries,
17
- parseBib,
18
- parseMarkdownRefs,
19
20
  primaryOf,
20
21
  readArxiv,
21
22
  readCrossref,
22
23
  recordFrom,
23
- resolveSource,
24
24
  } from "./extract-ref-facts.mjs";
25
25
 
26
26
  const SCRIPT = join(
@@ -29,72 +29,13 @@ const SCRIPT = join(
29
29
  );
30
30
  const root = useTempDir("extract-ref-facts-");
31
31
 
32
- test("markdown: the list ends at the file's end or a second References heading; lines continue an entry", () => {
33
- assert.deepEqual(parseMarkdownRefs("# P\n\n## References"), []);
34
- const refs = parseMarkdownRefs(
35
- [
36
- "## References",
37
- "",
38
- "1. A. Author, B. Author. *A Title.* Venue,",
39
- " continued 2021.",
40
- "2. *Untitled Authors.* 2019 note",
41
- "3. C. Writer 2018. *Late Year.* Venue",
42
- "4. No title here at all",
43
- "",
44
- "## References",
45
- "5. Never read. *X.* 2000",
46
- ].join("\n"),
47
- );
48
- assert.deepEqual(
49
- refs.map(({ n, line, authors, title, year, venue_text }) => ({
50
- n,
51
- line,
52
- authors,
53
- title,
54
- year,
55
- venue_text,
56
- })),
57
- [
58
- {
59
- n: 1,
60
- line: 3,
61
- authors: ["A. Author", "B. Author"],
62
- title: "A Title",
63
- year: "2021",
64
- venue_text: "Venue, continued 2021.",
65
- },
66
- {
67
- n: 2,
68
- line: 5,
69
- authors: [],
70
- title: "Untitled Authors",
71
- year: "2019",
72
- venue_text: "2019 note",
73
- },
74
- {
75
- n: 3,
76
- line: 6,
77
- authors: ["C. Writer 2018"],
78
- title: "Late Year",
79
- year: "2018",
80
- venue_text: "Venue",
81
- },
82
- {
83
- n: 4,
84
- line: 7,
85
- authors: [],
86
- title: null,
87
- year: null,
88
- venue_text: "No title here at all",
89
- },
90
- ],
91
- );
92
- });
93
-
94
- test(".bib: a key the text spells with spaces has no line; a missing title or year is null, not empty", async () => {
95
- const [entry] = await parseBib(
96
- "@misc{ spaced ,\n note={see arXiv:2101.00001}\n}\n",
32
+ test(".bib: an entry's line is where its `@` stands; a missing title or year is null, not empty", () => {
33
+ const bib = join(root, "lines.bib");
34
+ writeFileSync(
35
+ bib,
36
+ "% a comment\n\n@misc{ spaced ,\n note={see arXiv:2101.00001}\n}\n",
97
37
  );
38
+ const [entry] = loadEntries(bibliographyFrom(bib).texts[0]);
98
39
  assert.deepEqual(
99
40
  {
100
41
  line: entry.line,
@@ -104,7 +45,7 @@ test(".bib: a key the text spells with spaces has no line; a missing title or ye
104
45
  raw: entry.raw,
105
46
  },
106
47
  {
107
- line: 0,
48
+ line: 3,
108
49
  title: null,
109
50
  year: null,
110
51
  authors: [],
@@ -113,52 +54,6 @@ test(".bib: a key the text spells with spaces has no line; a missing title or ye
113
54
  );
114
55
  });
115
56
 
116
- test(".bib from a parser that returns no fields, or list and number fields: joined and printed, not dropped", async () => {
117
- const load = async () => ({
118
- parse: () => ({
119
- entries: [
120
- { key: "bare" },
121
- {
122
- key: "odd",
123
- fields: {
124
- title: ["Split", "Title"],
125
- year: 2021,
126
- note: null,
127
- author: [{ firstName: "Ada", lastName: "Lovelace" }],
128
- },
129
- },
130
- ],
131
- }),
132
- });
133
- const [bare, odd] = await parseBib("@misc{bare,}\n@misc{odd,}\n", { load });
134
- assert.deepEqual(
135
- [
136
- bare,
137
- { title: odd.title, year: odd.year, authors: odd.authors, raw: odd.raw },
138
- ],
139
- [
140
- {
141
- n: 1,
142
- line: 1,
143
- key: "bare",
144
- raw: "",
145
- authors: [],
146
- truncated: false,
147
- title: null,
148
- year: null,
149
- venue_text: "",
150
- doi_field: null,
151
- },
152
- {
153
- title: "Split Title",
154
- year: "2021",
155
- authors: ["Ada Lovelace"],
156
- raw: "Ada Lovelace Split Title 2021",
157
- },
158
- ],
159
- );
160
- });
161
-
162
57
  test("identifiers: a declared DOI field comes first; an entry without text fields has none", () => {
163
58
  const ids = extractIds({
164
59
  doi_field: "10.1111/field",
@@ -244,19 +139,22 @@ test("cache: no path and a corrupt file both read as empty", () => {
244
139
  assert.deepEqual([loadCache(undefined), loadCache(corrupt)], [{}, {}]);
245
140
  });
246
141
 
247
- test("sources: a file names itself; a .bib is read as BibTeX; facts without a cache say so", async () => {
142
+ test("sources: a .bib named is read as itself, as BibTeX; facts without a cache say so", async () => {
248
143
  const bib = join(root, "one.bib");
249
144
  writeFileSync(bib, "@misc{k, title={T}, year={2020}}\n");
250
- assert.equal(resolveSource(bib), bib);
251
- const entries = await loadEntries(bib);
145
+ const found = bibliographyFrom(bib);
146
+ assert.deepEqual([found.ok, found.texts.map((t) => t.path)], [true, [bib]]);
147
+ const entries = loadEntries(found.texts[0]);
252
148
  const facts = buildFacts({
253
- source: bib,
254
- text: "",
149
+ texts: found.texts,
255
150
  entries,
256
151
  cache: {},
257
152
  cachePath: null,
258
153
  });
259
- assert.deepEqual([facts.source_kind, facts.cache], ["bibtex", null]);
154
+ assert.deepEqual(
155
+ [facts.source_kind, facts.cache, facts.sources],
156
+ ["bibtex", null, [bib]],
157
+ );
260
158
  });
261
159
 
262
160
  // ── the command ─────────────────────────────────────────────────────────────
@@ -268,24 +166,59 @@ const CROSSREF_OK = JSON.stringify({
268
166
  issued: { "date-parts": [[2020]] },
269
167
  },
270
168
  });
169
+ /** A paper.tex declaring `\bibliography{refs}`, with `preamble` before its document. */
170
+ const declaring = (preamble = "") =>
171
+ `\\documentclass{article}\n${preamble}\\begin{document}x\\bibliography{refs}\\end{document}\n`;
271
172
  writeTree(root, {
272
- "paper/paper.md":
273
- "## References\n\n1. A. A. *Found.* doi:10.1234/ok\n2. B. B. *Lost.* doi:10.1234/down\n",
274
- "empty/paper.md": "# Nothing cited\n",
173
+ "paper/paper.tex": declaring(),
174
+ "paper/refs.bib":
175
+ "@misc{a, author={A. A}, title={Found}, note={doi:10.1234/ok}}\n" +
176
+ "@misc{b, author={B. B}, title={Lost}, note={doi:10.1234/down}}\n",
177
+ "empty/paper.tex": declaring(),
178
+ "empty/refs.bib": "% nothing cited\n",
179
+ "notes/notes.txt": "A. A. Found. doi:10.1234/ok\n",
275
180
  // `fetch` in the child: 10.1234/down fails at the transport, anything else is a CrossRef hit.
276
181
  "fetch.mjs":
277
182
  `globalThis.fetch = async (url) => {\n` +
278
183
  ` if (url.includes("down")) throw new Error("network down");\n` +
279
184
  ` return { status: 200, text: async () => ${JSON.stringify(CROSSREF_OK)} };\n` +
280
185
  `};\n`,
281
- // The optional .bib parser, uninstalled — and broken in some other way.
282
- "no-parser.mjs":
283
- "import { register } from 'node:module';\n" +
284
- "register('data:text/javascript,' + encodeURIComponent(\"export async function resolve(s, c, next) { if (s === '@retorquere/bibtex-parser') throw Object.assign(new Error('gone'), { code: 'ERR_MODULE_NOT_FOUND' }); return next(s, c); }\"));\n",
285
- "bad-parser.mjs":
286
- "import { register } from 'node:module';\n" +
287
- "register('data:text/javascript,' + encodeURIComponent(\"export async function resolve(s, c, next) { if (s === '@retorquere/bibtex-parser') throw Object.assign(new Error('broken install'), { code: 'EACCES' }); return next(s, c); }\"));\n",
186
+ "bib/paper.tex": declaring(),
187
+ "switched/paper.tex":
188
+ "\\documentclass{article}\\newif\\ifanon\\begin{document}x\\ifanon\\bibliography{anon}\\else\\bibliography{refs}\\fi\\end{document}\n",
189
+ "switched/anon.bib": "@misc{anon1, title={A}, note={doi:10.1234/a}}\n",
190
+ "switched/refs.bib": "@misc{refs1, title={R}, note={doi:10.1234/r}}\n",
288
191
  "bib/refs.bib": "@misc{k, title={T}, note={doi:10.1234/ok}}\n",
192
+ // The bibliography is embedded in paper.tex; the refs.bib beside it is what TeX wrote from the block.
193
+ "inline/paper.tex": declaring(
194
+ "\\begin{filecontents*}[overwrite]{refs.bib}\n" +
195
+ "@misc{a, author={A. A}, title={Found}, note={doi:10.1234/ok}}\n" +
196
+ "@misc{b, author={B. B}, title={Lost}, note={doi:10.1234/down}}\n" +
197
+ "\\end{filecontents*}\n",
198
+ ),
199
+ "inline/refs.bib":
200
+ "@misc{a, author={A. A}, title={Found}, note={doi:10.1234/ok}}\n" +
201
+ "@misc{b, author={B. B}, title={Lost}, note={doi:10.1234/down}}\n",
202
+ // A paper that was never built has no record: nothing says which bibliography TeX reads.
203
+ "unbuilt/paper.tex": declaring(),
204
+ "unbuilt/refs.bib": "@misc{k, title={T}, note={doi:10.1234/ok}}\n",
205
+ });
206
+ /** What a build records for a paper whose bibliography is the file `db` the author keeps. */
207
+ const keeps = (db = "refs.bib") => ({
208
+ inputs: [
209
+ ["paper.tex", "body"],
210
+ [db, "preamble"],
211
+ ],
212
+ databases: [db],
213
+ });
214
+ for (const dir of ["paper", "empty", "bib"])
215
+ recordOnDisk(join(root, dir), keeps());
216
+ // bibtex opened refs.bib only: the switch took its `\else`.
217
+ recordOnDisk(join(root, "switched"), keeps());
218
+ recordOnDisk(join(root, "inline"), {
219
+ inputs: [["paper.tex", "body"]],
220
+ written: ["refs.bib"],
221
+ databases: ["refs.bib"],
289
222
  });
290
223
  const cli = (args, { cwd = root, nodeArgs = [] } = {}) =>
291
224
  runNode(SCRIPT, args, {
@@ -309,7 +242,7 @@ test("online: hits are fetched and cached, a transport failure is named and stay
309
242
  {
310
243
  status: 0,
311
244
  stdout:
312
- "📚 paper.md → paper/_build/refs.facts.json (2 entries, 2 identifiers, 1 resolved, +1 fetched)\n",
245
+ "📚 refs.bib → paper/_build/refs.facts.json (2 entries, 2 identifiers, 1 resolved, +1 fetched)\n",
313
246
  stderr: " … doi:10.1234/down: network down\n",
314
247
  cached: ["doi:10.1234/ok"],
315
248
  },
@@ -321,7 +254,7 @@ test("online again: cached keys are not refetched; --quiet hides the failure; --
321
254
  assert.deepEqual(again, {
322
255
  status: 0,
323
256
  stdout:
324
- "📚 paper.md → paper/_build/refs.facts.json (2 entries, 2 identifiers, 1 resolved, all from cache)\n",
257
+ "📚 refs.bib → paper/_build/refs.facts.json (2 entries, 2 identifiers, 1 resolved, all from cache)\n",
325
258
  stderr: "",
326
259
  });
327
260
  const refresh = cli(["paper", "--refresh", "--quiet"], { nodeArgs: FETCH });
@@ -330,7 +263,7 @@ test("online again: cached keys are not refetched; --quiet hides the failure; --
330
263
 
331
264
  test("the paper file itself as the target, with --cache, --out and --offline", () => {
332
265
  const r = cli([
333
- join(root, "paper/paper.md"),
266
+ join(root, "paper/refs.bib"),
334
267
  "--offline",
335
268
  `--cache=${join(root, "paper/repro/refs-cache.json")}`,
336
269
  `--out=${join(root, "out/facts.json")}`,
@@ -338,7 +271,7 @@ test("the paper file itself as the target, with --cache, --out and --offline", (
338
271
  assert.deepEqual(r, {
339
272
  status: 0,
340
273
  stdout:
341
- "📚 paper.md → out/facts.json (2 entries, 2 identifiers, 1 resolved, offline)\n",
274
+ "📚 refs.bib → out/facts.json (2 entries, 2 identifiers, 1 resolved, offline)\n",
342
275
  stderr: "",
343
276
  });
344
277
  });
@@ -348,33 +281,113 @@ test("no target: the current directory", () => {
348
281
  assert.equal(r.status, 0, r.stderr);
349
282
  });
350
283
 
351
- test("refusals: nowhere to read from, and zero entries (not written as a clean bibliography)", () => {
352
- assert.deepEqual(cli([join(root, "nope"), "--offline"]), {
284
+ test("refusals: no bibliography to read (in bibliographyAt's words), and zero entries (not written as a clean bibliography)", () => {
285
+ // The path → bibliography vocabulary is tested once, beside bibliographyAt (src/paper-sources.ts).
286
+ // Here: that the command speaks it, and stops.
287
+ const notes = join(root, "notes/notes.txt");
288
+ assert.deepEqual(cli([notes, "--offline"]), {
353
289
  status: 1,
354
290
  stdout: "",
355
- stderr: `🛑 no paper.md, draft.md, refs.bib, build/custom.bib under ${join(root, "nope")} — nowhere to take a bibliography from.\n`,
291
+ stderr: `🛑 ${bibliographyUnreadWhy({ kind: "not-bib-or-tex", path: notes })}.\n`,
356
292
  });
357
293
  assert.deepEqual(cli(["empty", "--offline"]), {
358
294
  status: 1,
359
295
  stdout: "",
360
296
  stderr:
361
- "🛑 parsed 0 entries out of empty/paper.md. Silence here would look like a clean bibliography.\n",
297
+ "🛑 parsed 0 entries out of empty/refs.bib. Silence here would look like a clean bibliography.\n",
362
298
  });
363
299
  });
364
300
 
365
- test(".bib without its optional parser: the error names the install; any other failure is not masked", () => {
366
- const missing = cli(["bib", "--offline"], {
367
- nodeArgs: ["--import", join(root, "no-parser.mjs")],
301
+ test("🔴 a paper directory whose paper.tex embeds its .bib: the entries are read from the block that wrote it, where the author edits them", () => {
302
+ const r = cli(["inline", "--offline"]);
303
+ // Guards: only materialized .bib files were looked for, so a paper with its bibliography in
304
+ // `filecontents` was refused before a build and read from a stale refs.bib after an edit.
305
+ assert.deepEqual(r, {
306
+ status: 0,
307
+ stdout:
308
+ "📚 paper.tex → inline/_build/refs.facts.json (2 entries, 2 identifiers, 0 resolved, offline)\n",
309
+ stderr: "",
368
310
  });
369
- assert.equal(missing.status, 1);
370
- assert.match(
371
- missing.stderr,
372
- /parsing \.bib requires @retorquere\/bibtex-parser[\s\S]*Install: {2}npm i -D @retorquere\/bibtex-parser/,
311
+ const facts = JSON.parse(
312
+ readFileSync(join(root, "inline/_build/refs.facts.json"), "utf8"),
313
+ );
314
+ // Each entry's line is its line in paper.tex, where the author edits it.
315
+ assert.deepEqual(
316
+ facts.entries.map((e) => [e.file, e.key, e.line]),
317
+ [
318
+ ["inline/paper.tex", "a", 3],
319
+ ["inline/paper.tex", "b", 4],
320
+ ],
373
321
  );
374
- const broken = cli(["bib", "--offline"], {
375
- nodeArgs: ["--import", join(root, "bad-parser.mjs")],
322
+ });
323
+
324
+ test("a bibliography behind a switch: the database bibtex opened is read, not every candidate", () => {
325
+ assert.deepEqual(cli(["switched", "--offline"]), {
326
+ status: 0,
327
+ stdout:
328
+ "📚 refs.bib → switched/_build/refs.facts.json (1 entries, 1 identifiers, 0 resolved, offline)\n",
329
+ stderr: "",
376
330
  });
377
- assert.equal(broken.status, 1);
378
- assert.match(broken.stderr, /broken install/);
379
- assert.doesNotMatch(broken.stderr, /npm i -D/);
331
+ });
332
+
333
+ test("🔴 a paper that was not built is refused — run `npx paperlint build` first — never read from its source", () => {
334
+ assert.deepEqual(cli(["unbuilt", "--offline"]), {
335
+ status: 1,
336
+ stdout: "",
337
+ stderr: `🛑 ${join(root, "unbuilt")} has not been built — run \`npx paperlint build\` first, which records the databases bibtex reads.\n`,
338
+ });
339
+ });
340
+
341
+ test("an embedded entry's line is its line in paper.tex", () => {
342
+ const block = "@misc{ spaced ,\n note={arXiv:2101.00001}\n}\n";
343
+ writeTree(root, {
344
+ "spaced/paper.tex": declaring(
345
+ `\\begin{filecontents*}{refs.bib}\n${block}\\end{filecontents*}\n`,
346
+ ),
347
+ "spaced/refs.bib": block,
348
+ });
349
+ recordOnDisk(join(root, "spaced"), {
350
+ inputs: [["paper.tex", "body"]],
351
+ written: ["refs.bib"],
352
+ databases: ["refs.bib"],
353
+ });
354
+ const found = bibliographyFrom(join(root, "spaced", "paper.tex"));
355
+ assert.deepEqual(
356
+ loadEntries(found.texts[0]).map((e) => e.line),
357
+ [3],
358
+ );
359
+ });
360
+
361
+ // ── the planted papers of fixtures/paper-sources, laid out as a build leaves them ─────
362
+
363
+ test.each([
364
+ // [paper, the file read, the keys read] — tex-truth.json beside each paper is TeX's own answer: the
365
+ // databases bibtex opened. A `.bib` TeX wrote from a block is read from the block (paper.tex).
366
+ ["v1-stale", "refs.bib", ["stale2020"]],
367
+ ["v2-overwrite", "paper.tex", ["inline2024"]],
368
+ ["v3-declared", "paper.bib", ["declared2023"]],
369
+ ["v4-commented", "refs.bib", ["stale2020"]],
370
+ // What bibtex reads beyond these — a malformed entry, or one behind `%` or inside `@comment{…}` —
371
+ // the build's post-build check names (src/references.ts).
372
+ ["v5-percent-entry", "paper.tex", ["inline2024"]],
373
+ ["v6-unclosed", "refs.bib", ["a1", "a3"]],
374
+ ["v13-percent-text", "refs.bib", ["ok1"]],
375
+ ["v8-jobname", "paper.tex", ["jkey"]],
376
+ ])("%s: the bibliography bibtex opened", (paper, file, keys) => {
377
+ const dir = join(root, "planted", paper);
378
+ plantedOnDisk(dir, paper);
379
+ const out = join(root, "planted", `${paper}.json`);
380
+ const r = cli([
381
+ dir,
382
+ "--offline",
383
+ `--out=${out}`,
384
+ `--cache=${join(root, "planted", "cache.json")}`,
385
+ ]);
386
+ assert.deepEqual([r.status, r.stderr], [0, ""]);
387
+ assert.match(r.stdout, new RegExp(`^📚 ${file} → `));
388
+ const facts = JSON.parse(readFileSync(out, "utf8"));
389
+ assert.deepEqual(
390
+ facts.entries.map((e) => e.key),
391
+ keys,
392
+ );
380
393
  });
@@ -68,7 +68,7 @@ function run(dir, flagsOnly = true) {
68
68
  function fixture({ scripts = {}, allow = null, bundle = {}, at = "repro" }) {
69
69
  const d = join(tmp, `f${++n}`);
70
70
  mkdirSync(d, { recursive: true });
71
- writeFileSync(join(d, "paper.md"), "## 1. x\n\nprose\n");
71
+ writeFileSync(join(d, "paper.tex"), "\\section{x}\nprose\n");
72
72
  if (at === null) return d;
73
73
  mkdirSync(join(d, at), { recursive: true });
74
74
  for (const [p, src] of Object.entries(scripts)) {
@@ -142,13 +142,13 @@ function fixture({ scripts = {}, allow = null, bundle = {}, at = "repro" }) {
142
142
  fixture({
143
143
  scripts: {
144
144
  "wc.py":
145
- 'text = open("/home/example/papers/paper.md").read()\nprint(len(text.split()))\n',
145
+ 'text = open("/home/example/papers/paper.tex").read()\nprint(len(text.split()))\n',
146
146
  },
147
147
  }),
148
148
  );
149
149
  assert.match(
150
150
  r.out,
151
- /ABS_PATH\s+repro\/wc\.py — hard-codes 1 absolute path\(s\), e\.g\. `\/home\/example\/papers\/paper\.md`/,
151
+ /ABS_PATH\s+repro\/wc\.py — hard-codes 1 absolute path\(s\), e\.g\. `\/home\/example\/papers\/paper\.tex`/,
152
152
  "a script that only runs on its author's laptop shipped in the reproduction directory unremarked:\n" +
153
153
  r.out,
154
154
  );
@@ -4,7 +4,7 @@ description: Answer "what's the status of the paper?" in one pass — MEASURE wh
4
4
  allowed-tools: [Bash, Read, Grep, Glob]
5
5
  ---
6
6
 
7
- <!-- vigiles:sha256:cdf5e1fcefdb916d compiled from skills/paper-status/SKILL.md.spec.ts -->
7
+ <!-- vigiles:sha256:c90bd362b565fa22 compiled from skills/paper-status/SKILL.md.spec.ts -->
8
8
 
9
9
  # paper-status — state, measured before it is narrated
10
10
 
@@ -65,7 +65,7 @@ git log --oneline @{u}..HEAD # unpushed commits
65
65
  A gate is stale if the paper changed after the gate ran. Check it mechanically, not by recall:
66
66
 
67
67
  ```bash
68
- git log -1 --format=%cd --date=short -- paper.md # when the text last moved
68
+ git log -1 --format=%cd --date=short -- paper.tex # when the text last moved
69
69
  ls -la reviews/ | tail -20 # when each gate last reported
70
70
  ```
71
71
 
@@ -71,7 +71,7 @@ git log --oneline @{u}..HEAD # unpushed commits
71
71
  A gate is stale if the paper changed after the gate ran. Check it mechanically, not by recall:
72
72
 
73
73
  \`\`\`bash
74
- git log -1 --format=%cd --date=short -- paper.md # when the text last moved
74
+ git log -1 --format=%cd --date=short -- paper.tex # when the text last moved
75
75
  ls -la reviews/ | tail -20 # when each gate last reported
76
76
  \`\`\`
77
77
 
@@ -4,7 +4,7 @@ description: Compile a LaTeX paper (ACM/IEEE/arXiv) to PDF and render its pages
4
4
  allowed-tools: [Read, Write, Edit, Grep, Glob, Bash, SendUserFile]
5
5
  ---
6
6
 
7
- <!-- vigiles:sha256:5e2ee8850c098375 compiled from skills/render-paper/SKILL.md.spec.ts -->
7
+ <!-- vigiles:sha256:ff8dca1e1be3977e compiled from skills/render-paper/SKILL.md.spec.ts -->
8
8
 
9
9
  # render-paper — .tex → PDF → readable page PNGs
10
10
 
@@ -15,7 +15,7 @@ first try.
15
15
  ## 🔴 This is a COMMAND, not a stage
16
16
 
17
17
  You run it twenty times a day. It has no position in the pipeline and nothing waits its turn to reach
18
- it: **any `.tex` / `paper.md` edit is the trigger** — render, then eyeball pages 1–2 before believing
18
+ it: **any `.tex` edit is the trigger** — render, then eyeball pages 1–2 before believing
19
19
  anything you say about the paper. Scorecard row **`render`** (in the CONTINUOUS section of
20
20
  `paper-pipeline`) records the last render and its page count; that page count is the hard input the
21
21
  `tighten-paper` structure gate is blocked on, which is the only sense in which anything "comes after"
@@ -21,7 +21,7 @@ first try.
21
21
  ## 🔴 This is a COMMAND, not a stage
22
22
 
23
23
  You run it twenty times a day. It has no position in the pipeline and nothing waits its turn to reach
24
- it: **any \`.tex\` / \`paper.md\` edit is the trigger** — render, then eyeball pages 1–2 before believing
24
+ it: **any \`.tex\` edit is the trigger** — render, then eyeball pages 1–2 before believing
25
25
  anything you say about the paper. Scorecard row **\`render\`** (in the CONTINUOUS section of
26
26
  \`paper-pipeline\`) records the last render and its page count; that page count is the hard input the
27
27
  \`tighten-paper\` structure gate is blocked on, which is the only sense in which anything "comes after"
@@ -56,8 +56,8 @@ const ROOT = process.env.CLAUDE_PROJECT_DIR || process.cwd();
56
56
  /**
57
57
  * Accept either a path to a PDF or a PAPER DIRECTORY — and in the second case find the artifact on
58
58
  * its own: the path the paper declares in the `pdf` field of `paperlint.json`, else `paper.pdf` beside
59
- * it. A paper written in markdown and built by its own script into `build/acl_latex.pdf` would
60
- * otherwise be skipped silently. The facts ALWAYS go into `<paper directory>/_build/`, not next to
59
+ * it. A paper built by its own script into `build/acl_latex.pdf` would otherwise be skipped
60
+ * silently. The facts ALWAYS go into `<paper directory>/_build/`, not next to
61
61
  * the PDF, where neither a rule's glob nor a human looks.
62
62
  */
63
63
  export function resolveTarget(arg) {
@@ -4,7 +4,7 @@ description: "Use when a paper \"feels long / bloated / overly complex / hard to
4
4
  allowed-tools: [Read, Write, Edit, Grep, Glob, Agent]
5
5
  ---
6
6
 
7
- <!-- vigiles:sha256:fd00cde33ea4b81c compiled from skills/tighten-paper/SKILL.md.spec.ts -->
7
+ <!-- vigiles:sha256:c57d6da372b28acf compiled from skills/tighten-paper/SKILL.md.spec.ts -->
8
8
 
9
9
  # tighten-paper — the developmental edit
10
10
 
@@ -34,31 +34,12 @@ counter, is not this skill; it is the damage this skill exists to undo.
34
34
  If you are invoked while the author is still adding material, say so and decline the pass. The right
35
35
  answer is "not yet", not a smaller cut.
36
36
 
37
- ## 🏗 START WITH THE MECHANICAL LEG — run `structure.mjs` (added 2026-08-05)
37
+ ## 🏗 START WITH THE SHAPE — weigh every section before judging it
38
38
 
39
- ```
40
- node .claude/skills/tighten-paper/structure.mjs <paper.md> # whole paper
41
- node .claude/skills/tighten-paper/structure.mjs <paper.md> --section=3 # one section
42
- ```
43
-
44
- 🔴 **THE SCRIPT NO LONGER FINDS ANYTHING — it is an INVENTORY (2026-08-26).** All sixteen of its
45
- checks moved into ESLint rules (`eslint-rules/paper-structure.mjs`). The findings you used to read
46
- here now come from:
47
-
48
- ```
49
- npx eslint --no-config-lookup --config eslint.config.mjs <paper.md>
50
- ```
51
-
52
- ⚠️ Do NOT pass `--flags-only`. It is kept only because two harnesses assert on its silence, and it
53
- prints nothing and exits 0 whatever the paper looks like. Measured 26.08 on `the reference paper`:
54
- `--flags-only` printed 0 lines while ESLint on the same bytes reported **6 structural findings**
55
- (subsection-size ×2 · section-lead · free-section-size ×2 · block-ungraded). A mode that cannot
56
- speak is not a check, and reading its silence as "clean" is the defect this pipeline is named for.
57
-
58
- What the inventory prints, and why it stays a script: the outline **in order** — every heading, its
59
- own words, its total with subsections, its share of the body, and the `carries:`/`score:`/`verdict:`
60
- note the section wrote about itself, printed beside its weight. That comparison is the whole point
61
- and a linter cannot make it: a finding has nowhere to put a table.
39
+ Write the outline out **in order**: every heading, its own words before its first subsection, its
40
+ total with subsections, its share of the body — and beside each, the `carries:`/`score:`/`verdict:`
41
+ note the section wrote about itself. That comparison, a section's weight printed next to what it
42
+ claims about itself, is the whole point.
62
43
 
63
44
  🔴 **Why this exists, and why word counts alone mislead.** Asked whether a paper short-changed its
64
45
  contribution, per-section word counts said no — the contribution had 38% more words than related
@@ -67,7 +48,7 @@ owned **0, 28 and 38** words before their first subsection. Nothing in the toolc
67
48
  question, because every metric was per-sentence or per-document and none was per-section. The author's
68
49
  word for the result was a "salad" — a jumbled mix — and he was right while the numbers said fine.
69
50
 
70
- **What the script does NOT decide, and you must:** the ORDER, the NAMES, and whether a section earns
51
+ **What the count does NOT decide, and you must:** the ORDER, the NAMES, and whether a section earns
71
52
  its place. Those are judgement, and pretending otherwise would be the failure this whole pipeline
72
53
  exists to prevent.
73
54
 
@@ -188,18 +169,24 @@ called it "fat for no reason" after a pass that cut almost nothing and left it 1
188
169
 
189
170
  A verdict that lives in a chat log is gone by the next session, and the next session then re-derives
190
171
  it or ignores it. **Every section and subsection gets a one-line verdict comment immediately above its
191
- heading**, in the paper source, stamped with the date and the skill that produced it:
192
-
193
- ```
194
- <!-- TIGHTEN 2026-08-06 · score 8/10 · verdict: KEEP · 190w
195
- carries: tools get close but none resolves a NAMED rule against a committed config
196
- because: it is the only place the tool-ecosystem gap is closed; a reviewer who skips it
197
- asks "hasn't this been done" and gets no answer -->
198
- <!-- TIGHTEN 2026-08-06 · score 5/10 · verdict: SHORTEN -89w · 210w
199
- carries: why a linter for the rules file cannot work
200
- because: the point lands in two sentences; the rest re-establishes what §2 already did -->
201
- <!-- TIGHTEN 2026-08-06 · score 2/10 · verdict: MOVE→artifact · 45w
202
- carries: nothing the reader must hold to judge the contribution -->
172
+ heading**, in `paper.tex`, as LaTeX `%` comments, stamped with the date and the skill that
173
+ produced it:
174
+
175
+ ```latex
176
+ % TIGHTEN 2026-08-06 · score 8/10 · verdict: KEEP · 190w
177
+ % carries: tools get close but none resolves a NAMED rule against a committed config
178
+ % because: it is the only place the tool-ecosystem gap is closed; a reviewer who skips it
179
+ % asks "hasn't this been done" and gets no answer
180
+ \section{Related tools}
181
+
182
+ % TIGHTEN 2026-08-06 · score 5/10 · verdict: SHORTEN -89w · 210w
183
+ % carries: why a linter for the rules file cannot work
184
+ % because: the point lands in two sentences; the rest re-establishes what §2 already did
185
+ \subsection{Why not a linter}
186
+
187
+ % TIGHTEN 2026-08-06 · score 2/10 · verdict: MOVE→artifact · 45w
188
+ % carries: nothing the reader must hold to judge the contribution
189
+ \subsection{Implementation notes}
203
190
  ```
204
191
 
205
192
  🔴 **Before assigning any `MOVE→` verdict, read `paper-pipeline/references/body-vs-appendix.md`.**
@@ -224,7 +211,7 @@ written and **not one ever concluded "cut me"**. Author, 2026-08-06, reading the
224
211
  value on every page and compared nothing.
225
212
 
226
213
  The pairing is the mechanism: **a low score beside a KEEP verdict is a visible contradiction**, and
227
- `structure.mjs` reports it. Prose cannot contradict itself; a number can.
214
+ the outline above puts the two side by side. Prose cannot contradict itself; a number can.
228
215
 
229
216
  **Calibration, so the scale does not drift to "everything is an 8":**
230
217
  - **9–10** — remove it and a reviewer cannot assess the contribution. The claim, its evidence, its price.
@@ -279,10 +266,10 @@ Rules for these comments:
279
266
  unpriced prose accumulates, because nothing there costs a page.
280
267
  - **Stale beats absent, but not by much.** A comment whose date precedes the last substantive edit to
281
268
  its section is stale; re-grade it rather than trusting it.
282
- - **A verdict nobody executed is a note, not a decision.** `structure.mjs` reports blocks still
283
- carrying a non-KEEP verdict — a paper full of unexecuted CUTs is worse than one never graded,
269
+ - **A verdict nobody executed is a note, not a decision.** List the blocks still carrying a
270
+ non-KEEP verdict — a paper full of unexecuted CUTs is worse than one never graded,
284
271
  because it reads as decided.
285
- - They are working notes: stripped before typesetting, like every other `<!-- -->` block.
272
+ - They are working notes: a `%` comment never reaches the PDF.
286
273
  - The chat summary still gets written — but it is the *derivative*, and the paper is the record.
287
274
 
288
275
  ## 🧱 Enforce subsections — a section without them is a wall