paperlint 7.2.0 → 9.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (474) hide show
  1. package/CLAUDE.md +9 -25
  2. package/CONTRIBUTING.md +5 -4
  3. package/README.md +4 -3
  4. package/dist/adapters/bibtex/index.d.ts +3 -0
  5. package/dist/adapters/bibtex/index.d.ts.map +1 -0
  6. package/dist/adapters/bibtex/index.js +103 -0
  7. package/dist/adapters/bibtex/index.js.map +1 -0
  8. package/dist/adapters/latex/filecontents.d.ts +33 -0
  9. package/dist/adapters/latex/filecontents.d.ts.map +1 -0
  10. package/dist/adapters/latex/filecontents.js +65 -0
  11. package/dist/adapters/latex/filecontents.js.map +1 -0
  12. package/dist/adapters/latex/includes.d.ts +3 -3
  13. package/dist/adapters/latex/includes.d.ts.map +1 -1
  14. package/dist/adapters/latex/includes.js +18 -10
  15. package/dist/adapters/latex/includes.js.map +1 -1
  16. package/dist/adapters/latex/index.d.ts.map +1 -1
  17. package/dist/adapters/latex/index.js +2 -0
  18. package/dist/adapters/latex/index.js.map +1 -1
  19. package/dist/adapters/latex/nodes.d.ts +12 -0
  20. package/dist/adapters/latex/nodes.d.ts.map +1 -1
  21. package/dist/adapters/latex/nodes.js +39 -0
  22. package/dist/adapters/latex/nodes.js.map +1 -1
  23. package/dist/adapters/latex/parse.d.ts +7 -1
  24. package/dist/adapters/latex/parse.d.ts.map +1 -1
  25. package/dist/adapters/latex/parse.js +55 -6
  26. package/dist/adapters/latex/parse.js.map +1 -1
  27. package/dist/adapters/memory/index.d.ts +3 -0
  28. package/dist/adapters/memory/index.d.ts.map +1 -1
  29. package/dist/adapters/memory/index.js +9 -0
  30. package/dist/adapters/memory/index.js.map +1 -1
  31. package/dist/adapters/references/index.d.ts.map +1 -1
  32. package/dist/adapters/references/index.js +27 -24
  33. package/dist/adapters/references/index.js.map +1 -1
  34. package/dist/adapters/sources-record/index.d.ts +9 -0
  35. package/dist/adapters/sources-record/index.d.ts.map +1 -0
  36. package/dist/adapters/sources-record/index.js +68 -0
  37. package/dist/adapters/sources-record/index.js.map +1 -0
  38. package/dist/adapters/tex-output/blg.d.ts +9 -0
  39. package/dist/adapters/tex-output/blg.d.ts.map +1 -0
  40. package/dist/adapters/tex-output/blg.js +20 -0
  41. package/dist/adapters/tex-output/blg.js.map +1 -0
  42. package/dist/adapters/tex-output/fls.d.ts +8 -0
  43. package/dist/adapters/tex-output/fls.d.ts.map +1 -0
  44. package/dist/adapters/tex-output/fls.js +16 -0
  45. package/dist/adapters/tex-output/fls.js.map +1 -0
  46. package/dist/adapters/tex-output/index.d.ts +7 -0
  47. package/dist/adapters/tex-output/index.d.ts.map +1 -0
  48. package/dist/adapters/tex-output/index.js +7 -0
  49. package/dist/adapters/tex-output/index.js.map +1 -0
  50. package/dist/bib-rules.d.ts +47 -0
  51. package/dist/bib-rules.d.ts.map +1 -0
  52. package/dist/bib-rules.js +127 -0
  53. package/dist/bib-rules.js.map +1 -0
  54. package/dist/build.d.ts +29 -1
  55. package/dist/build.d.ts.map +1 -1
  56. package/dist/build.js +41 -30
  57. package/dist/build.js.map +1 -1
  58. package/dist/claim-provenance.d.ts +2 -2
  59. package/dist/cli.d.ts +9 -5
  60. package/dist/cli.d.ts.map +1 -1
  61. package/dist/cli.js +43 -68
  62. package/dist/cli.js.map +1 -1
  63. package/dist/domain/paper-source.d.ts +1 -10
  64. package/dist/domain/paper-source.d.ts.map +1 -1
  65. package/dist/domain/paper-source.js +34 -24
  66. package/dist/domain/paper-source.js.map +1 -1
  67. package/dist/domain/paper-sources.d.ts +72 -0
  68. package/dist/domain/paper-sources.d.ts.map +1 -0
  69. package/dist/domain/paper-sources.js +18 -0
  70. package/dist/domain/paper-sources.js.map +1 -0
  71. package/dist/domain/sources-record.d.ts +102 -0
  72. package/dist/domain/sources-record.d.ts.map +1 -0
  73. package/dist/domain/sources-record.js +38 -0
  74. package/dist/domain/sources-record.js.map +1 -0
  75. package/dist/domain/tex-run.d.ts +25 -0
  76. package/dist/domain/tex-run.d.ts.map +1 -0
  77. package/dist/domain/tex-run.js +79 -0
  78. package/dist/domain/tex-run.js.map +1 -0
  79. package/dist/eslint-rules/latex-language.d.ts.map +1 -1
  80. package/dist/eslint-rules/latex-language.js +10 -13
  81. package/dist/eslint-rules/latex-language.js.map +1 -1
  82. package/dist/eslint-rules/paper-context.d.ts.map +1 -1
  83. package/dist/eslint-rules/paper-context.js +6 -5
  84. package/dist/eslint-rules/paper-context.js.map +1 -1
  85. package/dist/eslint-rules/paper-typography.d.ts +0 -9
  86. package/dist/eslint-rules/paper-typography.d.ts.map +1 -1
  87. package/dist/eslint-rules/paper-typography.js +34 -96
  88. package/dist/eslint-rules/paper-typography.js.map +1 -1
  89. package/dist/eslint-rules/papers.d.ts +1 -3
  90. package/dist/eslint-rules/papers.d.ts.map +1 -1
  91. package/dist/eslint-rules/papers.js +2 -4
  92. package/dist/eslint-rules/papers.js.map +1 -1
  93. package/dist/eslint-rules/tex-build.d.ts.map +1 -1
  94. package/dist/eslint-rules/tex-build.js +8 -25
  95. package/dist/eslint-rules/tex-build.js.map +1 -1
  96. package/dist/init.d.ts +1 -4
  97. package/dist/init.d.ts.map +1 -1
  98. package/dist/init.js +3 -9
  99. package/dist/init.js.map +1 -1
  100. package/dist/latex-log.d.ts +10 -0
  101. package/dist/latex-log.d.ts.map +1 -1
  102. package/dist/latex-log.js +10 -0
  103. package/dist/latex-log.js.map +1 -1
  104. package/dist/latex-loop.d.ts +2 -0
  105. package/dist/latex-loop.d.ts.map +1 -1
  106. package/dist/latex-loop.js.map +1 -1
  107. package/dist/lib/markdown.d.ts +5 -81
  108. package/dist/lib/markdown.d.ts.map +1 -1
  109. package/dist/lib/markdown.js +5 -116
  110. package/dist/lib/markdown.js.map +1 -1
  111. package/dist/lib/paper-config.d.ts.map +1 -1
  112. package/dist/lib/paper-config.js +0 -2
  113. package/dist/lib/paper-config.js.map +1 -1
  114. package/dist/lib/skill-eval-fixture.d.ts +6 -4
  115. package/dist/lib/skill-eval-fixture.d.ts.map +1 -1
  116. package/dist/lib/skill-eval-fixture.js +25 -18
  117. package/dist/lib/skill-eval-fixture.js.map +1 -1
  118. package/dist/new-paper.d.ts +5 -11
  119. package/dist/new-paper.d.ts.map +1 -1
  120. package/dist/new-paper.js +12 -23
  121. package/dist/new-paper.js.map +1 -1
  122. package/dist/package-dirs.d.ts.map +1 -1
  123. package/dist/package-dirs.js +3 -2
  124. package/dist/package-dirs.js.map +1 -1
  125. package/dist/paper-files.d.ts +4 -4
  126. package/dist/paper-includes.d.ts +4 -7
  127. package/dist/paper-includes.d.ts.map +1 -1
  128. package/dist/paper-includes.js +29 -21
  129. package/dist/paper-includes.js.map +1 -1
  130. package/dist/paper-prose.d.ts +17 -0
  131. package/dist/paper-prose.d.ts.map +1 -0
  132. package/dist/paper-prose.js +46 -0
  133. package/dist/paper-prose.js.map +1 -0
  134. package/dist/paper-record.d.ts +25 -0
  135. package/dist/paper-record.d.ts.map +1 -0
  136. package/dist/paper-record.js +39 -0
  137. package/dist/paper-record.js.map +1 -0
  138. package/dist/paper-sources.d.ts +66 -0
  139. package/dist/paper-sources.d.ts.map +1 -0
  140. package/dist/paper-sources.js +92 -0
  141. package/dist/paper-sources.js.map +1 -0
  142. package/dist/ports/bib-reader.d.ts +18 -0
  143. package/dist/ports/bib-reader.d.ts.map +1 -0
  144. package/dist/ports/bib-reader.js +2 -0
  145. package/dist/ports/bib-reader.js.map +1 -0
  146. package/dist/ports/check-references.d.ts +9 -4
  147. package/dist/ports/check-references.d.ts.map +1 -1
  148. package/dist/ports/latex.d.ts +15 -0
  149. package/dist/ports/latex.d.ts.map +1 -1
  150. package/dist/ports/sources-codec.d.ts +11 -0
  151. package/dist/ports/sources-codec.d.ts.map +1 -0
  152. package/dist/ports/sources-codec.js +2 -0
  153. package/dist/ports/sources-codec.js.map +1 -0
  154. package/dist/ports/tex-output.d.ts +17 -0
  155. package/dist/ports/tex-output.d.ts.map +1 -0
  156. package/dist/ports/tex-output.js +2 -0
  157. package/dist/ports/tex-output.js.map +1 -0
  158. package/dist/recorded-bibliography.d.ts +89 -0
  159. package/dist/recorded-bibliography.d.ts.map +1 -0
  160. package/dist/recorded-bibliography.js +136 -0
  161. package/dist/recorded-bibliography.js.map +1 -0
  162. package/dist/reference-rules.d.ts +5 -5
  163. package/dist/reference-rules.d.ts.map +1 -1
  164. package/dist/reference-rules.js +150 -77
  165. package/dist/reference-rules.js.map +1 -1
  166. package/dist/references.d.ts +71 -16
  167. package/dist/references.d.ts.map +1 -1
  168. package/dist/references.js +110 -26
  169. package/dist/references.js.map +1 -1
  170. package/dist/register-bands.d.ts +3 -3
  171. package/dist/register-bands.d.ts.map +1 -1
  172. package/dist/register-bands.js.map +1 -1
  173. package/dist/register.d.ts +2 -2
  174. package/dist/sources-record.d.ts +38 -0
  175. package/dist/sources-record.d.ts.map +1 -0
  176. package/dist/sources-record.js +123 -0
  177. package/dist/sources-record.js.map +1 -0
  178. package/dist/sources-rules.d.ts +44 -0
  179. package/dist/sources-rules.d.ts.map +1 -0
  180. package/dist/sources-rules.js +76 -0
  181. package/dist/sources-rules.js.map +1 -0
  182. package/dist/structure.d.ts.map +1 -1
  183. package/dist/structure.js +4 -3
  184. package/dist/structure.js.map +1 -1
  185. package/dist/tex-paper.d.ts +10 -15
  186. package/dist/tex-paper.d.ts.map +1 -1
  187. package/dist/tex-paper.js +24 -40
  188. package/dist/tex-paper.js.map +1 -1
  189. package/dist/tex-venue-rules.d.ts +3 -3
  190. package/dist/tex-venue-rules.d.ts.map +1 -1
  191. package/dist/tex-venue-rules.js.map +1 -1
  192. package/dist/types.d.ts +0 -2
  193. package/dist/types.d.ts.map +1 -1
  194. package/docs/configuration.md +18 -12
  195. package/docs/design/paper-sources.md +197 -0
  196. package/docs/e2e.md +4 -6
  197. package/docs/package-shape-options.md +1 -1
  198. package/docs/prior-art/paper-folder-scaffolding.md +24 -29
  199. package/docs/references.md +34 -8
  200. package/docs/rules/bib/reachable-entry.md +64 -0
  201. package/docs/rules/paper/sources-fresh.md +81 -0
  202. package/docs/rules/tex/claim-provenance.md +0 -1
  203. package/docs/rules.md +62 -61
  204. package/eslint-rules/latex-language.harness.mjs +3 -3
  205. package/eslint-rules/latex-language.ts +10 -13
  206. package/eslint-rules/paper-context.ts +6 -5
  207. package/eslint-rules/paper-typography.test.mjs +29 -157
  208. package/eslint-rules/paper-typography.ts +35 -104
  209. package/eslint-rules/papers.harness.mjs +1 -1
  210. package/eslint-rules/papers.ts +2 -4
  211. package/eslint-rules/tex-build.harness.mjs +2 -2
  212. package/eslint-rules/tex-build.ts +8 -25
  213. package/eslint.config.mjs +22 -1
  214. package/fixtures/build-e2e/aidc/paper.tex +1 -1
  215. package/fixtures/build-e2e/anon-ack/paper.tex +1 -1
  216. package/fixtures/build-e2e/anon-author/paper.tex +1 -1
  217. package/fixtures/build-e2e/anon-bib/paper.tex +1 -1
  218. package/fixtures/build-e2e/anon-href/paper.tex +1 -1
  219. package/fixtures/build-e2e/anon-macro/paper.tex +1 -1
  220. package/fixtures/build-e2e/anon-pdfauthor/paper.tex +1 -1
  221. package/fixtures/paper-sources/p1/paper.tex +30 -0
  222. package/fixtures/paper-sources/p1/refs.bib +6 -0
  223. package/fixtures/paper-sources/p1/sections/dead.tex +6 -0
  224. package/fixtures/paper-sources/p1/sections/intro.tex +7 -0
  225. package/fixtures/paper-sources/p1/sections/parked.tex +6 -0
  226. package/fixtures/paper-sources/p1/tex-truth.json +37 -0
  227. package/fixtures/paper-sources/p1/versions/2026-01-01-review.tex +4 -0
  228. package/fixtures/paper-sources/v1-stale/paper.tex +14 -0
  229. package/fixtures/paper-sources/v1-stale/refs.bib +6 -0
  230. package/fixtures/paper-sources/v1-stale/tex-truth.json +32 -0
  231. package/fixtures/paper-sources/v10-ifthenelse/anon.bib +1 -0
  232. package/fixtures/paper-sources/v10-ifthenelse/paper.tex +9 -0
  233. package/fixtures/paper-sources/v10-ifthenelse/real.bib +1 -0
  234. package/fixtures/paper-sources/v10-ifthenelse/tex-truth.json +28 -0
  235. package/fixtures/paper-sources/v11-two-blocks/paper.tex +12 -0
  236. package/fixtures/paper-sources/v11-two-blocks/tex-truth.json +30 -0
  237. package/fixtures/paper-sources/v13-percent-text/paper.tex +6 -0
  238. package/fixtures/paper-sources/v13-percent-text/refs.bib +3 -0
  239. package/fixtures/paper-sources/v13-percent-text/tex-truth.json +30 -0
  240. package/fixtures/paper-sources/v15-after-end/paper.tex +6 -0
  241. package/fixtures/paper-sources/v15-after-end/refs.bib +1 -0
  242. package/fixtures/paper-sources/v15-after-end/tex-truth.json +17 -0
  243. package/fixtures/paper-sources/v16-included-block/bibblock.tex +3 -0
  244. package/fixtures/paper-sources/v16-included-block/paper.tex +7 -0
  245. package/fixtures/paper-sources/v16-included-block/refs.bib +1 -0
  246. package/fixtures/paper-sources/v16-included-block/tex-truth.json +35 -0
  247. package/fixtures/paper-sources/v17-include-order/bibsetup.tex +1 -0
  248. package/fixtures/paper-sources/v17-include-order/first.bib +1 -0
  249. package/fixtures/paper-sources/v17-include-order/later.bib +1 -0
  250. package/fixtures/paper-sources/v17-include-order/paper.tex +7 -0
  251. package/fixtures/paper-sources/v17-include-order/tex-truth.json +40 -0
  252. package/fixtures/paper-sources/v2-overwrite/paper.tex +14 -0
  253. package/fixtures/paper-sources/v2-overwrite/refs.bib +6 -0
  254. package/fixtures/paper-sources/v2-overwrite/tex-truth.json +30 -0
  255. package/fixtures/paper-sources/v22-parked-thebibliography/paper.tex +7 -0
  256. package/fixtures/paper-sources/v22-parked-thebibliography/tex-truth.json +17 -0
  257. package/fixtures/paper-sources/v23-parked-include/old.bib +1 -0
  258. package/fixtures/paper-sources/v23-parked-include/paper.tex +7 -0
  259. package/fixtures/paper-sources/v23-parked-include/parked.tex +1 -0
  260. package/fixtures/paper-sources/v23-parked-include/refs.bib +1 -0
  261. package/fixtures/paper-sources/v23-parked-include/tex-truth.json +28 -0
  262. package/fixtures/paper-sources/v24-quoted-fields/paper.tex +6 -0
  263. package/fixtures/paper-sources/v24-quoted-fields/refs.bib +5 -0
  264. package/fixtures/paper-sources/v24-quoted-fields/tex-truth.json +32 -0
  265. package/fixtures/paper-sources/v25-string-differs/paper.tex +10 -0
  266. package/fixtures/paper-sources/v25-string-differs/refs.bib +2 -0
  267. package/fixtures/paper-sources/v25-string-differs/tex-truth.json +32 -0
  268. package/fixtures/paper-sources/v26-comment-in-names/one.bib +1 -0
  269. package/fixtures/paper-sources/v26-comment-in-names/paper.tex +7 -0
  270. package/fixtures/paper-sources/v26-comment-in-names/tex-truth.json +32 -0
  271. package/fixtures/paper-sources/v26-comment-in-names/two.bib +1 -0
  272. package/fixtures/paper-sources/v27-shared-string/abbreviations.bib +2 -0
  273. package/fixtures/paper-sources/v27-shared-string/paper.tex +6 -0
  274. package/fixtures/paper-sources/v27-shared-string/references.bib +6 -0
  275. package/fixtures/paper-sources/v27-shared-string/tex-truth.json +31 -0
  276. package/fixtures/paper-sources/v28-shared-string-block/abbreviations.bib +2 -0
  277. package/fixtures/paper-sources/v28-shared-string-block/paper.tex +14 -0
  278. package/fixtures/paper-sources/v28-shared-string-block/tex-truth.json +33 -0
  279. package/fixtures/paper-sources/v29-generated-input/paper.tex +9 -0
  280. package/fixtures/paper-sources/v29-generated-input/tex-truth.json +24 -0
  281. package/fixtures/paper-sources/v3-declared/paper.bib +6 -0
  282. package/fixtures/paper-sources/v3-declared/paper.tex +6 -0
  283. package/fixtures/paper-sources/v3-declared/tex-truth.json +28 -0
  284. package/fixtures/paper-sources/v30-name-next-line/paper.tex +14 -0
  285. package/fixtures/paper-sources/v30-name-next-line/tex-truth.json +30 -0
  286. package/fixtures/paper-sources/v4-commented/paper.tex +14 -0
  287. package/fixtures/paper-sources/v4-commented/refs.bib +6 -0
  288. package/fixtures/paper-sources/v4-commented/tex-truth.json +28 -0
  289. package/fixtures/paper-sources/v5-percent-entry/paper.tex +20 -0
  290. package/fixtures/paper-sources/v5-percent-entry/tex-truth.json +37 -0
  291. package/fixtures/paper-sources/v6-unclosed/paper.tex +6 -0
  292. package/fixtures/paper-sources/v6-unclosed/refs.bib +5 -0
  293. package/fixtures/paper-sources/v6-unclosed/tex-truth.json +37 -0
  294. package/fixtures/paper-sources/v8-jobname/paper.tex +9 -0
  295. package/fixtures/paper-sources/v8-jobname/tex-truth.json +30 -0
  296. package/fixtures/paper-sources/v9-two-bibliographies/one.bib +1 -0
  297. package/fixtures/paper-sources/v9-two-bibliographies/paper.tex +7 -0
  298. package/fixtures/paper-sources/v9-two-bibliographies/tex-truth.json +35 -0
  299. package/fixtures/paper-sources/v9-two-bibliographies/two.bib +1 -0
  300. package/fixtures/paper-typography/clean-paper/paper.tex +1 -1
  301. package/fixtures/paper-typography/messy-paper/paper.tex +1 -1
  302. package/hooks/hooks.harness.ts +15 -15
  303. package/hooks/paper-edit-guard.hook.mjs +5 -8
  304. package/hooks/paper-edit-guard.test.ts +12 -3
  305. package/hooks/paper-skills-nudge.hook.mjs +2 -3
  306. package/hooks/paper-skills-nudge.test.ts +13 -3
  307. package/hooks/paper-status-gates.hook.mjs +2 -2
  308. package/hooks/paper-status-gates.test.ts +14 -2
  309. package/lib/markdown.test.mjs +5 -60
  310. package/lib/markdown.ts +9 -136
  311. package/lib/paper-config.ts +0 -2
  312. package/lib/skill-checks.test.mjs +4 -1
  313. package/lib/skill-eval-fixture.ts +25 -18
  314. package/package.json +2 -12
  315. package/presets/realm.jsonc +1 -1
  316. package/scripts/check.test.ts +23 -1
  317. package/scripts/check.ts +7 -3
  318. package/scripts/coverage-config.test.ts +19 -10
  319. package/scripts/harness-api.frozen.json +0 -5
  320. package/scripts/layer-legacy-frozen.test.ts +41 -2
  321. package/scripts/layer-legacy-frozen.ts +31 -2
  322. package/skills/cold-read-diff/SKILL.md +3 -3
  323. package/skills/cold-read-diff/SKILL.md.spec.ts +2 -2
  324. package/skills/grade-paper-writing/SKILL.md +15 -22
  325. package/skills/grade-paper-writing/SKILL.md.spec.ts +14 -21
  326. package/skills/grade-paper-writing/__snapshots__/prose-lint.test.ts.snap +40 -26
  327. package/skills/grade-paper-writing/prose-lint.mjs +117 -278
  328. package/skills/grade-paper-writing/prose-lint.test.ts +230 -79
  329. package/skills/paper-adversarial-review/SKILL.md +2 -2
  330. package/skills/paper-adversarial-review/SKILL.md.spec.ts +1 -1
  331. package/skills/paper-pipeline/SKILL.md +9 -12
  332. package/skills/paper-pipeline/SKILL.md.spec.ts +8 -11
  333. package/skills/paper-pipeline/description-language.eval.mjs +25 -15
  334. package/skills/paper-pipeline/framing-vs-vocabulary.eval.mjs +25 -15
  335. package/skills/paper-pipeline/grade-paper-writing-ablation.eval.mjs +25 -15
  336. package/skills/paper-pipeline/pipeline-firing.eval.mjs +27 -16
  337. package/skills/paper-pipeline/pipeline-language.eval.mjs +25 -15
  338. package/skills/paper-pipeline/references/acceptance-gate.md +16 -20
  339. package/skills/paper-pipeline/references/artifact-checklist.md +4 -5
  340. package/skills/paper-pipeline/references/pipeline-status-template.md +1 -1
  341. package/skills/paper-pipeline/scripts/README.md +0 -4
  342. package/skills/paper-pipeline/scripts/extract-ref-facts.harness.mjs +91 -105
  343. package/skills/paper-pipeline/scripts/extract-ref-facts.mjs +85 -241
  344. package/skills/paper-pipeline/scripts/extract-ref-facts.test.mjs +166 -153
  345. package/skills/paper-pipeline/scripts/generated-code.harness.mjs +3 -3
  346. package/skills/paper-status/SKILL.md +2 -2
  347. package/skills/paper-status/SKILL.md.spec.ts +1 -1
  348. package/skills/render-paper/SKILL.md +2 -2
  349. package/skills/render-paper/SKILL.md.spec.ts +1 -1
  350. package/skills/render-paper/extract-pdf-facts.mjs +2 -2
  351. package/skills/tighten-paper/SKILL.md +29 -42
  352. package/skills/tighten-paper/SKILL.md.spec.ts +28 -41
  353. package/skills/verify-citations/SKILL.md +7 -4
  354. package/skills/verify-citations/SKILL.md.spec.ts +6 -3
  355. package/skills/verify-citations/scripts/bib-authors.check.test.mjs +17 -32
  356. package/skills/verify-citations/scripts/bib-authors.cli.test.ts +138 -43
  357. package/skills/verify-citations/scripts/bib-authors.d.mts +22 -2
  358. package/skills/verify-citations/scripts/bib-authors.mjs +54 -193
  359. package/skills/verify-citations/scripts/bib-authors.test.mjs +1 -44
  360. package/skills/verify-citations/scripts/verify-cites.cli.test.ts +16 -0
  361. package/skills/verify-citations/scripts/verify-cites.d.mts +19 -3
  362. package/skills/verify-citations/scripts/verify-cites.mjs +42 -108
  363. package/skills/verify-citations/scripts/verify-cites.test.mjs +23 -36
  364. package/src/adapters/bibtex/index.test.ts +163 -0
  365. package/src/adapters/bibtex/index.ts +155 -0
  366. package/src/adapters/latex/filecontents.ts +114 -0
  367. package/src/adapters/latex/includes.ts +18 -10
  368. package/src/adapters/latex/index.test.ts +73 -0
  369. package/src/adapters/latex/index.ts +2 -0
  370. package/src/adapters/latex/nodes.ts +64 -0
  371. package/src/adapters/latex/parse.test.ts +38 -0
  372. package/src/adapters/latex/parse.ts +67 -6
  373. package/src/adapters/memory/index.ts +17 -0
  374. package/src/adapters/references/index.test.ts +75 -21
  375. package/src/adapters/references/index.ts +37 -32
  376. package/src/adapters/sources-record/index.test.ts +87 -0
  377. package/src/adapters/sources-record/index.ts +82 -0
  378. package/src/adapters/tex-output/blg.ts +27 -0
  379. package/src/adapters/tex-output/fls.ts +24 -0
  380. package/src/adapters/tex-output/index.test.ts +171 -0
  381. package/src/adapters/tex-output/index.ts +12 -0
  382. package/src/bib-rules.test.ts +356 -0
  383. package/src/bib-rules.ts +228 -0
  384. package/src/bibliography-at.test.ts +286 -0
  385. package/src/build.harness.mjs +2 -2
  386. package/src/build.test.ts +211 -1
  387. package/src/build.ts +101 -41
  388. package/src/claim-provenance.test.ts +33 -7
  389. package/src/claim-provenance.ts +2 -2
  390. package/src/cli.harness.mjs +46 -56
  391. package/src/cli.test.ts +44 -48
  392. package/src/cli.ts +51 -82
  393. package/src/domain/paper-source.test.ts +58 -20
  394. package/src/domain/paper-source.ts +38 -42
  395. package/src/domain/paper-sources.ts +94 -0
  396. package/src/domain/sources-record.test.ts +71 -0
  397. package/src/domain/sources-record.ts +136 -0
  398. package/src/domain/tex-run.test.ts +228 -0
  399. package/src/domain/tex-run.ts +139 -0
  400. package/src/folder-venue-rule.test.ts +1 -1
  401. package/src/heading-case.test.ts +24 -10
  402. package/src/init.test.ts +6 -7
  403. package/src/init.ts +3 -22
  404. package/src/latex-log.ts +20 -0
  405. package/src/latex-loop.harness.ts +1 -0
  406. package/src/latex-loop.ts +2 -0
  407. package/src/lint-fix.test.ts +10 -4
  408. package/src/linted-set.test.ts +9 -1
  409. package/src/new-paper.harness.mjs +12 -30
  410. package/src/new-paper.test.ts +15 -11
  411. package/src/new-paper.ts +11 -30
  412. package/src/package-dirs.ts +3 -2
  413. package/src/paper-files.test.ts +5 -4
  414. package/src/paper-files.ts +4 -4
  415. package/src/paper-includes.test.ts +157 -38
  416. package/src/paper-includes.ts +39 -35
  417. package/src/paper-prose.test.ts +78 -0
  418. package/src/paper-prose.ts +67 -0
  419. package/src/paper-record.test.ts +173 -0
  420. package/src/paper-record.ts +66 -0
  421. package/src/paper-settings-commands.test.ts +5 -2
  422. package/src/paper-sources.ts +151 -0
  423. package/src/parse-args.test.ts +0 -1
  424. package/src/ports/bib-reader.ts +27 -0
  425. package/src/ports/check-references.ts +7 -3
  426. package/src/ports/latex.ts +19 -0
  427. package/src/ports/sources-codec.ts +11 -0
  428. package/src/ports/tex-output.ts +17 -0
  429. package/src/recorded-bibliography.ts +297 -0
  430. package/src/reference-rules.ts +217 -104
  431. package/src/references.test.ts +520 -177
  432. package/src/references.ts +185 -37
  433. package/src/register-bands.test.ts +5 -1
  434. package/src/register-bands.ts +3 -5
  435. package/src/register.test.ts +6 -1
  436. package/src/register.ts +2 -2
  437. package/src/rule-cases.test.ts +68 -12
  438. package/src/rule-docs.test.ts +0 -1
  439. package/src/sources-record.test.ts +308 -0
  440. package/src/sources-record.ts +231 -0
  441. package/src/sources-rules.test.ts +152 -0
  442. package/src/sources-rules.ts +117 -0
  443. package/src/structure.harness.ts +5 -11
  444. package/src/structure.ts +4 -3
  445. package/src/tex-paper.test.ts +148 -78
  446. package/src/tex-paper.ts +31 -61
  447. package/src/tex-venue-rules.test.ts +36 -8
  448. package/src/tex-venue-rules.ts +3 -5
  449. package/src/types.ts +0 -2
  450. package/dist/eslint-rules/bib-reachable-entry.d.ts +0 -22
  451. package/dist/eslint-rules/bib-reachable-entry.d.ts.map +0 -1
  452. package/dist/eslint-rules/bib-reachable-entry.js +0 -75
  453. package/dist/eslint-rules/bib-reachable-entry.js.map +0 -1
  454. package/eslint-rules/bib-reachable-entry.ts +0 -76
  455. package/fixtures/real-markdown-paper/baseline.json +0 -22
  456. package/fixtures/real-markdown-paper/baseline.ts +0 -70
  457. package/skills/paper-pipeline/PIPELINE-MAP.md +0 -381
  458. package/skills/paper-pipeline/pipeline-firing.baseline.json +0 -270
  459. package/skills/paper-pipeline/references/sweep-2026-08-09-ideal-pipeline.md +0 -585
  460. package/skills/paper-pipeline/scripts/artifact-coverage.harness.mjs +0 -492
  461. package/skills/paper-pipeline/scripts/artifact-coverage.mjs +0 -397
  462. package/skills/paper-pipeline/scripts/artifact-coverage.test.ts +0 -86
  463. package/skills/paper-pipeline/scripts/check-provenance.mjs +0 -184
  464. package/skills/paper-pipeline/scripts/population-map.harness.mjs +0 -135
  465. package/skills/paper-pipeline/scripts/population-map.mjs +0 -188
  466. package/skills/paper-pipeline/scripts/population-map.test.mjs +0 -154
  467. package/skills/paper-pipeline/scripts/provenance.harness.mjs +0 -266
  468. package/skills/paper-pipeline/scripts/round-diff.harness.ts +0 -907
  469. package/skills/paper-pipeline/scripts/round-diff.mjs +0 -583
  470. package/skills/paper-pipeline/scripts/round-diff.test.mjs +0 -159
  471. package/skills/tighten-paper/__snapshots__/structure.test.ts.snap +0 -133
  472. package/skills/tighten-paper/structure.mjs +0 -364
  473. package/skills/tighten-paper/structure.test.ts +0 -117
  474. package/templates/paper/paper.md +0 -4
@@ -1,159 +0,0 @@
1
- /**
2
- * round-diff.mjs beyond its harness: the manifest reader's edge lines, `*` coverage, the census of
3
- * an empty text, and — in real git repositories — manifests without a round number, a first base
4
- * that does not resolve, a removed section that is declared, more than eight new numbers, and the
5
- * command with no git on PATH, no paper.md, and no directory argument.
6
- */
7
- import assert from "node:assert/strict";
8
- import { spawnSync } from "node:child_process";
9
- import { chmodSync } from "node:fs";
10
- import { dirname, join } from "node:path";
11
- import { fileURLToPath } from "node:url";
12
- import { test } from "vitest";
13
- import { runNode, useTempDir, writeTree } from "../../../test/support.ts";
14
- import { census, check, covers, parseManifest } from "./round-diff.mjs";
15
-
16
- const SCRIPT = join(dirname(fileURLToPath(import.meta.url)), "round-diff.mjs");
17
- const root = useTempDir("round-diff-");
18
- let n = 0;
19
-
20
- /** A git repo whose HEAD holds `base` as paper.md; the working tree then holds `now`. */
21
- function repo(base, now = base, rounds = {}) {
22
- const dir = join(root, `r${String(n++)}`, "paper");
23
- writeTree(dir, { "paper.md": base });
24
- const g = (...a) => spawnSync("git", a, { cwd: dir, encoding: "utf8" });
25
- g("init", "-q");
26
- g(
27
- "-c",
28
- "user.name=t",
29
- "-c",
30
- "user.email=t@t",
31
- "commit",
32
- "-qm",
33
- "base",
34
- "--allow-empty",
35
- );
36
- g("add", "paper.md");
37
- g("-c", "user.name=t", "-c", "user.email=t@t", "commit", "-qm", "paper");
38
- const head = g("rev-parse", "HEAD").stdout.trim();
39
- writeTree(dir, {
40
- "paper.md": now,
41
- ...Object.fromEntries(
42
- Object.entries(rounds).map(([f, body]) => [
43
- `rounds/${f}`,
44
- body.replaceAll("HEAD_SHA", head),
45
- ]),
46
- ),
47
- });
48
- return dir;
49
- }
50
- const kinds = (r) => r.findings.map((f) => f.kind);
51
-
52
- test("manifest: no frontmatter, comments and blanks, a list item before any key, lists both ways", () => {
53
- assert.deepEqual(parseManifest("no frontmatter", "m.md"), {
54
- error: "no frontmatter block",
55
- path: "m.md",
56
- });
57
- assert.deepEqual(
58
- parseManifest(
59
- [
60
- "---",
61
- "- stray item",
62
- "# a comment",
63
- "",
64
- "base: abc # the base",
65
- "touches:",
66
- " - 1",
67
- " - Intro",
68
- "allows: [new-number, 'new-citation']",
69
- "not a key line",
70
- "notes:",
71
- "---",
72
- "",
73
- ].join("\n"),
74
- "m.md",
75
- ),
76
- {
77
- path: "m.md",
78
- touches: ["1", "Intro"],
79
- allows: ["new-number", "new-citation"],
80
- base: "abc",
81
- notes: [],
82
- },
83
- );
84
- });
85
-
86
- test("`*` covers every heading; an empty text has no hedge density", () => {
87
- assert.equal(covers("*", "7. Anything"), true);
88
- assert.equal(census("").hedgeDensity, 0);
89
- });
90
-
91
- const P = (...secs) => secs.map(([h, t]) => `## ${h}\n\n${t}\n`).join("\n");
92
- // Untouched sections, so a round that touches one or two is not an over-broad scope.
93
- const REST = ["3. Three", "4. Four", "5. Five", "6. Six"].map((h) => [
94
- h,
95
- "Same.",
96
- ]);
97
- const round = (r, base, touches, extra = "") =>
98
- `---\n${r === null ? "" : `round: ${r}\n`}base: ${base}\ntouches: [${touches}]\nbudget: 1000\n${extra}---\n`;
99
-
100
- test("a manifest without a round number is named by its file, not as `round undefined`; its unresolvable base is named", () => {
101
- const text = P(["1. Intro", "Words here."], ...REST);
102
- const dir = repo(text, text, {
103
- "a.md": round(null, "deadbeef", "1", "closed: true\n"),
104
- "b.md": round(null, "HEAD_SHA", "1"),
105
- });
106
- const r = check(dir);
107
- assert.deepEqual(kinds(r), ["unresolvable-base"]);
108
- assert.match(
109
- r.findings[0].msg,
110
- /^paper\/rounds\/a\.md's base `deadbeef` does not resolve/,
111
- );
112
- });
113
-
114
- test("a removed section the round declares is not a finding; more than eight new numbers are elided", () => {
115
- const base = P(["1. Intro", "Words."], ["2. Gone", "Old words."], ...REST);
116
- const now = P(["1. Intro", "Words 1 2 3 4 5 6 7 8 9 10."], ...REST);
117
- const dir = repo(base, now, { "a.md": round(1, "HEAD_SHA", "1, 2") });
118
- const r = check(dir);
119
- assert.deepEqual(kinds(r), ["unauthorised-number"]);
120
- assert.match(
121
- r.findings[0].msg,
122
- /^10 numeric literal\(s\) new to the body this round \(1, 2, 3, 4, 5, 6, 7, 8, …\)/,
123
- );
124
- });
125
-
126
- test("the command: no paper.md is a note; no git on PATH is 'not a git repository'; no argument is the cwd", () => {
127
- const empty = writeTree(join(root, "empty"), { "x.txt": "" });
128
- const none = runNode(SCRIPT, [], { cwd: empty });
129
- assert.equal(none.status, 0);
130
- assert.match(none.stdout, new RegExp(`^ {3}no paper\\.md in ${empty}$`, "m"));
131
- const dir = writeTree(join(root, "nogit"), { "paper.md": P(["1. A", "x"]) });
132
- const r = runNode(SCRIPT, [dir, "--json", "--since=HEAD"], {
133
- env: { PATH: dirname(process.execPath) },
134
- });
135
- assert.deepEqual(JSON.parse(r.stdout), [
136
- {
137
- kind: "unresolvable-base",
138
- msg: "cannot read paper.md at `HEAD`: not a git repository. A round whose base does not resolve is a round with no gate, and it must not read as a clean run.",
139
- },
140
- ]);
141
- });
142
-
143
- test("a git that fails without a word still names the revision it could not read", () => {
144
- // A stand-in `git` first on PATH: it knows the repository root, and `show` exits 1 silently.
145
- const bin = writeTree(join(root, "quiet-git-bin"), {
146
- git: '#!/bin/sh\ncase "$1" in rev-parse) pwd ;; show) exit 1 ;; *) exit 0 ;; esac\n',
147
- });
148
- chmodSync(join(bin, "git"), 0o755);
149
- const dir = writeTree(join(root, "quiet"), { "paper.md": P(["1. A", "x"]) });
150
- const r = runNode(SCRIPT, [dir, "--json", "--since=abc"], {
151
- env: { PATH: `${bin}:${dirname(process.execPath)}` },
152
- });
153
- assert.deepEqual(JSON.parse(r.stdout), [
154
- {
155
- kind: "unresolvable-base",
156
- msg: "cannot read paper.md at `abc`: git show abc:paper.md failed. A round whose base does not resolve is a round with no gate, and it must not read as a clean run.",
157
- },
158
- ]);
159
- });
@@ -1,133 +0,0 @@
1
- // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2
-
3
- exports[`--section=1 keeps section 1 and its sub-sections, and nothing before it 1`] = `
4
- "
5
- === full.md — outline, in order ===
6
- own total share section
7
- 10 16 53.3% 1 Introduction
8
- ↳ [7/10 KEEP] carries: the claim
9
- 4 1.1 Detail
10
- ↳ carries: a sub-point
11
- 2 1.2 Bare sub
12
- ↳ 🔴 no carries: note — nobody has said why this section is here
13
-
14
- body total 30 words across 4 sections
15
-
16
- === outside the page limit ===
17
- 40 Limitations
18
- ↳ 🔴 no carries: note — and no page limit forcing the question
19
- 5 Scope
20
- ↳ carries: why the scope note
21
- 30 Appendix A
22
- ↳ carries: the raw tables
23
-
24
- free total 75 words = 250% of the body
25
- 🔴 The unbudgeted half is LARGER than the paper. Nothing prices these sections, which
26
- is exactly why prose settles here. Ask of each: would a reviewer miss it?
27
-
28
- The rest is judgement and stays with the skill: is this the ORDER a stranger needs, does each
29
- heading name a thing, and does each section earn its place. Read the headings as a set — a
30
- stranger should be able to reconstruct the argument from them alone.
31
- "
32
- `;
33
-
34
- exports[`the inventory of body-only.md 1`] = `
35
- "
36
- === body-only.md — outline, in order ===
37
- own total share section
38
- 8 8 66.7% 1 Intro
39
- ↳ 🔴 no carries: note — nobody has said why this section is here
40
- 4 4 33.3% 2 Rest
41
- ↳ 🔴 no carries: note — nobody has said why this section is here
42
- 0 0 0.0% 3 Empty
43
- ↳ 🔴 no carries: note — nobody has said why this section is here
44
-
45
- body total 12 words across 3 sections
46
-
47
- The rest is judgement and stays with the skill: is this the ORDER a stranger needs, does each
48
- heading name a thing, and does each section earn its place. Read the headings as a set — a
49
- stranger should be able to reconstruct the argument from them alone.
50
- "
51
- `;
52
-
53
- exports[`the inventory of full.md 1`] = `
54
- "
55
- === full.md — outline, in order ===
56
- own total share section
57
- 3 Orphan sub
58
- 10 16 53.3% 1 Introduction
59
- ↳ [7/10 KEEP] carries: the claim
60
- 4 1.1 Detail
61
- ↳ carries: a sub-point
62
- 2 1.2 Bare sub
63
- ↳ 🔴 no carries: note — nobody has said why this section is here
64
- 6 6 20.0% 2 Method
65
- ↳ 🔴 no carries: note — nobody has said why this section is here
66
- 5 5 16.7% 3 Results
67
- ↳ [?/10 CUT] carries: only a verdict
68
- 3 3 10.0% Discussion
69
- ↳ [4/10 NO VERDICT] carries: score only
70
-
71
- body total 30 words across 4 sections
72
-
73
- === outside the page limit ===
74
- 40 Limitations
75
- ↳ 🔴 no carries: note — and no page limit forcing the question
76
- 5 Scope
77
- ↳ carries: why the scope note
78
- 30 Appendix A
79
- ↳ carries: the raw tables
80
-
81
- free total 75 words = 250% of the body
82
- 🔴 The unbudgeted half is LARGER than the paper. Nothing prices these sections, which
83
- is exactly why prose settles here. Ask of each: would a reviewer miss it?
84
-
85
- The rest is judgement and stays with the skill: is this the ORDER a stranger needs, does each
86
- heading name a thing, and does each section earn its place. Read the headings as a set — a
87
- stranger should be able to reconstruct the argument from them alone.
88
- "
89
- `;
90
-
91
- exports[`the inventory of no-appendix.md 1`] = `
92
- "
93
- === no-appendix.md — outline, in order ===
94
- own total share section
95
- 8 8 100.0% 1 Intro
96
- ↳ 🔴 no carries: note — nobody has said why this section is here
97
-
98
- body total 8 words across 1 sections
99
-
100
- === outside the page limit ===
101
- 3 Limitations
102
- ↳ 🔴 no carries: note — and no page limit forcing the question
103
-
104
- free total 3 words = 38% of the body
105
-
106
- The rest is judgement and stays with the skill: is this the ORDER a stranger needs, does each
107
- heading name a thing, and does each section earn its place. Read the headings as a set — a
108
- stranger should be able to reconstruct the argument from them alone.
109
- "
110
- `;
111
-
112
- exports[`the inventory of refs-first.md 1`] = `
113
- "
114
- === refs-first.md — outline, in order ===
115
- own total share section
116
- 8 8 100.0% 1 Intro
117
- ↳ 🔴 no carries: note — nobody has said why this section is here
118
-
119
- body total 8 words across 1 sections
120
-
121
- === outside the page limit ===
122
- 2 References
123
- ↳ 🔴 no carries: note — and no page limit forcing the question
124
- 2 Ethical note
125
- ↳ 🔴 no carries: note — and no page limit forcing the question
126
-
127
- free total 4 words = 50% of the body
128
-
129
- The rest is judgement and stays with the skill: is this the ORDER a stranger needs, does each
130
- heading name a thing, and does each section earn its place. Read the headings as a set — a
131
- stranger should be able to reconstruct the argument from them alone.
132
- "
133
- `;
@@ -1,364 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * 🔴 2026-08-26 — ALL SIXTEEN CHECKS LEFT THIS FILE. THIS FILE IS NOW ONLY AN INVENTORY.
4
- *
5
- * Fourteen became `@eslint/markdown` rules in `eslint-rules/paper-structure.mjs`
6
- * (twelve rules — two of them have two `messageId`s each), two were deleted as duplicates of things
7
- * already moved: the share of the free half duplicated `paper/appendix-ratio`, and "a numbered
8
- * section without `carries:`" duplicated `paper/section-justification`, both from
9
- * `eslint-rules/paper-prose.mjs`.
10
- * Parity was proven BEFORE the removal: 29 inputs (a real paper + 22 "one defect at a time" fixtures
11
- * + 6 edge cases), 0 divergences, every number matched byte for byte. The analysis and the
12
- * classification table are in the author's private notes
13
- * (`papers/research/2026-08-26-klassifikatsiya-structure.md`).
14
- *
15
- * WHAT IS LEFT AND WHY IT IS NOT A CHECK. The inventory is a table "own / total / share / section"
16
- * with a `carries:` note under each row. It has no threshold and produces no findings: it is read by
17
- * a HUMAN in the `tighten-paper` skill, to compare a section's weight with what the section claims
18
- * about itself. That comparison was exactly the author's idea ("print the claim next to the
19
- * weight"), and a linter cannot express it — a finding has nowhere to put a whole table.
20
- *
21
- * ⚠️ `--flags-only` IS KEPT, BUT IT ALWAYS STAYS SILENT AND ALWAYS EXITS 0.
22
- *
23
- * ── 2026-08-26, EVENING: BOTH NAMED CONSUMERS ARE CLOSED, AND A THIRD ONE TURNED UP ────────
24
- *
25
- * The paragraph above named two callers and said that until they are converted, they read
26
- * "no findings" BY SILENCE. That was true and measured: on `the reference paper/paper.md`
27
- * `--flags-only` printed 0 lines, while the same bytes through ESLint give **6 findings**
28
- * (`subsection-size` ×2 · `section-lead` · `free-section-size` ×2 · `block-ungraded`).
29
- *
30
- * `.github/workflows/paper-gates.yml`, the `structure` step → REMOVED. An `eslint` step already
31
- * stood next to it, running the same rules over the same glob; a measurement showed a
32
- * byte-identical list of files. The full justification is in the comment where the removed
33
- * step used to be.
34
- *
35
- * 🔴 A THIRD CALLER THE BANNER DID NOT KNOW ABOUT: `.githooks/pre-commit`. It printed nothing and
36
- * swallowed the exit code through `|| true`. The lesson is exactly the one already recorded in
37
- * `CLAUDE.md` about the four forms of a reference: the banner listed the callers FROM MEMORY, while
38
- * only a grep for the flag's name gives the full list.
39
- *
40
- * ✅ CLOSED 2026-08-26. The call was converted to ESLint through `.githooks/structure-gate.mjs`
41
- * (the rule names are DERIVED from `eslint-rules/paper-structure.mjs`, not rewritten as a list),
42
- * the `|| true` was removed, and the exit codes were separated: 0 — it judged · 2 — it did NOT
43
- * judge, the commit is rejected · 3 — no `node_modules/.bin/eslint`, loudly and without blocking.
44
- * The test is `.githooks/structure-gate.harness.mjs`, five mutations, each one with proof that the
45
- * patch applied. Measured: on the same bytes the old call gave 0 findings and 0 bytes of output,
46
- * ESLint with the same rules — **6**.
47
- *
48
- * ⚠️ And the COMPLETENESS check of the list, which was missing last time: in `pre-commit` the idiom
49
- * `--flags-only … || true` occurs FOUR times. All of them were run, 26.08:
50
- * `prose-lint.mjs` — alive (2 findings, exit 1)
51
- * `artifact-coverage.mjs` — alive (6 findings, exit 1)
52
- * `population-map.mjs` — silent LEGITIMATELY (line 144: in this mode a clean run prints
53
- * nothing; on a real paper the registry adds up)
54
- * `structure.mjs` — was dead, closed above
55
- * A list without the third column is an enumeration, not a check.
56
- *
57
- * ⚠️ THE FLAG IS NOT DELETED AND MUST NOT BE DELETED WITHOUT EDITING OTHER PEOPLE'S FILES: its
58
- * silence is pinned by two asserts — `eslint-rules/paper-structure.harness.mjs` (≈550) and
59
- * `.claude/skills/paper-pipeline/scripts/gates.harness.mjs` (block 1). Both fail if this mode prints
60
- * something again OR exits with a non-zero code. Printing a "see eslint" pointer here is still
61
- * forbidden: `read: 'flags'` at any remaining consumer would turn it into a permanent false finding.
62
- */
63
- /**
64
- * structure.mjs — the mechanical leg of tighten-paper.
65
- *
66
- * node structure.mjs <paper.md> the whole paper
67
- * node structure.mjs <paper.md> --section=3 one section and its subsections
68
- * node structure.mjs <paper.md> --flags-only ⚠️ ALWAYS SILENT (see the banner above)
69
- *
70
- * WHY THIS EXISTS, and why it is NOT in prose-lint. Prose-lint judges sentences. Structure is a
71
- * different question and was homeless: `tighten-paper` is a skill — a model reading and judging —
72
- * with no script under it, so nothing mechanical ever looked at the document's SHAPE. Author,
73
- * 2026-08-05: *"I thought prose was about prose, and structure is something else entirely"*, and
74
- * before that *"the relevant skill needs to do the same thing for the whole paper by default, or for
75
- * one section"* (both said in Russian).
76
- *
77
- * 🔴 The failure that motivated it. Asked whether the contribution was short-changed, word counts
78
- * answered "no" — section 3 had 38% more words than related work — and that answer was true and
79
- * useless. The shape showed the real defect: three top-level sections owned 0, 28 and 38 words
80
- * before their first subsection. They were dividers wearing section numbers, and a reader met a
81
- * heading and fell straight through it into a subsection. No metric in the toolchain asked that
82
- * question, because every metric was per-sentence or per-document and none was per-SECTION.
83
- *
84
- * What this prints is an inventory first and findings second, deliberately. The judgement — is this
85
- * the right order, does this heading name a thing, does this section earn its place — stays with the
86
- * skill, because it is judgement. Only the mechanically decidable part is compiled.
87
- */
88
- import { readFileSync } from "node:fs";
89
- import {
90
- headings as mdHeadings,
91
- splitSections,
92
- stripFences,
93
- requireMarkdown,
94
- stripFrontmatter,
95
- } from "#lib/markdown";
96
-
97
- // Markup is parsed with a PARSER (`CLAUDE.md`, 2026-08-11). We fail rather than degrade: without a
98
- // parser not one section would be found in the paper, `--flags-only` would return 0 findings and
99
- // code 0 — that is, "the structure is fine" about a document nobody read. This file is precisely
100
- // about the fact that a metric without a threshold and a check without observability silently mean
101
- // nothing; a confident zero here would be the third instance of the same error in one file.
102
- requireMarkdown();
103
-
104
- // Headings of depth 2-3 are what this file calls a "section" and a "subsection". This used to be
105
- // written as `#{2,3}`, that is, as a statement about the NUMBER OF HASHES: `#{2,3}` counted any such
106
- // line as a section, including a line inside a ``` block. The papers in this repo quote other
107
- // people's markup in chunks, so "an extra section out of a quotation" is not a hypothesis.
108
- const SECTION_MIN = 2,
109
- SECTION_MAX = 3;
110
-
111
- // The thresholds (60 words of lead · 350 words of a subsection · 100% of the free half · 1.0
112
- // against the heaviest section) left together with the checks — they live as rule options in
113
- // `eslint-rules/paper-structure.mjs`, and the CALIBRATION of each (why exactly 60 and exactly 350,
114
- // and on which corpus it was measured) moved there as comments. They must not be kept here: a number
115
- // without a threshold is prose, and a threshold without a check is a second source of truth that
116
- // will drift away from the first.
117
-
118
- const args = process.argv.slice(2);
119
- const file = args.find((a) => !a.startsWith("--"));
120
- if (!file) {
121
- console.error(
122
- "usage: node structure.mjs <paper.md> [--section=N] (--flags-only is accepted and stays silent)",
123
- );
124
- process.exit(0);
125
- }
126
- const only =
127
- (args.find((a) => a.startsWith("--section=")) || "").split("=")[1] || null;
128
- const flagsOnly = args.includes("--flags-only");
129
-
130
- let t = readFileSync(file, "utf8");
131
- // `stripFences()` instead of `/^```[\s\S]*?^```/gm`: with an ODD number of fences that expression
132
- // glued the end of one block to the beginning of the next and cut out the prose between them — and
133
- // prose cut out here means an understated section weight, that is, a finding of "this is a divider,
134
- // not a section" on a section that is perfectly fine.
135
- // `blank: true` — the block's lines become empty rather than disappear: a code block is a paragraph
136
- // boundary, and removing the lines would glue neighbouring paragraphs together. Here it does not
137
- // affect the numbers (words and headings are counted the same either way), but the form must match
138
- // `prose-lint`, where it does.
139
- t = stripFences(
140
- stripFrontmatter(t) // frontmatter
141
- .replace(/<!--[\s\S]*?-->/g, ""), // working comments
142
- { blank: true },
143
- );
144
- // Body and FREE SECTIONS are measured separately. They hold different bars — the body has a page
145
- // limit and the free sections do not — but "does not count against the limit" is not "unmeasured".
146
- // 🔴 Until 2026-08-05 this script cut at `## Limitations` and never looked further, so every answer
147
- // it gave to "do all the sections survive?" silently excluded Limitations, Ethics and every
148
- // appendix. The author asked the question about those exact sections and the tool could not have
149
- // answered it. A free section is where unbudgeted prose accumulates precisely because nothing
150
- // prices it.
151
- // 2026-08-11: the three boundaries (free sections · bibliography · appendices) are found by the
152
- // parser. The `-1 / offset` convention and the `> 0`, `< 0` comparisons are kept as they were — the
153
- // previous expressions did not treat a heading on the very first line of the file as a boundary
154
- // either, and changing that here would mean introducing new behaviour under the guise of moving to a
155
- // parser.
156
- const headOffset = (re) => {
157
- const h = mdHeadings(t).find((x) => x.depth === 2 && re.test(x.text));
158
- return h ? h.offset : -1;
159
- };
160
- const cut = headOffset(/^(Limitations|References|Ethical)/u);
161
- const body = cut > 0 ? t.slice(0, cut) : t;
162
- const refs = headOffset(/^References/u);
163
- const appx = headOffset(/^Appendix/u);
164
- const freeText =
165
- cut > 0
166
- ? refs > cut
167
- ? t.slice(cut, refs) + t.slice(appx < 0 ? t.length : appx)
168
- : t.slice(cut)
169
- : "";
170
-
171
- const wordsOf = (s) => (s.match(/[A-Za-z0-9%.'’-]+/g) || []).length;
172
-
173
- // 🔴 `carries:` notes are read back OUT, not just written. The convention puts a working comment
174
- // above each heading saying what that section carries and whether it is justified — and until
175
- // 2026-08-05 nothing ever read one. They were write-only: authored during a tighten pass, then
176
- // invisible to every later pass, including the passes deciding whether a section earns its place.
177
- // Author's idea, and it is the right one: print the claim beside the weight, so "this section is
178
- // 564 words" and "this section claims to carry X" are read in the same glance.
179
- // 🔴 A justification with no SCALE and no VERDICT cannot decide anything, and for a week none of
180
- // these notes ever concluded "cut me". Author, 2026-08-06 (both quotes said in Russian): *"reading
181
- // the paper you get the impression — damn, is this section really needed"*, and *"every appendix
182
- // section must also have a comment justifying itself, with a score from 0 to 10 and a note on
183
- // whether it should be cut / moved / shortened"*.
184
- //
185
- // The defect is structural, not laziness. A free-text note is a section arguing its own case, and a
186
- // section always wins that argument — there is no scale to rank it against its neighbours and no
187
- // slot in which the answer can come out negative. So the note now carries three things:
188
- //
189
- // score: 0-10, how much a REVIEWER'S DECISION depends on this. Not how interesting it is.
190
- // verdict: KEEP | SHORTEN | MOVE | MERGE | CUT — what to DO, in a fixed vocabulary
191
- // carries: what it holds, as before
192
- //
193
- // The pairing is what makes it work: a low score with a KEEP verdict is a visible contradiction,
194
- // and the script reports it. Free text could never contradict itself.
195
- // The colon is optional, because the skill's own documented example writes `· score 7/10 ·` and the
196
- // first version of this regex demanded `score:`. The spec and the check disagreed, so the checker
197
- // reported "no score:" on notes that carried a perfectly good score — the same defect class this
198
- // paper is about, committed between a skill and its own compiled leg. Caught 2026-08-06 within
199
- // minutes, because the check was running.
200
- const NOTE_RE = /score:?\s*(\d{1,2})\s*(?:\/\s*10)?/i;
201
- const VERDICT_RE = /verdict:\s*(KEEP|SHORTEN|MOVE|MERGE|CUT)\b/i;
202
-
203
- const rawSections = stripFrontmatter(readFileSync(file, "utf8"));
204
- const carriesFor = new Map();
205
- const scoreFor = new Map();
206
- const verdictFor = new Map();
207
- // `(?:(?!-->)[\s\S])*?` and not `[\s\S]*?`: a lazy match will happily jump ACROSS a closing `-->`
208
- // when the comment it belongs to is not followed by a heading, swallowing the next comment whole and
209
- // attributing its note to the wrong section. Caught 2026-08-05 within a minute of the check existing,
210
- // which is the same reason the check exists.
211
- for (const m of rawSections.matchAll(
212
- /<!--((?:(?!-->)[\s\S])*?)-->\s*\n(#{2,3}) (.+)/g,
213
- )) {
214
- const note = m[1].replace(/\s+/g, " ");
215
- const c = note.match(
216
- /carries:\s*(.+?)(?:·|justified:|note:|costs:|verdict:|because:|-->|$)/i,
217
- );
218
- if (c) carriesFor.set(m[3].trim(), c[1].trim());
219
- const s = note.match(NOTE_RE);
220
- if (s) scoreFor.set(m[3].trim(), Number(s[1]));
221
- const v = note.match(VERDICT_RE);
222
- if (v) verdictFor.set(m[3].trim(), v[1].toUpperCase());
223
- }
224
-
225
- // The notes on the appendix's bold lead-ins (`score:`/`verdict:` above a `**Block.**`) are now read
226
- // by `paper/block-ungraded` · `paper/block-note` · `paper/block-verdict`. The inventory does not
227
- // print them, so they are not parsed here.
228
-
229
- const outline = [];
230
- for (const chunk of splitSections(body, {
231
- min: SECTION_MIN,
232
- max: SECTION_MAX,
233
- })) {
234
- if (!chunk.heading) continue; // the preamble before the first heading is not a section
235
- const title = chunk.heading.text;
236
- outline.push({
237
- // `chunk.body` is the chunk ALREADY without the heading line, so the previous
238
- // `chunk.replace(/^#{2,3} .+$/m, '')` is no longer needed: removing the heading line is part of
239
- // the splitting, not a separate operation over the text.
240
- level: chunk.heading.depth,
241
- title,
242
- words: wordsOf(chunk.body),
243
- carries: carriesFor.get(title) ?? null,
244
- });
245
- }
246
-
247
- // Own words + everything underneath, so a section's true weight is visible next to its lead.
248
- for (let i = 0; i < outline.length; i++) {
249
- let total = outline[i].words;
250
- if (outline[i].level === 2)
251
- for (let j = i + 1; j < outline.length && outline[j].level === 3; j++)
252
- total += outline[j].words;
253
- outline[i].total = total;
254
- }
255
-
256
- const inScope = (row, i) => {
257
- if (!only) return true;
258
- const num = (s) => (s.match(/^(\d+)/) || [])[1];
259
- if (row.level === 2) return num(row.title) === only;
260
- for (let j = i; j >= 0; j--)
261
- if (outline[j].level === 2) return num(outline[j].title) === only;
262
- return false;
263
- };
264
-
265
- const bodyTotal = outline
266
- .filter((r) => r.level === 2)
267
- .reduce((a, r) => a + r.total, 0);
268
-
269
- // The free sections are inventoried HERE, above the findings loop, and not down in the printing
270
- // block where they used to live. That placement was the whole defect: measured in a branch that
271
- // only the human-readable mode reaches, the ratio could never become a finding, so `--flags-only`
272
- // — the mode hooks and CI run — reported a clean document while the unpriced half stood at 165%.
273
- const freeSections = [];
274
- for (const chunk of splitSections(freeText || "", {
275
- min: SECTION_MIN,
276
- max: SECTION_MAX,
277
- })) {
278
- if (!chunk.heading) continue;
279
- freeSections.push({
280
- level: chunk.heading.depth,
281
- title: chunk.heading.text,
282
- words: wordsOf(chunk.body),
283
- });
284
- }
285
- const freeTotal = freeSections.reduce((a, r) => a + r.words, 0);
286
-
287
- // 🔴 THERE WERE SIXTEEN FINDINGS HERE — see the banner in the header. There is DELIBERATELY not a
288
- // single check left in this file: two sources of truth about one fact drift apart on the very first
289
- // edit, and there is nothing to settle the question of which one is right.
290
-
291
- if (flagsOnly) {
292
- // Silence and exit code 0 — ALWAYS. Deliberately not one line on stdout/stderr: a caller that
293
- // reads "non-empty output = a finding" would turn a pointer saying "the checks have moved" into a
294
- // permanent false finding. The limitation of this mode is named in the banner at the top.
295
- process.exit(0);
296
- }
297
-
298
- console.log(`\n=== ${file.split("/").pop()} — outline, in order ===`);
299
- console.log(
300
- `${"own".padStart(6)} ${"total".padStart(6)} ${"share".padStart(6)} section`,
301
- );
302
- for (const [i, r] of outline.entries()) {
303
- if (!inScope(r, i)) continue;
304
- const share =
305
- r.level === 2
306
- ? `${((100 * r.total) / bodyTotal).toFixed(1)}%`.padStart(6)
307
- : " ".repeat(6);
308
- console.log(
309
- `${String(r.words).padStart(6)} ${String(r.level === 2 ? r.total : "").padStart(6)} ${share} ` +
310
- `${r.level === 3 ? " " : ""}${r.title}`,
311
- );
312
- // The section's own claim about itself, printed where its weight is read. A section whose note
313
- // does not survive being read next to its size is a section to cut, and that comparison was
314
- // impossible to make until both appeared in one place.
315
- if (r.carries) {
316
- const s = scoreFor.get(r.title),
317
- v = verdictFor.get(r.title);
318
- const tag =
319
- s === undefined && !v ? "" : `[${s ?? "?"}/10 ${v ?? "NO VERDICT"}] `;
320
- console.log(
321
- `${" ".repeat(21)}${r.level === 3 ? " " : ""}↳ ${tag}carries: ${r.carries.slice(0, 80)}`,
322
- );
323
- } else if (/^\d/.test(r.title))
324
- console.log(
325
- `${" ".repeat(21)}${r.level === 3 ? " " : ""}↳ 🔴 no carries: note — nobody has said why this section is here`,
326
- );
327
- }
328
- console.log(
329
- `\nbody total ${bodyTotal} words across ${outline.filter((r) => r.level === 2).length} sections`,
330
- );
331
-
332
- // The free sections, measured against the body they hang off. A paper whose unbudgeted prose
333
- // outweighs its budgeted prose is telling you where its author was allowed to keep writing.
334
- if (freeSections.length) {
335
- console.log(`\n=== outside the page limit ===`);
336
- for (const f of freeSections) {
337
- console.log(
338
- `${String(f.words).padStart(6)} ${" ".repeat(14)}${f.level === 3 ? " " : ""}${f.title}`,
339
- );
340
- const c = carriesFor.get(f.title);
341
- if (c)
342
- console.log(
343
- `${" ".repeat(21)}${f.level === 3 ? " " : ""}↳ carries: ${c.slice(0, 96)}`,
344
- );
345
- else if (f.level === 2)
346
- console.log(
347
- `${" ".repeat(21)}↳ 🔴 no carries: note — and no page limit forcing the question`,
348
- );
349
- }
350
- console.log(
351
- `\nfree total ${freeTotal} words = ${((100 * freeTotal) / bodyTotal).toFixed(0)}% of the body`,
352
- );
353
- if (freeTotal > bodyTotal)
354
- console.log(
355
- `🔴 The unbudgeted half is LARGER than the paper. Nothing prices these sections, which\n` +
356
- ` is exactly why prose settles here. Ask of each: would a reviewer miss it?`,
357
- );
358
- }
359
-
360
- console.log(
361
- "\nThe rest is judgement and stays with the skill: is this the ORDER a stranger needs, does each\n" +
362
- "heading name a thing, and does each section earn its place. Read the headings as a set — a\n" +
363
- "stranger should be able to reconstruct the argument from them alone.",
364
- );