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
@@ -10,54 +10,61 @@
10
10
  * and the zero was an artifact of the empty directory, not a fact about the skill.
11
11
  *
12
12
  * Extracted from `paper-pipeline/pipeline-firing.eval.mjs`, which had it right
13
- * from the start, so the two runners cannot drift apart on what the model sees.
13
+ * from the start; the `paper-pipeline/*.eval.mjs` experiments carry the same
14
+ * block, byte for byte.
14
15
  *
15
16
  * ⚠️ AND THE PRICE, so this file is not a one-sided story (measured 2026-08-12).
16
17
  * With a fixture present the model OPENS THE PAPER and looks around, so the number
17
18
  * stops being "did the DESCRIPTION fire" and becomes "did the model go investigate
18
19
  * and then fire". A different quantity, and a costlier one: ~590k → ~780k tokens
19
- * per skill (+190k) and 67s → 84s, while this whole fixture is 1,537 characters
20
- * ≈ 384 tokens. The growth is reading, not input size (`cache` 470k → 652k).
20
+ * per skill (+190k) and 67s → 84s, while the fixture measured then was 1,537
21
+ * characters ≈ 384 tokens. The growth is reading, not input size (`cache`
22
+ * 470k → 652k).
21
23
  * Cite the trigger numbers with that caveat attached — it is written out in
22
24
  * `vigiles/repro/skill-trigger-2026-08-11/README.md` under "How to read".
23
25
  */
24
26
  export const FIXTURE = {
25
- "paper/paper.md": [
26
- "# Prose Isn't Policy: Measuring Whether Agent-Config Rules Are Enforceable",
27
+ "paper/paper.tex": [
28
+ "\\documentclass{article}",
29
+ "\\title{Prose Isn't Policy: Measuring Whether Agent-Config Rules Are Enforceable}",
30
+ "\\begin{document}",
31
+ "\\maketitle",
27
32
  "",
28
- "## Abstract",
33
+ "\\begin{abstract}",
29
34
  "Agent configuration files state rules in prose and assume the model obeys them. We compile a",
30
- "corpus of real rules and measure what fraction can be mechanically enforced. We find that 84%",
35
+ "corpus of real rules and measure what fraction can be mechanically enforced. We find that 84\\%",
31
36
  "of rules in our corpus are enforceable, and that LLM-authored checkers for the remainder leak",
32
- "silently in 84-96% of adversarial cases \\cite{greshake2023}.",
37
+ "silently in 84--96\\% of adversarial cases~\\cite{greshake2023}.",
38
+ "\\end{abstract}",
33
39
  "",
34
- "## 1 Introduction",
40
+ "\\section{Introduction}",
35
41
  "Every agent harness ships a natural-language rulebook. Nothing checks it. This is the same",
36
- "mistake as a code comment that claims an invariant no test enforces \\cite{thompson1984}.",
42
+ "mistake as a code comment that claims an invariant no test enforces~\\cite{thompson1984}.",
37
43
  "",
38
- "## 2 Method",
44
+ "\\section{Method}",
39
45
  "We gather rules from public repositories, classify each by enforceability, and build a",
40
46
  "two-stage adversarial gate that validates a synthesized rule against a blind gold set.",
41
47
  "",
42
- "## 3 Results",
43
- "See Table 1. The headline number is 84%.",
48
+ "\\section{Results}",
49
+ "See Table~1. The headline number is 84\\%.",
44
50
  "",
45
- "## 4 Discussion",
51
+ "\\section{Discussion}",
46
52
  "The result generalizes beyond our corpus in the sense that the mechanism is not corpus-specific,",
47
53
  "though of course the specific percentages are, and it is important to note in this context that",
48
54
  "the framing itself may be what carries, rather than the measurement.",
49
55
  "",
50
- "## 5 Threats to Validity",
56
+ "\\section{Threats to Validity}",
51
57
  "Our corpus is drawn from public repositories and may not represent private configurations.",
52
58
  "",
53
- "## 6 Related Work",
59
+ "\\section{Related Work}",
54
60
  "TODO",
55
61
  "",
56
- "## 7 Conclusion",
62
+ "\\section{Conclusion}",
57
63
  "Prose is not policy.",
64
+ "\\end{document}",
58
65
  ].join("\n"),
59
66
  "paper/repro/README.md":
60
- "# Reproduction artifact\n\n`python3 paper_numbers.py` recomputes every bolded figure in paper.md.\n",
67
+ "# Reproduction artifact\n\n`python3 paper_numbers.py` recomputes every bolded figure in paper.tex.\n",
61
68
  "paper/PIPELINE-STATUS.md":
62
69
  "# Pipeline status\n\n| gate | state |\n|---|---|\n| numbers | pass |\n| structure | not run |\n| citations | not run |\n",
63
70
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "paperlint",
3
- "version": "7.2.0",
3
+ "version": "9.0.0",
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",
@@ -37,7 +38,6 @@
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"
@@ -114,8 +106,6 @@
114
106
  "eslint.config.mjs",
115
107
  "fixtures",
116
108
  "!fixtures/accepted-papers",
117
- "!fixtures/real-markdown-paper",
118
- "fixtures/real-markdown-paper/baseline.*",
119
109
  "hooks",
120
110
  "lib",
121
111
  "presets",
@@ -72,7 +72,7 @@
72
72
  //
73
73
  // A gate on banal's number would give 13 against a limit of 8 and would redden on a paper that
74
74
  // is within the limit. For an `error`-level rule that is the worst outcome. The limit stays
75
- // owned by the counter inside `build-submission.sh`: it reads the markdown source and therefore
75
+ // owned by the counter inside `build-submission.sh`: it reads the paper's source and therefore
76
76
  // KNOWS where the body ends, which the PDF does not.
77
77
  //
78
78
  // The numbers below are DATA for that counter and for humans, not an input to the rule. Source:
@@ -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
 
@@ -10,7 +10,6 @@
10
10
  "eslint-rules/pdf-last-page-balance.harness.mjs",
11
11
  "eslint-rules/temp-root-realpath.harness.mjs",
12
12
  "eslint-rules/tex-build.harness.mjs",
13
- "fixtures/real-markdown-paper/real-markdown-paper.harness.ts",
14
13
  "lib/agent-cli-version.harness.mjs",
15
14
  "lib/paper-config.harness.mjs",
16
15
  "lib/skill-trigger-cases.harness.mjs",
@@ -30,13 +29,9 @@
30
29
  "skills/osf-artifact-upload/osf-artifact-upload.harness.ts",
31
30
  "skills/paper-adversarial-review/paper-adversarial-review.harness.mjs",
32
31
  "skills/paper-pipeline/paper-pipeline.harness.mjs",
33
- "skills/paper-pipeline/scripts/artifact-coverage.harness.mjs",
34
32
  "skills/paper-pipeline/scripts/consumer.harness.mjs",
35
33
  "skills/paper-pipeline/scripts/extract-ref-facts.harness.mjs",
36
34
  "skills/paper-pipeline/scripts/generated-code.harness.mjs",
37
- "skills/paper-pipeline/scripts/population-map.harness.mjs",
38
- "skills/paper-pipeline/scripts/provenance.harness.mjs",
39
- "skills/paper-pipeline/scripts/round-diff.harness.ts",
40
35
  "skills/paper-status/paper-status.harness.mjs",
41
36
  "skills/pc-panel-review/pc-panel-review.harness.mjs",
42
37
  "skills/plan-paper-timeline/plan-paper-timeline.harness.mjs",
@@ -9,6 +9,7 @@ import { fileURLToPath } from "node:url";
9
9
  import { test } from "vitest";
10
10
  import {
11
11
  checkFrozen,
12
+ filesToLint,
12
13
  FROZEN_FILE,
13
14
  judge,
14
15
  main,
@@ -164,8 +165,9 @@ test("main: problems are listed and fail; a clean count passes", async () => {
164
165
  );
165
166
  });
166
167
 
167
- // Lints the whole of src/ with the real config: ~3 s alone, 5.6 s measured under c8 with the
168
- // suite running in parallel — past vitest's 5 s default, so the budget is stated here.
168
+ // Loads the real config (about 2.5 s) and lints the src/ files that carry a directive (21 of 223),
169
+ // measured 2026-10-07: 5.5 s alone, 10–11 s under c8 — past vitest's 5 s default, so the budget is
170
+ // stated here. Linting all of src/ took 22–23 s under c8 and timed out with the suite in parallel.
169
171
  test(
170
172
  "checkFrozen on this repository: nothing new, nothing grown — and on a tree with no src/, ESLint refuses loudly",
171
173
  {
@@ -186,6 +188,43 @@ test(
186
188
  },
187
189
  );
188
190
 
191
+ test("filesToLint: every src/ file whose text carries an eslint-disable directive, in any form — and no other", () => {
192
+ const root = useTempDir("layer-frozen-files-");
193
+ writeTree(root, {
194
+ "src/line.ts": "export const a = 1; // eslint-disable-line no-console\n",
195
+ "src/next.ts":
196
+ "// eslint-disable-next-line no-console\nexport const b = 1;\n",
197
+ "src/block.ts": "/* eslint-disable no-console */\nexport const c = 1;\n",
198
+ "src/deep/inner.ts":
199
+ "// eslint-disable-next-line x -- why\nexport const d = 1;\n",
200
+ "src/plain.ts": "export const e = 1;\n",
201
+ "lib/outside.ts": "// eslint-disable-next-line no-console\n",
202
+ });
203
+ assert.deepEqual(filesToLint(root), [
204
+ "src/block.ts",
205
+ "src/deep/inner.ts",
206
+ "src/line.ts",
207
+ "src/next.ts",
208
+ ]);
209
+ });
210
+
211
+ test("checkFrozen with the real ESLint on a tree: a new legacy directive in a file the list does not name still fails", async () => {
212
+ const root = useTempDir("layer-frozen-real-");
213
+ writeTree(root, {
214
+ [FROZEN_FILE]: JSON.stringify({ files: {} }),
215
+ "eslint.config.mjs":
216
+ 'export default [{ files: ["src/**/*.ts"], rules: { "no-restricted-globals": ["error", "process"] } }];\n',
217
+ "src/plain.ts": "export const quiet = 1;\n",
218
+ "src/new.ts": `// eslint-disable-next-line no-restricted-globals -- ${IO}\nexport const env = process;\n`,
219
+ });
220
+ const r = await checkFrozen(root);
221
+ assert.deepEqual(r.counts, { "src/new.ts": { "no-restricted-globals": 1 } });
222
+ assert.match(
223
+ r.problems.join("\n"),
224
+ /^src\/new\.ts: a new legacy layer exemption/,
225
+ );
226
+ });
227
+
189
228
  test("checkFrozen: a lint that returns no result reports that nothing was counted", async () => {
190
229
  const root = useTempDir("layer-frozen-empty-");
191
230
  writeTree(root, {
@@ -32,10 +32,15 @@
32
32
  * counted by construction — and a range directive (`/* eslint-disable … *\/` around a multi-line
33
33
  * import) counts exactly what it silences.
34
34
  *
35
+ * ESLint lints only the files that can carry one (`filesToLint`): a suppressed finding comes from a
36
+ * directive, and every directive's text holds `eslint-disable` (`-line`, `-next-line`, the block
37
+ * form). The bulk-suppressions file is not applied here. So the narrowing drops only files that
38
+ * cannot add to the count — 21 of 223 today — and the count still comes from ESLint's report.
39
+ *
35
40
  * Run: `node scripts/layer-legacy-frozen.ts` (also part of `npm run check`)
36
41
  */
37
42
  import { isMain } from "../skills/paper-pipeline/scripts/consumer.mjs";
38
- import { readFileSync } from "node:fs";
43
+ import { existsSync, readdirSync, readFileSync } from "node:fs";
39
44
  import { dirname, join, relative, sep } from "node:path";
40
45
  import { fileURLToPath } from "node:url";
41
46
  import { ESLint } from "eslint";
@@ -195,11 +200,35 @@ function layerLint(cwd: string): ESLint {
195
200
  });
196
201
  }
197
202
 
203
+ /**
204
+ * The `src/**\/*.ts` files of `root` whose text holds `eslint-disable` — every file that can carry a
205
+ * suppressed finding — relative to `root`, POSIX separators, sorted.
206
+ */
207
+ export function filesToLint(root: string): string[] {
208
+ const src = join(root, "src");
209
+ const all = readdirSync(src, { recursive: true, withFileTypes: true })
210
+ .filter((e) => e.isFile() && e.name.endsWith(".ts"))
211
+ .map((e) => join(e.parentPath, e.name));
212
+ return all
213
+ .filter((f) => readFileSync(f, "utf8").includes("eslint-disable"))
214
+ .map((f) => relative(root, f).split(sep).join("/"))
215
+ .sort();
216
+ }
217
+
218
+ /**
219
+ * The files ESLint lints: those that can carry a suppression, or — when there are none, or no
220
+ * `src/` — the whole glob, so ESLint itself refuses a tree with no source.
221
+ */
222
+ const lintTargets = (root: string): string[] => {
223
+ const narrowed = existsSync(join(root, "src")) ? filesToLint(root) : [];
224
+ return narrowed.length > 0 ? narrowed : ["src/**/*.ts"];
225
+ };
226
+
198
227
  /** Lint `src/` with the repository's configuration and run the whole check. */
199
228
  export async function checkFrozen(
200
229
  root: string = ROOT,
201
230
  {
202
- lint = (cwd: string) => layerLint(cwd).lintFiles(["src/**/*.ts"]),
231
+ lint = (cwd: string) => layerLint(cwd).lintFiles(lintTargets(cwd)),
203
232
  }: { lint?: (cwd: string) => Promise<readonly Suppressed[]> } = {},
204
233
  ): Promise<FrozenCheck> {
205
234
  const data = FrozenFile.parse(
@@ -5,7 +5,7 @@ context: fork
5
5
  allowed-tools: [Bash, Read, Agent]
6
6
  ---
7
7
 
8
- <!-- vigiles:sha256:293f0627e6873774 compiled from skills/cold-read-diff/SKILL.md.spec.ts -->
8
+ <!-- vigiles:sha256:c3a1f2644781eb6f compiled from skills/cold-read-diff/SKILL.md.spec.ts -->
9
9
 
10
10
  # cold-read-diff — the reader who cannot fake understanding
11
11
 
@@ -55,7 +55,7 @@ one, and its confusion is the finding.
55
55
 
56
56
  ## How to run it
57
57
 
58
- 1. **Take the diff.** `git diff -U0 -- <paper.md>` since the last commit, or the `new_string` of the
58
+ 1. **Take the diff.** `git diff -U0 -- <paper.tex>` since the last commit, or the `new_string` of the
59
59
  edits just applied. Prose only — skip comments, tables, bibliography.
60
60
  2. **Spawn TWO agents with no repository context, one per persona, in parallel.** Give each the
61
61
  changed paragraphs **as text in the prompt**, not as a file path. A path is an invitation to read
@@ -105,7 +105,7 @@ Two more things the same run proved:
105
105
  - **Expand build-time macros first.** The reader saw seven raw `{{placeholders}}` and correctly
106
106
  reported that every headline number was missing. That is a true statement about the extract and a
107
107
  false one about the paper.
108
- - **Strip working comments.** He graded `<!-- JUSTIFY … -->` notes as prose because he could not tell
108
+ - **Strip working comments.** He graded `% JUSTIFY …` comment notes as prose because he could not tell
109
109
  them apart — and said so, which is itself a finding about the extract.
110
110
 
111
111
  **So:** take the changed lines, then widen each to its enclosing paragraph, expand macros, drop
@@ -60,7 +60,7 @@ one, and its confusion is the finding.
60
60
 
61
61
  ## How to run it
62
62
 
63
- 1. **Take the diff.** \`git diff -U0 -- <paper.md>\` since the last commit, or the \`new_string\` of the
63
+ 1. **Take the diff.** \`git diff -U0 -- <paper.tex>\` since the last commit, or the \`new_string\` of the
64
64
  edits just applied. Prose only — skip comments, tables, bibliography.
65
65
  2. **Spawn TWO agents with no repository context, one per persona, in parallel.** Give each the
66
66
  changed paragraphs **as text in the prompt**, not as a file path. A path is an invitation to read
@@ -110,7 +110,7 @@ Two more things the same run proved:
110
110
  - **Expand build-time macros first.** The reader saw seven raw \`{{placeholders}}\` and correctly
111
111
  reported that every headline number was missing. That is a true statement about the extract and a
112
112
  false one about the paper.
113
- - **Strip working comments.** He graded \`<!-- JUSTIFY … -->\` notes as prose because he could not tell
113
+ - **Strip working comments.** He graded \`% JUSTIFY …\` comment notes as prose because he could not tell
114
114
  them apart — and said so, which is itself a finding about the extract.
115
115
 
116
116
  **So:** take the changed lines, then widen each to its enclosing paragraph, expand macros, drop
@@ -4,7 +4,7 @@ description: Use when asking "does this paper read well?" / "is the writing any
4
4
  allowed-tools: [Read, Write, Edit, Grep, Glob, Agent, Skill]
5
5
  ---
6
6
 
7
- <!-- vigiles:sha256:03d0377baf71967f compiled from skills/grade-paper-writing/SKILL.md.spec.ts -->
7
+ <!-- vigiles:sha256:21c8899648c9b223 compiled from skills/grade-paper-writing/SKILL.md.spec.ts -->
8
8
 
9
9
  # grade-paper-writing — grade how the paper READS, then fix it sentence by sentence
10
10
 
@@ -53,7 +53,7 @@ derived from that gap.
53
53
  ### 🔴 Read the BUILT PDF, not the source
54
54
 
55
55
  **The stall pass reads the typeset pages.** The source is for locating the fix afterwards, never for
56
- judging the read. This was previously written as "read the `.tex`/`.md`, **or** run `render-paper`",
56
+ judging the read. This was previously written as "read the `.tex`, **or** run `render-paper`",
57
57
  and that *or* is why the pass kept missing what the author saw immediately: he reads the PDF.
58
58
 
59
59
  Things that exist only in the render, every one of which produces a real stall:
@@ -153,7 +153,7 @@ is what happened.
153
153
 
154
154
  ## How to run it
155
155
 
156
- 1. **Read the actual draft** (the `.tex`/`.md`, or run `render-paper` to read the built pages as a reader
156
+ 1. **Read the actual draft** (the `.tex`, or run `render-paper` to read the built pages as a reader
157
157
  would). Grade the writing as written — never from a summary.
158
158
  2. **Run the cold-read stall pass FIRST — the full specification is below, under "The stall pass".**
159
159
  It was previously a pointer to a reference section that did not exist, which is why the pass drifted
@@ -215,10 +215,18 @@ is what happened.
215
215
  5. **Flag the two prose traps by name:** (a) Attention-style dense contribution-less prose used where the
216
216
  audience does NOT already care; (b) hedge-stacking ("may possibly in some cases potentially") — replace
217
217
  each pile with one precise scope sentence.
218
- 6. 🔴 **Run `prose-lint.mjs` and treat every FLAG as a finding, not as context.** It is wired into
219
- `paper-lint` on paper edits, so its output arrives whether or not this skill was invoked — but the
220
- skill is what must ACT on it. Its thresholds are sourced; a FLAG means the text sits above a
221
- published human or machine baseline, which is a defect with a number attached, not an opinion.
218
+ 6. 🔴 **Run `prose-lint.mjs` on the paper and treat every FLAG as a finding, not as context:**
219
+
220
+ ```
221
+ node ${CLAUDE_SKILL_DIR}/prose-lint.mjs <paper-dir>/paper.tex # the report
222
+ node ${CLAUDE_SKILL_DIR}/prose-lint.mjs --headings <paper-dir>/paper.tex # the headings as a set
223
+ ```
224
+
225
+ It measures the prose a reader sees — the files the last build read spliced in, no preamble,
226
+ comments, floats or inline `.bib` — so its rates are the paper's, not the source file's. Run
227
+ `npx paperlint build` first: with no current build record it reads `paper.tex` alone and says so. The skill is what must ACT on it.
228
+ Its thresholds are sourced; a FLAG means the text sits above a published human or machine
229
+ baseline, which is a defect with a number attached, not an opinion.
222
230
  Two entries in `THRESHOLDS` exist because this skill previously ran five times without either
223
231
  firing: the `"not X, but Y"` density had **no threshold at all** and printed as a neutral line
224
232
  while standing at 50.8 per 10,000 words against an LLM baseline of 7.8, and the lexicon missed
@@ -227,21 +235,6 @@ is what happened.
227
235
  A number printed without a threshold is prose; that is this project's own thesis, and the linter
228
236
  was violating it.
229
237
 
230
- 6a. 🔴 **`prose-lint.mjs` IS NO LONGER ALONE — seven of its twelve gating metrics moved
231
- on 2026-08-26 into ESLint rules, and running only it shows you five twelfths.** A second
232
- run is mandatory; without it, step 6 is incomplete:
233
-
234
- ```
235
- npx eslint "papers/*/paper.md" "papers/*/draft.md"
236
- ```
237
-
238
- From there come: `paper/citation-density` · `paper/unexplained-jargon` ·
239
- `paper/multi-claim-sentence` · `paper/conceits` · `paper/hedge-density` ·
240
- `paper/discourse-subject` · `paper/undefined-coinage` (file — `eslint-rules/paper-craft.mjs`).
241
- Gain from the move — `file:line:col` on each finding: the old report printed the sentence head,
242
- and you had to find it in the article by eye. Everything step 6b says about
243
- meaning regression applies to them just the same: this is diagnosis, not objective.
244
-
245
238
  6b. 🔴 **A lint fix that costs meaning is a REGRESSION, and no linter can see it.** The metrics above
246
239
  are diagnostics, never objectives. Optimising one directly is how a sentence gets worse while the
247
240
  number gets better — and the number is exactly what makes it feel like progress.
@@ -59,7 +59,7 @@ derived from that gap.
59
59
  ### 🔴 Read the BUILT PDF, not the source
60
60
 
61
61
  **The stall pass reads the typeset pages.** The source is for locating the fix afterwards, never for
62
- judging the read. This was previously written as "read the \`.tex\`/\`.md\`, **or** run \`render-paper\`",
62
+ judging the read. This was previously written as "read the \`.tex\`, **or** run \`render-paper\`",
63
63
  and that *or* is why the pass kept missing what the author saw immediately: he reads the PDF.
64
64
 
65
65
  Things that exist only in the render, every one of which produces a real stall:
@@ -159,7 +159,7 @@ is what happened.
159
159
 
160
160
  ## How to run it
161
161
 
162
- 1. **Read the actual draft** (the \`.tex\`/\`.md\`, or run \`render-paper\` to read the built pages as a reader
162
+ 1. **Read the actual draft** (the \`.tex\`, or run \`render-paper\` to read the built pages as a reader
163
163
  would). Grade the writing as written — never from a summary.
164
164
  2. **Run the cold-read stall pass FIRST — the full specification is below, under "The stall pass".**
165
165
  It was previously a pointer to a reference section that did not exist, which is why the pass drifted
@@ -221,10 +221,18 @@ is what happened.
221
221
  5. **Flag the two prose traps by name:** (a) Attention-style dense contribution-less prose used where the
222
222
  audience does NOT already care; (b) hedge-stacking ("may possibly in some cases potentially") — replace
223
223
  each pile with one precise scope sentence.
224
- 6. 🔴 **Run \`prose-lint.mjs\` and treat every FLAG as a finding, not as context.** It is wired into
225
- \`paper-lint\` on paper edits, so its output arrives whether or not this skill was invoked — but the
226
- skill is what must ACT on it. Its thresholds are sourced; a FLAG means the text sits above a
227
- published human or machine baseline, which is a defect with a number attached, not an opinion.
224
+ 6. 🔴 **Run \`prose-lint.mjs\` on the paper and treat every FLAG as a finding, not as context:**
225
+
226
+ \`\`\`
227
+ node \${CLAUDE_SKILL_DIR}/prose-lint.mjs <paper-dir>/paper.tex # the report
228
+ node \${CLAUDE_SKILL_DIR}/prose-lint.mjs --headings <paper-dir>/paper.tex # the headings as a set
229
+ \`\`\`
230
+
231
+ It measures the prose a reader sees — the files the last build read spliced in, no preamble,
232
+ comments, floats or inline \`.bib\` — so its rates are the paper's, not the source file's. Run
233
+ \`npx paperlint build\` first: with no current build record it reads \`paper.tex\` alone and says so. The skill is what must ACT on it.
234
+ Its thresholds are sourced; a FLAG means the text sits above a published human or machine
235
+ baseline, which is a defect with a number attached, not an opinion.
228
236
  Two entries in \`THRESHOLDS\` exist because this skill previously ran five times without either
229
237
  firing: the \`"not X, but Y"\` density had **no threshold at all** and printed as a neutral line
230
238
  while standing at 50.8 per 10,000 words against an LLM baseline of 7.8, and the lexicon missed
@@ -233,21 +241,6 @@ is what happened.
233
241
  A number printed without a threshold is prose; that is this project's own thesis, and the linter
234
242
  was violating it.
235
243
 
236
- 6a. 🔴 **\`prose-lint.mjs\` IS NO LONGER ALONE — seven of its twelve gating metrics moved
237
- on 2026-08-26 into ESLint rules, and running only it shows you five twelfths.** A second
238
- run is mandatory; without it, step 6 is incomplete:
239
-
240
- \`\`\`
241
- npx eslint "papers/*/paper.md" "papers/*/draft.md"
242
- \`\`\`
243
-
244
- From there come: \`paper/citation-density\` · \`paper/unexplained-jargon\` ·
245
- \`paper/multi-claim-sentence\` · \`paper/conceits\` · \`paper/hedge-density\` ·
246
- \`paper/discourse-subject\` · \`paper/undefined-coinage\` (file — \`eslint-rules/paper-craft.mjs\`).
247
- Gain from the move — \`file:line:col\` on each finding: the old report printed the sentence head,
248
- and you had to find it in the article by eye. Everything step 6b says about
249
- meaning regression applies to them just the same: this is diagnosis, not objective.
250
-
251
244
  6b. 🔴 **A lint fix that costs meaning is a REGRESSION, and no linter can see it.** The metrics above
252
245
  are diagnostics, never objectives. Optimising one directly is how a sentence gets worse while the
253
246
  number gets better — and the number is exactly what makes it feel like progress.