paperlint 8.0.0 → 9.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (393) hide show
  1. package/.github/CLAUDE.md +22 -0
  2. package/.github/CLAUDE.md.spec.ts +20 -0
  3. package/CLAUDE.md +59 -531
  4. package/CONTRIBUTING.md +7 -6
  5. package/README.md +4 -3
  6. package/dist/CLAUDE.md.spec.d.ts +3 -0
  7. package/dist/CLAUDE.md.spec.d.ts.map +1 -0
  8. package/dist/CLAUDE.md.spec.js +62 -0
  9. package/dist/CLAUDE.md.spec.js.map +1 -0
  10. package/dist/adapters/bibtex/index.d.ts +3 -0
  11. package/dist/adapters/bibtex/index.d.ts.map +1 -0
  12. package/dist/adapters/bibtex/index.js +103 -0
  13. package/dist/adapters/bibtex/index.js.map +1 -0
  14. package/dist/adapters/latex/filecontents.d.ts +33 -0
  15. package/dist/adapters/latex/filecontents.d.ts.map +1 -0
  16. package/dist/adapters/latex/filecontents.js +65 -0
  17. package/dist/adapters/latex/filecontents.js.map +1 -0
  18. package/dist/adapters/latex/includes.d.ts +3 -3
  19. package/dist/adapters/latex/includes.d.ts.map +1 -1
  20. package/dist/adapters/latex/includes.js +18 -10
  21. package/dist/adapters/latex/includes.js.map +1 -1
  22. package/dist/adapters/latex/index.d.ts.map +1 -1
  23. package/dist/adapters/latex/index.js +2 -0
  24. package/dist/adapters/latex/index.js.map +1 -1
  25. package/dist/adapters/latex/nodes.d.ts +12 -0
  26. package/dist/adapters/latex/nodes.d.ts.map +1 -1
  27. package/dist/adapters/latex/nodes.js +39 -0
  28. package/dist/adapters/latex/nodes.js.map +1 -1
  29. package/dist/adapters/latex/parse.d.ts +7 -1
  30. package/dist/adapters/latex/parse.d.ts.map +1 -1
  31. package/dist/adapters/latex/parse.js +55 -6
  32. package/dist/adapters/latex/parse.js.map +1 -1
  33. package/dist/adapters/memory/index.d.ts +3 -0
  34. package/dist/adapters/memory/index.d.ts.map +1 -1
  35. package/dist/adapters/memory/index.js +9 -0
  36. package/dist/adapters/memory/index.js.map +1 -1
  37. package/dist/adapters/references/index.d.ts.map +1 -1
  38. package/dist/adapters/references/index.js +27 -24
  39. package/dist/adapters/references/index.js.map +1 -1
  40. package/dist/adapters/sources-record/index.d.ts +9 -0
  41. package/dist/adapters/sources-record/index.d.ts.map +1 -0
  42. package/dist/adapters/sources-record/index.js +68 -0
  43. package/dist/adapters/sources-record/index.js.map +1 -0
  44. package/dist/adapters/tex-output/blg.d.ts +9 -0
  45. package/dist/adapters/tex-output/blg.d.ts.map +1 -0
  46. package/dist/adapters/tex-output/blg.js +20 -0
  47. package/dist/adapters/tex-output/blg.js.map +1 -0
  48. package/dist/adapters/tex-output/fls.d.ts +8 -0
  49. package/dist/adapters/tex-output/fls.d.ts.map +1 -0
  50. package/dist/adapters/tex-output/fls.js +16 -0
  51. package/dist/adapters/tex-output/fls.js.map +1 -0
  52. package/dist/adapters/tex-output/index.d.ts +7 -0
  53. package/dist/adapters/tex-output/index.d.ts.map +1 -0
  54. package/dist/adapters/tex-output/index.js +7 -0
  55. package/dist/adapters/tex-output/index.js.map +1 -0
  56. package/dist/bib-rules.d.ts +47 -0
  57. package/dist/bib-rules.d.ts.map +1 -0
  58. package/dist/bib-rules.js +127 -0
  59. package/dist/bib-rules.js.map +1 -0
  60. package/dist/build.d.ts +29 -1
  61. package/dist/build.d.ts.map +1 -1
  62. package/dist/build.js +41 -25
  63. package/dist/build.js.map +1 -1
  64. package/dist/claim-provenance.d.ts +2 -2
  65. package/dist/cli.d.ts +6 -1
  66. package/dist/cli.d.ts.map +1 -1
  67. package/dist/cli.js +30 -26
  68. package/dist/cli.js.map +1 -1
  69. package/dist/domain/paper-source.d.ts +1 -10
  70. package/dist/domain/paper-source.d.ts.map +1 -1
  71. package/dist/domain/paper-source.js +34 -24
  72. package/dist/domain/paper-source.js.map +1 -1
  73. package/dist/domain/paper-sources.d.ts +72 -0
  74. package/dist/domain/paper-sources.d.ts.map +1 -0
  75. package/dist/domain/paper-sources.js +18 -0
  76. package/dist/domain/paper-sources.js.map +1 -0
  77. package/dist/domain/sources-record.d.ts +102 -0
  78. package/dist/domain/sources-record.d.ts.map +1 -0
  79. package/dist/domain/sources-record.js +38 -0
  80. package/dist/domain/sources-record.js.map +1 -0
  81. package/dist/domain/tex-run.d.ts +25 -0
  82. package/dist/domain/tex-run.d.ts.map +1 -0
  83. package/dist/domain/tex-run.js +79 -0
  84. package/dist/domain/tex-run.js.map +1 -0
  85. package/dist/eslint-rules/CLAUDE.md.spec.d.ts +3 -0
  86. package/dist/eslint-rules/CLAUDE.md.spec.d.ts.map +1 -0
  87. package/dist/eslint-rules/CLAUDE.md.spec.js +39 -0
  88. package/dist/eslint-rules/CLAUDE.md.spec.js.map +1 -0
  89. package/dist/eslint-rules/install-path-literals.d.ts +1 -1
  90. package/dist/eslint-rules/install-path-literals.js +4 -4
  91. package/dist/eslint-rules/install-path-literals.js.map +1 -1
  92. package/dist/eslint-rules/paper-context.d.ts.map +1 -1
  93. package/dist/eslint-rules/paper-context.js +4 -3
  94. package/dist/eslint-rules/paper-context.js.map +1 -1
  95. package/dist/eslint-rules/paper-typography.d.ts +0 -9
  96. package/dist/eslint-rules/paper-typography.d.ts.map +1 -1
  97. package/dist/eslint-rules/paper-typography.js +12 -5
  98. package/dist/eslint-rules/paper-typography.js.map +1 -1
  99. package/dist/eslint-rules/pdf-last-page-balance.js +1 -1
  100. package/dist/latex-log.d.ts +10 -0
  101. package/dist/latex-log.d.ts.map +1 -1
  102. package/dist/latex-log.js +10 -0
  103. package/dist/latex-log.js.map +1 -1
  104. package/dist/latex-loop.d.ts +2 -0
  105. package/dist/latex-loop.d.ts.map +1 -1
  106. package/dist/latex-loop.js.map +1 -1
  107. package/dist/lib/agent-cli-version.js +1 -1
  108. package/dist/lib/agent-cli-version.js.map +1 -1
  109. package/dist/package-dirs.d.ts.map +1 -1
  110. package/dist/package-dirs.js +3 -2
  111. package/dist/package-dirs.js.map +1 -1
  112. package/dist/paper-includes.d.ts +4 -7
  113. package/dist/paper-includes.d.ts.map +1 -1
  114. package/dist/paper-includes.js +29 -21
  115. package/dist/paper-includes.js.map +1 -1
  116. package/dist/paper-prose.d.ts +2 -2
  117. package/dist/paper-record.d.ts +25 -0
  118. package/dist/paper-record.d.ts.map +1 -0
  119. package/dist/paper-record.js +39 -0
  120. package/dist/paper-record.js.map +1 -0
  121. package/dist/paper-sources.d.ts +66 -0
  122. package/dist/paper-sources.d.ts.map +1 -0
  123. package/dist/paper-sources.js +92 -0
  124. package/dist/paper-sources.js.map +1 -0
  125. package/dist/ports/bib-reader.d.ts +18 -0
  126. package/dist/ports/bib-reader.d.ts.map +1 -0
  127. package/dist/ports/bib-reader.js +2 -0
  128. package/dist/ports/bib-reader.js.map +1 -0
  129. package/dist/ports/check-references.d.ts +9 -4
  130. package/dist/ports/check-references.d.ts.map +1 -1
  131. package/dist/ports/latex.d.ts +15 -0
  132. package/dist/ports/latex.d.ts.map +1 -1
  133. package/dist/ports/sources-codec.d.ts +11 -0
  134. package/dist/ports/sources-codec.d.ts.map +1 -0
  135. package/dist/ports/sources-codec.js +2 -0
  136. package/dist/ports/sources-codec.js.map +1 -0
  137. package/dist/ports/tex-output.d.ts +17 -0
  138. package/dist/ports/tex-output.d.ts.map +1 -0
  139. package/dist/ports/tex-output.js +2 -0
  140. package/dist/ports/tex-output.js.map +1 -0
  141. package/dist/recorded-bibliography.d.ts +89 -0
  142. package/dist/recorded-bibliography.d.ts.map +1 -0
  143. package/dist/recorded-bibliography.js +136 -0
  144. package/dist/recorded-bibliography.js.map +1 -0
  145. package/dist/reference-rules.d.ts +5 -5
  146. package/dist/reference-rules.d.ts.map +1 -1
  147. package/dist/reference-rules.js +150 -77
  148. package/dist/reference-rules.js.map +1 -1
  149. package/dist/references.d.ts +71 -16
  150. package/dist/references.d.ts.map +1 -1
  151. package/dist/references.js +110 -26
  152. package/dist/references.js.map +1 -1
  153. package/dist/register-bands.d.ts +3 -3
  154. package/dist/register-bands.d.ts.map +1 -1
  155. package/dist/register-bands.js.map +1 -1
  156. package/dist/register.d.ts +2 -2
  157. package/dist/sources-record.d.ts +38 -0
  158. package/dist/sources-record.d.ts.map +1 -0
  159. package/dist/sources-record.js +123 -0
  160. package/dist/sources-record.js.map +1 -0
  161. package/dist/sources-rules.d.ts +44 -0
  162. package/dist/sources-rules.d.ts.map +1 -0
  163. package/dist/sources-rules.js +76 -0
  164. package/dist/sources-rules.js.map +1 -0
  165. package/dist/tex-paper.d.ts +10 -15
  166. package/dist/tex-paper.d.ts.map +1 -1
  167. package/dist/tex-paper.js +24 -40
  168. package/dist/tex-paper.js.map +1 -1
  169. package/dist/tex-requirements.js +1 -1
  170. package/dist/tex-venue-rules.d.ts +3 -3
  171. package/dist/tex-venue-rules.d.ts.map +1 -1
  172. package/dist/tex-venue-rules.js.map +1 -1
  173. package/dist/toolchain.js +1 -1
  174. package/docs/configuration.md +12 -4
  175. package/docs/design/paper-sources.md +197 -0
  176. package/docs/e2e.md +1 -1
  177. package/docs/incidents.md +5 -5
  178. package/docs/package-shape-options.md +1 -1
  179. package/docs/references.md +34 -8
  180. package/docs/rules/bib/reachable-entry.md +64 -0
  181. package/docs/rules/paper/sources-fresh.md +81 -0
  182. package/docs/rules.md +62 -57
  183. package/eslint-rules/CLAUDE.md +44 -0
  184. package/eslint-rules/CLAUDE.md.spec.ts +43 -0
  185. package/eslint-rules/install-path-literals.harness.mjs +1 -1
  186. package/eslint-rules/install-path-literals.ts +4 -4
  187. package/eslint-rules/paper-context.ts +4 -3
  188. package/eslint-rules/paper-typography.test.mjs +12 -72
  189. package/eslint-rules/paper-typography.ts +13 -6
  190. package/eslint-rules/pdf-last-page-balance.ts +1 -1
  191. package/eslint.config.mjs +28 -3
  192. package/fixtures/build-e2e/aidc/paper.tex +1 -1
  193. package/fixtures/build-e2e/anon-ack/paper.tex +1 -1
  194. package/fixtures/build-e2e/anon-author/paper.tex +1 -1
  195. package/fixtures/build-e2e/anon-bib/paper.tex +1 -1
  196. package/fixtures/build-e2e/anon-href/paper.tex +1 -1
  197. package/fixtures/build-e2e/anon-macro/paper.tex +1 -1
  198. package/fixtures/build-e2e/anon-pdfauthor/paper.tex +1 -1
  199. package/fixtures/paper-sources/p1/paper.tex +30 -0
  200. package/fixtures/paper-sources/p1/refs.bib +6 -0
  201. package/fixtures/paper-sources/p1/sections/dead.tex +6 -0
  202. package/fixtures/paper-sources/p1/sections/intro.tex +7 -0
  203. package/fixtures/paper-sources/p1/sections/parked.tex +6 -0
  204. package/fixtures/paper-sources/p1/tex-truth.json +37 -0
  205. package/fixtures/paper-sources/p1/versions/2026-01-01-review.tex +4 -0
  206. package/fixtures/paper-sources/v1-stale/paper.tex +14 -0
  207. package/fixtures/paper-sources/v1-stale/refs.bib +6 -0
  208. package/fixtures/paper-sources/v1-stale/tex-truth.json +32 -0
  209. package/fixtures/paper-sources/v10-ifthenelse/anon.bib +1 -0
  210. package/fixtures/paper-sources/v10-ifthenelse/paper.tex +9 -0
  211. package/fixtures/paper-sources/v10-ifthenelse/real.bib +1 -0
  212. package/fixtures/paper-sources/v10-ifthenelse/tex-truth.json +28 -0
  213. package/fixtures/paper-sources/v11-two-blocks/paper.tex +12 -0
  214. package/fixtures/paper-sources/v11-two-blocks/tex-truth.json +30 -0
  215. package/fixtures/paper-sources/v13-percent-text/paper.tex +6 -0
  216. package/fixtures/paper-sources/v13-percent-text/refs.bib +3 -0
  217. package/fixtures/paper-sources/v13-percent-text/tex-truth.json +30 -0
  218. package/fixtures/paper-sources/v15-after-end/paper.tex +6 -0
  219. package/fixtures/paper-sources/v15-after-end/refs.bib +1 -0
  220. package/fixtures/paper-sources/v15-after-end/tex-truth.json +17 -0
  221. package/fixtures/paper-sources/v16-included-block/bibblock.tex +3 -0
  222. package/fixtures/paper-sources/v16-included-block/paper.tex +7 -0
  223. package/fixtures/paper-sources/v16-included-block/refs.bib +1 -0
  224. package/fixtures/paper-sources/v16-included-block/tex-truth.json +35 -0
  225. package/fixtures/paper-sources/v17-include-order/bibsetup.tex +1 -0
  226. package/fixtures/paper-sources/v17-include-order/first.bib +1 -0
  227. package/fixtures/paper-sources/v17-include-order/later.bib +1 -0
  228. package/fixtures/paper-sources/v17-include-order/paper.tex +7 -0
  229. package/fixtures/paper-sources/v17-include-order/tex-truth.json +40 -0
  230. package/fixtures/paper-sources/v2-overwrite/paper.tex +14 -0
  231. package/fixtures/paper-sources/v2-overwrite/refs.bib +6 -0
  232. package/fixtures/paper-sources/v2-overwrite/tex-truth.json +30 -0
  233. package/fixtures/paper-sources/v22-parked-thebibliography/paper.tex +7 -0
  234. package/fixtures/paper-sources/v22-parked-thebibliography/tex-truth.json +17 -0
  235. package/fixtures/paper-sources/v23-parked-include/old.bib +1 -0
  236. package/fixtures/paper-sources/v23-parked-include/paper.tex +7 -0
  237. package/fixtures/paper-sources/v23-parked-include/parked.tex +1 -0
  238. package/fixtures/paper-sources/v23-parked-include/refs.bib +1 -0
  239. package/fixtures/paper-sources/v23-parked-include/tex-truth.json +28 -0
  240. package/fixtures/paper-sources/v24-quoted-fields/paper.tex +6 -0
  241. package/fixtures/paper-sources/v24-quoted-fields/refs.bib +5 -0
  242. package/fixtures/paper-sources/v24-quoted-fields/tex-truth.json +32 -0
  243. package/fixtures/paper-sources/v25-string-differs/paper.tex +10 -0
  244. package/fixtures/paper-sources/v25-string-differs/refs.bib +2 -0
  245. package/fixtures/paper-sources/v25-string-differs/tex-truth.json +32 -0
  246. package/fixtures/paper-sources/v26-comment-in-names/one.bib +1 -0
  247. package/fixtures/paper-sources/v26-comment-in-names/paper.tex +7 -0
  248. package/fixtures/paper-sources/v26-comment-in-names/tex-truth.json +32 -0
  249. package/fixtures/paper-sources/v26-comment-in-names/two.bib +1 -0
  250. package/fixtures/paper-sources/v27-shared-string/abbreviations.bib +2 -0
  251. package/fixtures/paper-sources/v27-shared-string/paper.tex +6 -0
  252. package/fixtures/paper-sources/v27-shared-string/references.bib +6 -0
  253. package/fixtures/paper-sources/v27-shared-string/tex-truth.json +31 -0
  254. package/fixtures/paper-sources/v28-shared-string-block/abbreviations.bib +2 -0
  255. package/fixtures/paper-sources/v28-shared-string-block/paper.tex +14 -0
  256. package/fixtures/paper-sources/v28-shared-string-block/tex-truth.json +33 -0
  257. package/fixtures/paper-sources/v29-generated-input/paper.tex +9 -0
  258. package/fixtures/paper-sources/v29-generated-input/tex-truth.json +24 -0
  259. package/fixtures/paper-sources/v3-declared/paper.bib +6 -0
  260. package/fixtures/paper-sources/v3-declared/paper.tex +6 -0
  261. package/fixtures/paper-sources/v3-declared/tex-truth.json +28 -0
  262. package/fixtures/paper-sources/v30-name-next-line/paper.tex +14 -0
  263. package/fixtures/paper-sources/v30-name-next-line/tex-truth.json +30 -0
  264. package/fixtures/paper-sources/v4-commented/paper.tex +14 -0
  265. package/fixtures/paper-sources/v4-commented/refs.bib +6 -0
  266. package/fixtures/paper-sources/v4-commented/tex-truth.json +28 -0
  267. package/fixtures/paper-sources/v5-percent-entry/paper.tex +20 -0
  268. package/fixtures/paper-sources/v5-percent-entry/tex-truth.json +37 -0
  269. package/fixtures/paper-sources/v6-unclosed/paper.tex +6 -0
  270. package/fixtures/paper-sources/v6-unclosed/refs.bib +5 -0
  271. package/fixtures/paper-sources/v6-unclosed/tex-truth.json +37 -0
  272. package/fixtures/paper-sources/v8-jobname/paper.tex +9 -0
  273. package/fixtures/paper-sources/v8-jobname/tex-truth.json +30 -0
  274. package/fixtures/paper-sources/v9-two-bibliographies/one.bib +1 -0
  275. package/fixtures/paper-sources/v9-two-bibliographies/paper.tex +7 -0
  276. package/fixtures/paper-sources/v9-two-bibliographies/tex-truth.json +35 -0
  277. package/fixtures/paper-sources/v9-two-bibliographies/two.bib +1 -0
  278. package/fixtures/paper-typography/clean-paper/paper.tex +1 -1
  279. package/fixtures/paper-typography/messy-paper/paper.tex +1 -1
  280. package/hooks/CLAUDE.md +34 -0
  281. package/hooks/CLAUDE.md.spec.ts +30 -0
  282. package/lib/agent-cli-version.ts +1 -1
  283. package/package.json +3 -11
  284. package/plugin/CLAUDE.md +22 -0
  285. package/plugin/CLAUDE.md.spec.ts +22 -0
  286. package/scripts/check.test.ts +23 -1
  287. package/scripts/check.ts +7 -3
  288. package/scripts/coverage-config.test.ts +21 -11
  289. package/scripts/layer-legacy-frozen.test.ts +41 -2
  290. package/scripts/layer-legacy-frozen.ts +31 -2
  291. package/skills/CLAUDE.md +19 -0
  292. package/skills/CLAUDE.md.spec.ts +18 -0
  293. package/skills/grade-paper-writing/SKILL.md +4 -3
  294. package/skills/grade-paper-writing/SKILL.md.spec.ts +3 -2
  295. package/skills/grade-paper-writing/prose-lint.mjs +11 -2
  296. package/skills/grade-paper-writing/prose-lint.test.ts +52 -4
  297. package/skills/paper-pipeline/scripts/extract-ref-facts.harness.mjs +83 -33
  298. package/skills/paper-pipeline/scripts/extract-ref-facts.mjs +67 -115
  299. package/skills/paper-pipeline/scripts/extract-ref-facts.test.mjs +128 -121
  300. package/skills/plan-paper-timeline/plan-paper-timeline.effects.harness.mjs +21 -102
  301. package/skills/verify-citations/SKILL.md +7 -4
  302. package/skills/verify-citations/SKILL.md.spec.ts +6 -3
  303. package/skills/verify-citations/scripts/bib-authors.check.test.mjs +17 -19
  304. package/skills/verify-citations/scripts/bib-authors.cli.test.ts +129 -46
  305. package/skills/verify-citations/scripts/bib-authors.d.mts +22 -2
  306. package/skills/verify-citations/scripts/bib-authors.mjs +57 -111
  307. package/skills/verify-citations/scripts/verify-cites.cli.test.ts +16 -0
  308. package/skills/verify-citations/scripts/verify-cites.d.mts +19 -3
  309. package/skills/verify-citations/scripts/verify-cites.mjs +42 -108
  310. package/skills/verify-citations/scripts/verify-cites.test.mjs +23 -36
  311. package/src/CLAUDE.md +23 -1
  312. package/src/CLAUDE.md.spec.ts +72 -0
  313. package/src/adapters/bibtex/index.test.ts +163 -0
  314. package/src/adapters/bibtex/index.ts +155 -0
  315. package/src/adapters/latex/filecontents.ts +114 -0
  316. package/src/adapters/latex/includes.ts +18 -10
  317. package/src/adapters/latex/index.test.ts +73 -0
  318. package/src/adapters/latex/index.ts +2 -0
  319. package/src/adapters/latex/nodes.ts +64 -0
  320. package/src/adapters/latex/parse.test.ts +38 -0
  321. package/src/adapters/latex/parse.ts +67 -6
  322. package/src/adapters/memory/index.ts +17 -0
  323. package/src/adapters/references/index.test.ts +75 -21
  324. package/src/adapters/references/index.ts +37 -32
  325. package/src/adapters/sources-record/index.test.ts +87 -0
  326. package/src/adapters/sources-record/index.ts +82 -0
  327. package/src/adapters/tex-output/blg.ts +27 -0
  328. package/src/adapters/tex-output/fls.ts +24 -0
  329. package/src/adapters/tex-output/index.test.ts +171 -0
  330. package/src/adapters/tex-output/index.ts +12 -0
  331. package/src/bib-rules.test.ts +356 -0
  332. package/src/bib-rules.ts +228 -0
  333. package/src/bibliography-at.test.ts +286 -0
  334. package/src/build.test.ts +210 -0
  335. package/src/build.ts +101 -36
  336. package/src/claim-provenance.test.ts +33 -7
  337. package/src/claim-provenance.ts +2 -2
  338. package/src/cli.harness.mjs +20 -10
  339. package/src/cli.test.ts +14 -10
  340. package/src/cli.ts +37 -28
  341. package/src/domain/paper-source.test.ts +58 -20
  342. package/src/domain/paper-source.ts +38 -42
  343. package/src/domain/paper-sources.ts +94 -0
  344. package/src/domain/sources-record.test.ts +71 -0
  345. package/src/domain/sources-record.ts +136 -0
  346. package/src/domain/tex-run.test.ts +228 -0
  347. package/src/domain/tex-run.ts +139 -0
  348. package/src/heading-case.test.ts +24 -10
  349. package/src/latex-log.ts +20 -0
  350. package/src/latex-loop.harness.ts +1 -0
  351. package/src/latex-loop.ts +2 -0
  352. package/src/linted-set.test.ts +9 -1
  353. package/src/new-paper.harness.mjs +3 -2
  354. package/src/new-paper.test.ts +9 -5
  355. package/src/package-dirs.ts +3 -2
  356. package/src/paper-includes.test.ts +157 -38
  357. package/src/paper-includes.ts +39 -35
  358. package/src/paper-prose.test.ts +34 -5
  359. package/src/paper-prose.ts +2 -2
  360. package/src/paper-record.test.ts +173 -0
  361. package/src/paper-record.ts +66 -0
  362. package/src/paper-settings-commands.test.ts +5 -2
  363. package/src/paper-sources.ts +151 -0
  364. package/src/ports/bib-reader.ts +27 -0
  365. package/src/ports/check-references.ts +7 -3
  366. package/src/ports/latex.ts +19 -0
  367. package/src/ports/sources-codec.ts +11 -0
  368. package/src/ports/tex-output.ts +17 -0
  369. package/src/recorded-bibliography.ts +297 -0
  370. package/src/reference-rules.ts +217 -104
  371. package/src/references.test.ts +520 -177
  372. package/src/references.ts +185 -37
  373. package/src/register-bands.test.ts +5 -1
  374. package/src/register-bands.ts +3 -5
  375. package/src/register.test.ts +6 -1
  376. package/src/register.ts +2 -2
  377. package/src/rule-cases.test.ts +68 -12
  378. package/src/rule-docs.test.ts +0 -1
  379. package/src/sources-record.test.ts +308 -0
  380. package/src/sources-record.ts +231 -0
  381. package/src/sources-rules.test.ts +152 -0
  382. package/src/sources-rules.ts +117 -0
  383. package/src/tex-paper.test.ts +148 -78
  384. package/src/tex-paper.ts +31 -61
  385. package/src/tex-requirements.ts +1 -1
  386. package/src/tex-venue-rules.test.ts +36 -8
  387. package/src/tex-venue-rules.ts +3 -5
  388. package/src/toolchain.ts +1 -1
  389. package/dist/eslint-rules/bib-reachable-entry.d.ts +0 -22
  390. package/dist/eslint-rules/bib-reachable-entry.d.ts.map +0 -1
  391. package/dist/eslint-rules/bib-reachable-entry.js +0 -75
  392. package/dist/eslint-rules/bib-reachable-entry.js.map +0 -1
  393. package/eslint-rules/bib-reachable-entry.ts +0 -76
@@ -0,0 +1,28 @@
1
+ {
2
+ "schema": 1,
3
+ "inputs": [
4
+ {
5
+ "path": "paper.tex",
6
+ "role": "body"
7
+ }
8
+ ],
9
+ "written": [],
10
+ "bibdata": [
11
+ "paper"
12
+ ],
13
+ "bibtex": {
14
+ "ran": true,
15
+ "databases": [
16
+ "paper.bib"
17
+ ],
18
+ "keys": [
19
+ "declared2023"
20
+ ],
21
+ "exit": 0,
22
+ "errors": []
23
+ },
24
+ "sha256": {
25
+ "paper.tex": "365c348f503b060973c7b1d5db0348659dffa91d7b149d10181204ae27e31dfb",
26
+ "paper.bib": "efe5f0577c8173e99be73ed728679904dcac9c90b1ec56350cf64d0771f8305e"
27
+ }
28
+ }
@@ -0,0 +1,14 @@
1
+ \documentclass{article}
2
+ \begin{filecontents*}
3
+ {refs.bib}
4
+ @misc{nl2024,
5
+ title = {The Name on the Next Line},
6
+ author = {Doe, Jane},
7
+ year = {2024}
8
+ }
9
+ \end{filecontents*}
10
+ \begin{document}
11
+ Text that cites~\cite{nl2024}.
12
+ \bibliographystyle{plain}
13
+ \bibliography{refs}
14
+ \end{document}
@@ -0,0 +1,30 @@
1
+ {
2
+ "schema": 1,
3
+ "inputs": [
4
+ {
5
+ "path": "paper.tex",
6
+ "role": "body"
7
+ }
8
+ ],
9
+ "written": [
10
+ "refs.bib"
11
+ ],
12
+ "bibdata": [
13
+ "refs"
14
+ ],
15
+ "bibtex": {
16
+ "ran": true,
17
+ "databases": [
18
+ "refs.bib"
19
+ ],
20
+ "keys": [
21
+ "nl2024"
22
+ ],
23
+ "exit": 0,
24
+ "errors": []
25
+ },
26
+ "sha256": {
27
+ "paper.tex": "143d03485e70ffefc525e6f783601c6f71e6650ccf779c77a4d7a8f12ba6075b",
28
+ "refs.bib": "b4c47cdd91aff598589dae3f0f068495af3680e5cdf0bce4747332750812ee1d"
29
+ }
30
+ }
@@ -0,0 +1,14 @@
1
+ \documentclass{article}
2
+ % \begin{filecontents*}{refs.bib}
3
+ % @misc{commented2024,
4
+ % title = {The Inline Entry},
5
+ % author = {Doe, Jane},
6
+ % year = {2024},
7
+ % url = {https://example.org/inline}
8
+ % }
9
+ % \end{filecontents*}
10
+ \begin{document}
11
+ Text that cites~\cite{commented2024} and~\cite{stale2020}.
12
+ \bibliographystyle{plain}
13
+ \bibliography{refs}
14
+ \end{document}
@@ -0,0 +1,6 @@
1
+ @misc{stale2020,
2
+ title = {The Stale Entry},
3
+ author = {Roe, Rich},
4
+ year = {2020},
5
+ url = {https://example.org/stale}
6
+ }
@@ -0,0 +1,28 @@
1
+ {
2
+ "schema": 1,
3
+ "inputs": [
4
+ {
5
+ "path": "paper.tex",
6
+ "role": "body"
7
+ }
8
+ ],
9
+ "written": [],
10
+ "bibdata": [
11
+ "refs"
12
+ ],
13
+ "bibtex": {
14
+ "ran": true,
15
+ "databases": [
16
+ "refs.bib"
17
+ ],
18
+ "keys": [
19
+ "stale2020"
20
+ ],
21
+ "exit": 0,
22
+ "errors": []
23
+ },
24
+ "sha256": {
25
+ "paper.tex": "a0a0349afca7a2de8b41bdbdf777de14d8e07df5993a351492ca11c858f8c34e",
26
+ "refs.bib": "7a720777755f3dd727c2428f66f06d5f818f367e41fe4c47543aa9f55adfebb4"
27
+ }
28
+ }
@@ -0,0 +1,20 @@
1
+ \documentclass{article}
2
+ \begin{filecontents*}[overwrite]{refs.bib}
3
+ @misc{inline2024,
4
+ title = {The Inline Entry},
5
+ author = {Doe, Jane},
6
+ year = {2024},
7
+ url = {https://example.org/inline}
8
+ }
9
+ % @misc{dead2020,
10
+ % title = {The Entry Behind a Percent Sign},
11
+ % author = {Moe, Max},
12
+ % year = {2020},
13
+ % url = {https://example.org/dead}
14
+ % }
15
+ \end{filecontents*}
16
+ \begin{document}
17
+ Text that cites~\cite{inline2024} and~\cite{dead2020}.
18
+ \bibliographystyle{plain}
19
+ \bibliography{refs}
20
+ \end{document}
@@ -0,0 +1,37 @@
1
+ {
2
+ "schema": 1,
3
+ "inputs": [
4
+ {
5
+ "path": "paper.tex",
6
+ "role": "body"
7
+ }
8
+ ],
9
+ "written": [
10
+ "refs.bib"
11
+ ],
12
+ "bibdata": [
13
+ "refs"
14
+ ],
15
+ "bibtex": {
16
+ "ran": true,
17
+ "databases": [
18
+ "refs.bib"
19
+ ],
20
+ "keys": [
21
+ "dead2020",
22
+ "inline2024"
23
+ ],
24
+ "exit": 2,
25
+ "errors": [
26
+ {
27
+ "message": "You're missing a field name",
28
+ "file": "refs.bib",
29
+ "line": 8
30
+ }
31
+ ]
32
+ },
33
+ "sha256": {
34
+ "paper.tex": "f520de6b9f34e249851913d2e9fc23532603378787b034e08e0b43e2bc111994",
35
+ "refs.bib": "247d564a35129b3fa087ae2541cd51130c1e6875fafe5dad36d694f6a4d1b7fc"
36
+ }
37
+ }
@@ -0,0 +1,6 @@
1
+ \documentclass{article}
2
+ \begin{document}
3
+ \nocite{*}
4
+ \bibliographystyle{plain}
5
+ \bibliography{refs}
6
+ \end{document}
@@ -0,0 +1,5 @@
1
+ @misc{a1, title={A}, author={A, B}, year={2020}, url={https://e.org/a1}}
2
+ @misc{a2unclosed, title={Two}, author={A, B}, year={2020}, url={https://e.org/a2}
3
+
4
+ @misc{a3, title={Three}, author={A, B}, year={2020}}
5
+ % @misc{a4, title={Four}, author={A, B}, year={2020}}
@@ -0,0 +1,37 @@
1
+ {
2
+ "schema": 1,
3
+ "inputs": [
4
+ {
5
+ "path": "paper.tex",
6
+ "role": "body"
7
+ }
8
+ ],
9
+ "written": [],
10
+ "bibdata": [
11
+ "refs"
12
+ ],
13
+ "bibtex": {
14
+ "ran": true,
15
+ "databases": [
16
+ "refs.bib"
17
+ ],
18
+ "keys": [
19
+ "a1",
20
+ "a4",
21
+ "a3",
22
+ "a2unclosed"
23
+ ],
24
+ "exit": 2,
25
+ "errors": [
26
+ {
27
+ "message": "I was expecting a `,' or a `}'",
28
+ "file": "refs.bib",
29
+ "line": 4
30
+ }
31
+ ]
32
+ },
33
+ "sha256": {
34
+ "paper.tex": "9ad22f9c50070c2620c5bf8885ed53cee4b0901e7bfdc83c61f70902686ac5e0",
35
+ "refs.bib": "1d33d35cf2e384fc54acf45654493881315c98a4b494b952464668d33924aeec"
36
+ }
37
+ }
@@ -0,0 +1,9 @@
1
+ \documentclass{article}
2
+ \begin{filecontents*}[overwrite]{\jobname.bib}
3
+ @misc{jkey, title={J}, author={Doe, J}, year={2024}}
4
+ % @misc{jdead, title={D}, author={Doe, J}, year={2020}}
5
+ \end{filecontents*}
6
+ \begin{document}
7
+ Text~\cite{jkey}.
8
+ \bibliographystyle{plain}\bibliography{\jobname}
9
+ \end{document}
@@ -0,0 +1,30 @@
1
+ {
2
+ "schema": 1,
3
+ "inputs": [
4
+ {
5
+ "path": "paper.tex",
6
+ "role": "body"
7
+ }
8
+ ],
9
+ "written": [
10
+ "paper.bib"
11
+ ],
12
+ "bibdata": [
13
+ "paper"
14
+ ],
15
+ "bibtex": {
16
+ "ran": true,
17
+ "databases": [
18
+ "paper.bib"
19
+ ],
20
+ "keys": [
21
+ "jkey"
22
+ ],
23
+ "exit": 0,
24
+ "errors": []
25
+ },
26
+ "sha256": {
27
+ "paper.tex": "d68bcaabe9be4cbc07f5a1e672e8cd448e96bc9b5134be350d2906ee48834482",
28
+ "paper.bib": "bc5a9faf4bbdcdf22705c4c8b69d656382ab9d1d03f57388c00631c3aa6ebf70"
29
+ }
30
+ }
@@ -0,0 +1 @@
1
+ @misc{onekey, title={One}, author={A, B}, year={2020}}
@@ -0,0 +1,7 @@
1
+ \documentclass{article}
2
+ \begin{document}
3
+ \nocite{*}
4
+ \bibliographystyle{plain}
5
+ \bibliography{one}
6
+ \bibliography{two}
7
+ \end{document}
@@ -0,0 +1,35 @@
1
+ {
2
+ "schema": 1,
3
+ "inputs": [
4
+ {
5
+ "path": "paper.tex",
6
+ "role": "body"
7
+ }
8
+ ],
9
+ "written": [],
10
+ "bibdata": [
11
+ "one",
12
+ "two"
13
+ ],
14
+ "bibtex": {
15
+ "ran": true,
16
+ "databases": [
17
+ "one.bib"
18
+ ],
19
+ "keys": [
20
+ "onekey"
21
+ ],
22
+ "exit": 2,
23
+ "errors": [
24
+ {
25
+ "message": "Illegal, another \\bibdata command",
26
+ "file": "paper.aux",
27
+ "line": 5
28
+ }
29
+ ]
30
+ },
31
+ "sha256": {
32
+ "paper.tex": "f45db61354f01066eb49ab48d240033bea99ed172f04a2c4558daca8640ce796",
33
+ "one.bib": "61cca311ca7d77570ed410fc536c655ba76d36168fc3e56068235a2c4b16f7bf"
34
+ }
35
+ }
@@ -0,0 +1 @@
1
+ @misc{twokey, title={Two}, author={A, B}, year={2020}}
@@ -1,5 +1,5 @@
1
1
  \documentclass{acmart}
2
- \begin{filecontents*}{refs.bib}
2
+ \begin{filecontents*}[overwrite]{refs.bib}
3
3
  @inproceedings{reachable,
4
4
  title = {A paper a reader can actually open},
5
5
  author = {Doe, Jane},
@@ -1,5 +1,5 @@
1
1
  \documentclass{acmart}
2
- \begin{filecontents*}{refs.bib}
2
+ \begin{filecontents*}[overwrite]{refs.bib}
3
3
  @inproceedings{unreachableone,
4
4
  title = {No doi, no url, no arXiv id},
5
5
  author = {Doe, Jane},
@@ -0,0 +1,34 @@
1
+ <!-- vigiles:sha256:0407b53b2036133e compiled from hooks/CLAUDE.md.spec.ts -->
2
+
3
+ # CLAUDE.md
4
+
5
+ ## `hooks/`
6
+
7
+ The hooks this package ships into a consumer's Claude Code session. Loaded when working under
8
+ `hooks/`. Every shipped hook is run in both halves, firing and silent, by `hooks/hooks.harness.ts`.
9
+
10
+ A bug here does not break this repository; it breaks somebody else's whole session. The Bash gate
11
+ fails closed, so a hook that cannot read its config denies every command, including the one that
12
+ would undo it.
13
+
14
+ ## Rules
15
+
16
+ ### Ship Hook Mjs Never Hook Ts
17
+
18
+ **Guidance only** — A `.ts` hook inside `node_modules` fails to load and blocks every Bash command. Rejected, so not proposed again: a `.hook.ts` twin compiled to the `.mjs` (a twin drifts from its build, and nothing would notice), and a thin `.hook.ts` in the consumer importing the logic from here (`vigiles compile` refuses the import, so the hook can never be re-stamped, and the runtime fails closed on the stale stamp). [Measurements](https://github.com/zernie/paperlint/blob/b4a3dc3a81d436186f41db2d69b65fd93f1af3b6/CLAUDE.md#L272-L357).
19
+
20
+ ### No Sibling Imports
21
+
22
+ **Guidance only** — A shipped hook cannot import a sibling module. The papers-root resolver is repeated in each hook, and `hooks.harness.ts` compares the copies.
23
+
24
+ ### No Import Shaped Text
25
+
26
+ **Guidance only** — `checkHookImports` (run by `hooks.harness.ts`) is a text regex. Describe a forbidden import in words; never write one, not even in a comment.
27
+
28
+ ### One Wiring File
29
+
30
+ **Guidance only** — The wiring is `plugin/hooks/hooks.json`. `paperlint init` merges it into the consumer's `.claude/settings.json`; there is no second copy.
31
+
32
+ ### Vigiles Moves With The Consumer
33
+
34
+ **Guidance only** — `vigiles` moves with the consumer's pin, in one pass. A major mismatch makes `PreToolUse` refuse every command. `npm ls vigiles` compares what is installed with the manifest, not with the registry; Dependabot (`.github/dependabot.yml`) carries the registry half.
@@ -0,0 +1,30 @@
1
+ // Compiled to hooks/CLAUDE.md by `npx vigiles compile`. Edit THIS file, never the markdown.
2
+ import { file, guidance, instructionFile, prose } from "vigiles/spec";
3
+
4
+ export default instructionFile({
5
+ sections: {
6
+ "`hooks/`": prose`The hooks this package ships into a consumer's Claude Code session. Loaded when working under
7
+ \`hooks/\`. Every shipped hook is run in both halves, firing and silent, by ${file("hooks/hooks.harness.ts")}.
8
+
9
+ A bug here does not break this repository; it breaks somebody else's whole session. The Bash gate
10
+ fails closed, so a hook that cannot read its config denies every command, including the one that
11
+ would undo it.`,
12
+ },
13
+ rules: {
14
+ "ship-hook-mjs-never-hook-ts": guidance(
15
+ "A `.ts` hook inside `node_modules` fails to load and blocks every Bash command. Rejected, so not proposed again: a `.hook.ts` twin compiled to the `.mjs` (a twin drifts from its build, and nothing would notice), and a thin `.hook.ts` in the consumer importing the logic from here (`vigiles compile` refuses the import, so the hook can never be re-stamped, and the runtime fails closed on the stale stamp). [Measurements](https://github.com/zernie/paperlint/blob/b4a3dc3a81d436186f41db2d69b65fd93f1af3b6/CLAUDE.md#L272-L357).",
16
+ ),
17
+ "no-sibling-imports": guidance(
18
+ "A shipped hook cannot import a sibling module. The papers-root resolver is repeated in each hook, and `hooks.harness.ts` compares the copies.",
19
+ ),
20
+ "no-import-shaped-text": guidance(
21
+ "`checkHookImports` (run by `hooks.harness.ts`) is a text regex. Describe a forbidden import in words; never write one, not even in a comment.",
22
+ ),
23
+ "one-wiring-file": guidance(
24
+ "The wiring is `plugin/hooks/hooks.json`. `paperlint init` merges it into the consumer's `.claude/settings.json`; there is no second copy.",
25
+ ),
26
+ "vigiles-moves-with-the-consumer": guidance(
27
+ "`vigiles` moves with the consumer's pin, in one pass. A major mismatch makes `PreToolUse` refuse every command. `npm ls vigiles` compares what is installed with the manifest, not with the registry; Dependabot (`.github/dependabot.yml`) carries the registry half.",
28
+ ),
29
+ },
30
+ });
@@ -95,7 +95,7 @@ export function observeAgentCli({
95
95
 
96
96
  // ⚠️ EXIT 0 AND NOTHING ON STDOUT IS NOT A CLEAN OBSERVATION — it is exactly what the discarded
97
97
  // probe looked like, and reporting `characterized against ""` would be the same lie in a new
98
- // place. This is rule 4 of CLAUDE.md applied to the check that enforces it.
98
+ // place. This is the green zero of scripts/rules-see-files.mjs, applied to the check that enforces it.
99
99
  if (version === "")
100
100
  throw new Error(
101
101
  `\`${program} --version\` exited 0 but printed no version on stdout (got ${JSON.stringify(raw)}` +
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "paperlint",
3
- "version": "8.0.0",
3
+ "version": "9.0.1",
4
4
  "description": "A linter for scientific papers written in LaTeX: catches mistakes before you submit to a conference or journal.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -20,6 +20,7 @@
20
20
  "dependencies": {
21
21
  "@eslint/markdown": "^8.0.3",
22
22
  "@eslint/plugin-kit": "^0.7.2",
23
+ "@retorquere/bibtex-parser": "^10.0.1",
23
24
  "@unified-latex/unified-latex-types": "^1.8.4",
24
25
  "@unified-latex/unified-latex-util-parse": "^1.8.4",
25
26
  "@unified-latex/unified-latex-util-visit": "^1.8.4",
@@ -33,11 +34,10 @@
33
34
  "ts-essentials": "^10.2.1",
34
35
  "typescript": "^6.0.3",
35
36
  "unpdf": "^1.8.1",
36
- "vigiles": "^32.1.0",
37
+ "vigiles": "^38.2.0",
37
38
  "zod": "^4.6.5"
38
39
  },
39
40
  "devDependencies": {
40
- "@retorquere/bibtex-parser": "^10.0.1",
41
41
  "@semantic-release/commit-analyzer": "13.0.1",
42
42
  "@semantic-release/release-notes-generator": "14.1.1",
43
43
  "@types/js-yaml": "^4.0.9",
@@ -56,14 +56,6 @@
56
56
  "typescript-eslint": "^8.70.1",
57
57
  "vitest": "^4.1.11"
58
58
  },
59
- "peerDependencies": {
60
- "@retorquere/bibtex-parser": "^10.0.1"
61
- },
62
- "peerDependenciesMeta": {
63
- "@retorquere/bibtex-parser": {
64
- "optional": true
65
- }
66
- },
67
59
  "repository": {
68
60
  "type": "git",
69
61
  "url": "git+https://github.com/zernie/paperlint.git"
@@ -0,0 +1,22 @@
1
+ <!-- vigiles:sha256:6312a3b8f9804eba compiled from plugin/CLAUDE.md.spec.ts -->
2
+
3
+ # CLAUDE.md
4
+
5
+ ## `plugin/`
6
+
7
+ Not a plugin any more. `plugin/hooks/hooks.json` is the hook wiring that `paperlint init` merges
8
+ into the consumer's `.claude/settings.json`, and it is the only source of that wiring.
9
+
10
+ ## Why there is no Claude Code plugin
11
+
12
+ There is no Claude Code plugin or marketplace entry; it was removed in the release after 1.0.0
13
+ (#82). Do not bring it back without answering each of these, measured (`docs/install.md`,
14
+ "Why it is shaped this way"):
15
+
16
+ - **A plugin cannot carry the code.** Claude Code installs a plugin's dependencies with a 60-second
17
+ timeout, and a failed install never blocks the plugin: the hooks load and fail with
18
+ `cannot find module vigiles`, silently.
19
+ - **The skills need the npm package anyway.** 23 of 24 skills ran scripts that resolve only through
20
+ `node_modules/paperlint/`.
21
+ - **Its manifests carried versions nothing updated**, and Claude Code decides plugin updates from
22
+ that number.
@@ -0,0 +1,22 @@
1
+ // Compiled to plugin/CLAUDE.md by `npx vigiles compile`. Edit THIS file, never the markdown.
2
+ import { file, instructionFile, prose } from "vigiles/spec";
3
+
4
+ export default instructionFile({
5
+ sections: {
6
+ "`plugin/`": prose`Not a plugin any more. ${file("plugin/hooks/hooks.json")} is the hook wiring that \`paperlint init\` merges
7
+ into the consumer's \`.claude/settings.json\`, and it is the only source of that wiring.`,
8
+
9
+ "Why there is no Claude Code plugin": prose`There is no Claude Code plugin or marketplace entry; it was removed in the release after 1.0.0
10
+ (#82). Do not bring it back without answering each of these, measured (${file("docs/install.md")},
11
+ "Why it is shaped this way"):
12
+
13
+ - **A plugin cannot carry the code.** Claude Code installs a plugin's dependencies with a 60-second
14
+ timeout, and a failed install never blocks the plugin: the hooks load and fail with
15
+ \`cannot find module vigiles\`, silently.
16
+ - **The skills need the npm package anyway.** 23 of 24 skills ran scripts that resolve only through
17
+ \`node_modules/paperlint/\`.
18
+ - **Its manifests carried versions nothing updated**, and Claude Code decides plugin updates from
19
+ that number.`,
20
+ },
21
+ rules: {},
22
+ });
@@ -6,7 +6,7 @@
6
6
  import assert from "node:assert/strict";
7
7
  import { spawnSync } from "node:child_process";
8
8
  import { fileURLToPath } from "node:url";
9
- import { test } from "vitest";
9
+ import { test, vi } from "vitest";
10
10
  import {
11
11
  GATES,
12
12
  NOT_COVERED,
@@ -122,6 +122,28 @@ test("🔴 runGates: a vitest gate that exits 0 but leaves no report is a FAILUR
122
122
  );
123
123
  });
124
124
 
125
+ test("🔴 every gate runs with CI=true, as CI runs it — a local check is the CI check", () => {
126
+ // Libraries change behaviour under CI=true (typescript-eslint infers a single run and parses from
127
+ // disk): a gate run without it passed locally and failed in CI on 2026-10-07.
128
+ // Whatever the caller's shell says: on CI it is already "true", so the test sets it otherwise.
129
+ vi.stubEnv("CI", "false");
130
+ const seen: (string | undefined)[] = [];
131
+ runGates({
132
+ gates: [
133
+ { name: "a", job: "gates", run: ["true"] },
134
+ { name: "b", job: "gates", script: "build" },
135
+ ],
136
+ spawn: (_cmd, _args, options) => {
137
+ seen.push(options.env["CI"]);
138
+ return { status: 0 };
139
+ },
140
+ write: () => {},
141
+ ...capture().io,
142
+ });
143
+ vi.unstubAllEnvs();
144
+ assert.deepEqual(seen, ["true", "true"]);
145
+ });
146
+
125
147
  test("run as a program with every gate passing: the plain verdict, exit 0", () => {
126
148
  // spawnSync is replaced before check.ts loads, so the real file runs end to end — every gate
127
149
  // "passes" in microseconds instead of the eleven real ones running.
package/scripts/check.ts CHANGED
@@ -30,8 +30,10 @@
30
30
  *
31
31
  * ── NO `--fast` FLAG, ON PURPOSE ───────────────────────────────────────────────────────────
32
32
  * A subset flag re-creates the exact failure above: the cheap half gets run and reported as
33
- * "the gates". The slow steps instead SKIP LOUDLY when their prerequisite is genuinely absent
34
- * (no TeX, no network), and the tail names every skip. "I ran check" then means one thing.
33
+ * "the gates". Every gate runs with CI=true, as in CI, so a local check is the CI check: the e2e
34
+ * steps are strict (`test/e2e/need.ts`) — a missing prerequisite (no TeX) FAILS with what to
35
+ * install, as it does in CI — and a test that still skips is named in the tail, never counted as a
36
+ * pass. "I ran check" then means one thing.
35
37
  */
36
38
  import { spawnSync } from "node:child_process";
37
39
  import { existsSync, readFileSync, rmSync } from "node:fs";
@@ -293,7 +295,9 @@ function runEach(
293
295
  cwd: ROOT,
294
296
  stdio: "inherit",
295
297
  encoding: "utf8",
296
- env: { ...process.env, PATH: BIN_FIRST_PATH },
298
+ // CI=true, as CI runs every gate: libraries read it (typescript-eslint infers a single run and
299
+ // parses from disk), so without it a local check is not the CI check (2026-10-07).
300
+ env: { ...process.env, CI: "true", PATH: BIN_FIRST_PATH },
297
301
  });
298
302
  // 🔴 The exit code is read from the command itself, never from a pipe. `cmd | tail && …`
299
303
  // reports the FILTER's status, which is almost always zero — that is how a red harness
@@ -21,7 +21,7 @@
21
21
  import assert from "node:assert/strict";
22
22
  import { spawnSync } from "node:child_process";
23
23
  import { globSync, readFileSync } from "node:fs";
24
- import { dirname, join, matchesGlob, resolve } from "node:path";
24
+ import { basename, dirname, join, matchesGlob, resolve } from "node:path";
25
25
  import { fileURLToPath } from "node:url";
26
26
  import yaml from "js-yaml";
27
27
  import ts from "typescript";
@@ -146,16 +146,23 @@ test("every typeOnly exclusion has no runtime code", () => {
146
146
  });
147
147
 
148
148
  test("every evalOnly exclusion is imported only by an eval or another evalOnly module", () => {
149
- const evalOnly = new Set(expand(CONFIG.evalOnly).map((f) => join(ROOT, f)));
149
+ const only = expand(CONFIG.evalOnly);
150
+ const evalOnly = new Set(only.map((f) => join(ROOT, f)));
151
+ // Any import of one names it — `./x.mjs`, `#lib/x` — so a source whose text holds none of their
152
+ // base names imports none of them, and only the others are parsed.
153
+ const names = only.map((f) => basename(f).replace(/\.[^.]+$/, ""));
150
154
  const sources = globSync("**/*.{mjs,ts}", {
151
155
  cwd: ROOT,
152
156
  exclude: (p) => p === "node_modules" || p === "dist" || p === "coverage",
153
157
  });
154
158
  const importers = sources.flatMap((f) => {
155
159
  const abs = join(ROOT, f);
156
- return importedPaths(abs, readFileSync(abs, "utf8"))
157
- .filter((target) => evalOnly.has(target))
158
- .map(() => f);
160
+ const text = readFileSync(abs, "utf8");
161
+ return names.some((n) => text.includes(n))
162
+ ? importedPaths(abs, text)
163
+ .filter((target) => evalOnly.has(target))
164
+ .map(() => f)
165
+ : [];
159
166
  });
160
167
  const outsiders = importers.filter(
161
168
  (f) => !f.endsWith(".eval.mjs") && !evalOnly.has(join(ROOT, f)),
@@ -258,11 +265,13 @@ test("no measured source carries a coverage-ignore comment", () => {
258
265
  // The scan covers exactly what c8 measures: the same set, derived from the tracked files.
259
266
  assert.deepEqual(files, trackedSources().filter(isMeasured));
260
267
  assert.ok(files.length > 0);
261
- const offenders = files.filter((f) =>
262
- comments(f, readFileSync(join(ROOT, f), "utf8")).some((c) =>
263
- IGNORE.test(c),
264
- ),
265
- );
268
+ // An ignore comment's text matches IGNORE, so a source whose text does not holds none, and only
269
+ // the others have their comments read off the tree.
270
+ const offenders = files
271
+ .map((f) => [f, readFileSync(join(ROOT, f), "utf8")] as const)
272
+ .filter(([, text]) => IGNORE.test(text))
273
+ .filter(([f, text]) => comments(f, text).some((c) => IGNORE.test(c)))
274
+ .map(([f]) => f);
266
275
  assert.deepEqual(offenders, []);
267
276
  });
268
277
 
@@ -391,8 +400,9 @@ test("the exclude list is exactly the justified set", () => {
391
400
  "**/*.test.*",
392
401
  // PAID LLM evaluations, never run by `npm test`
393
402
  "**/*.eval.mjs",
394
- // skill sources vigiles compiles into SKILL.md
403
+ // skill and CLAUDE.md sources vigiles compiles; a glob's `**` skips a dot directory
395
404
  "**/*.spec.ts",
405
+ ".github/*.spec.ts",
396
406
  // type declarations
397
407
  "**/*.d.ts",
398
408
  "**/*.d.mts",