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,397 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * artifact-coverage.mjs — does the released bundle actually contain what the paper points at?
4
- *
5
- * node artifact-coverage.mjs <paper-dir> report
6
- * node artifact-coverage.mjs <paper-dir> --flags-only for hooks and pre-commit
7
- *
8
- * 🔴 WHY THIS EXISTS. On 2026-08-06, hours before a submission, the author asked a plain question —
9
- * "did we ever actually run the artifact?" — and the answer exposed that the released bundle held NO
10
- * DATA for the paper's §5.2: the agent experiment behind "stopped and asked a human in 34 of 48
11
- * gated runs". That result is in the ABSTRACT, it is the single reason an agent workshop is the
12
- * right venue for the paper, and the artifact reference promises "every data file behind the
13
- * numbers in this paper".
14
- *
15
- * Six `harden-paper` passes and five review panels had not caught it, and none of them was broken.
16
- * They were all asking a different question:
17
- *
18
- * the anonymity gate asks — does anything in the bundle leak an identity?
19
- * the numbers gate asks — does a printed number match its data file?
20
- * the artifact reviewer asks— does the code in the bundle run?
21
- * NOBODY asked — is the data for this experiment IN THE BUNDLE AT ALL?
22
- *
23
- * A missing directory passes every one of those: nothing leaks, nothing mismatches, and everything
24
- * present still runs. Absence is invisible to a checker that only inspects what is there.
25
- *
26
- * The two checks below are deliberately dumb, because dumb is what survives:
27
- * 1. every body section that REPORTS A NUMBER is named somewhere in the artifact's index;
28
- * 2. every path the index names EXISTS in the bundle.
29
- *
30
- * ── 2026-08-10: THE THIRD CHECK RUNS THE OTHER WAY ────────────────────────────────────────────
31
- *
32
- * 🔴 Both checks above run FORWARD, from the paper outward, and neither can see the opposite
33
- * failure: a result that EXISTS ON DISK and that the paper never mentions. Ported from
34
- * `check_artifact_coverage.py` in MedSci Skills (Nam, Jeong & Kim, arXiv:2606.09500v4, tag
35
- * v3.8.0), where it is a Major verdict named `DISK_UNREPORTED` and defined as
36
- *
37
- * "an analysis output that exists on disk … is never mentioned in the manuscript. The work was
38
- * done and run but its result, which may contradict the headline, is silently absent."
39
- *
40
- * That is a CHERRY-PICKING DETECTOR, and it is the one direction no other gate in this pipeline
41
- * covers. `check-anon` reads the bundle. `paper_numbers` reads printed quantities. `arm_permutation`
42
- * reads a quantity against its arm label. Every one of them starts from something the paper says.
43
- * An experiment that was run and quietly dropped says nothing, so nothing looks at it.
44
- *
45
- * ⚠️ WHAT WE PORTED IS THE MECHANISM, NOT THE PAPER'S HEADLINE. That paper's 27/27-versus-11/27
46
- * result does not survive its own artifact: seven of the 27 defects were in files never shown to
47
- * the model, and one of its eleven "hits" is a substring match on the word `random` in a sentence
48
- * about random forests. The honest number is 10/27, and 10/20 on what the model could see. Read
49
- * `the author's private research notes` §2.4 before citing
50
- * anything from it. The DETECTOR is a good idea regardless of how its evaluation was scored.
51
- *
52
- * 🔴 FALSE POSITIVES ARE THE WHOLE DESIGN PROBLEM, AND THE ANSWER IS AN IGNORE SET YOU CAN COUNT.
53
- * `repro/` is mostly things nobody should ever report: fixtures, fetch caches, working clones,
54
- * harness scratch, per-repository dumps. So this check needs an ignore set — and an ignore set that
55
- * is applied silently would turn a cherry-picking detector into the very thing it exists to catch.
56
- * So it is PRINTED IN FULL ON EVERY RUN, findings or none, exactly as `check_grandfather` in
57
- * `repro/paper_numbers.py` prints the quantities the numbers gate does not cover. Two halves:
58
- * — structural rules, in the code below, each one printed with the names it swallowed;
59
- * — `repro/unreported-grandfathered.txt`, one row per knowingly-unreported analysis, and rows of
60
- * the form `ships-as:<path>` are RE-VERIFIED against the bundle so that reason cannot rot.
61
- */
62
- import { readFileSync, existsSync, readdirSync, statSync } from "node:fs";
63
- import { join } from "node:path";
64
- import {
65
- headings as mdHeadings,
66
- splitSections,
67
- requireMarkdown,
68
- } from "#lib/markdown";
69
-
70
- // Markup is parsed with a parser (`CLAUDE.md`, 2026-08-11). We fail rather than degrade: without
71
- // the parser NOT A SINGLE section would be found in the paper, and the check "every section with a
72
- // number is named in the artifact index" would pass clean — that is exactly the failure this file
73
- // was written for (absence is invisible to a checker that looks only at what is present).
74
- requireMarkdown();
75
-
76
- const args = process.argv.slice(2);
77
- const dir = args.find((a) => !a.startsWith("--"));
78
- const flagsOnly = args.includes("--flags-only");
79
- if (!dir) {
80
- console.error("usage: node artifact-coverage.mjs <paper-dir> [--flags-only]");
81
- process.exit(0);
82
- }
83
-
84
- const paperPath = ["paper.md", "draft.md"]
85
- .map((f) => join(dir, f))
86
- .find(existsSync);
87
- // 🔴 A SILENT exit(0) here read as «clean» for four of five papers. Measured 2026-08-25: only
88
- // <paper-a> has a markdown source; agenticdev/aisec/<paper-e>/scored ship
89
- // `paper.tex`, so this checker printed NOTHING and returned 0 on the camera-ready — which has
90
- // an artifact (reproduce.py, data/) it was built to inspect. Nothing anywhere said so, and the
91
- // CI step mirrored the same condition, so both layers agreed to say nothing. A skip that cannot
92
- // be told apart from a pass is the failure this whole file exists to prevent.
93
- if (!paperPath) {
94
- console.error(
95
- `⏭️ artifact coverage SKIPPED for ${dir} — no paper.md/draft.md. This checker\n reads markdown only; a .tex paper is NOT covered by it. Absence of findings here is\n absence of checking, not a clean bill.`,
96
- );
97
- process.exit(0);
98
- }
99
-
100
- // The bundle is whatever directory the paper releases. `artifact-anon` is the anonymised one that
101
- // actually ships; a working `repro/` is NOT the release and must never be treated as one.
102
- const bundle = [join(dir, "repro/artifact-anon"), join(dir, "artifact")].find(
103
- existsSync,
104
- );
105
- if (!bundle) {
106
- console.error(
107
- `⏭️ artifact coverage SKIPPED for ${dir} — no repro/artifact-anon and no artifact/ directory.`,
108
- );
109
- process.exit(0);
110
- }
111
- const indexPath = ["NUMBERS.md", "README.md"]
112
- .map((f) => join(bundle, f))
113
- .find(existsSync);
114
- if (!indexPath) {
115
- const msg = `${bundle} has no NUMBERS.md and no README.md — nothing maps the paper's numbers to its data`;
116
- console.error(`📦 artifact — ${msg}`);
117
- process.exit(1);
118
- }
119
-
120
- const paper = readFileSync(paperPath, "utf8").replace(/<!--[\s\S]*?-->/g, "");
121
- const index = readFileSync(indexPath, "utf8");
122
- const findings = [];
123
-
124
- // ---- 1. every number-reporting section is represented in the index --------------------------
125
- // "Reports a number" means a BOLD span containing digits — the paper's own convention for a figure
126
- // it is asserting, as opposed to a section number or a citation. A section that merely mentions
127
- // §4.3 in passing does not trip this.
128
- // 2026-08-11: both the body boundary and the split into sections come from the parser. The former
129
- // `^## ` / `^### ` counted any line with hashes as a section, including hashes inside a ```-block:
130
- // a paper QUOTING somebody else's `## 4.2 …` in an example got an extra "section", and its bold
131
- // numbers got a requirement to be named in the artifact index. The `-1 / >>> 0` convention is kept:
132
- // the former expression did not treat a heading on the very first line of the file as a boundary
133
- // either.
134
- const freeH = mdHeadings(paper).find(
135
- (h) => h.depth === 2 && /^(Limitations|References|Ethical)/u.test(h.text),
136
- );
137
- const body = paper.slice(0, (freeH ? freeH.offset : -1) >>> 0 || paper.length);
138
- const SECTION_NUM = /^(\d+(?:\.\d+)?)\.?\s+(.+)$/u;
139
- const SUBSECTION_NUM = /^(\d+\.\d+)\s+(.+)$/u;
140
- for (const chunk of splitSections(body, { min: 2, max: 2 })) {
141
- const h = chunk.heading && SECTION_NUM.exec(chunk.heading.text);
142
- if (!h) continue;
143
- const subs = splitSections(chunk.raw, { min: 3, max: 3 });
144
- for (const subSec of subs) {
145
- // `subs.length === 1` means "there are no subsections at all", exactly what used to be checked
146
- // as `sub === chunk`: the numbers are then counted against the section itself. If subsections DO
147
- // exist, the section's leading chunk (before the first `###`) is not checked — as it was before.
148
- const sh = subSec.heading
149
- ? SUBSECTION_NUM.exec(subSec.heading.text)
150
- : subs.length === 1
151
- ? [null, h[1], h[2]]
152
- : null;
153
- if (!sh) continue;
154
- const sub = subSec.raw;
155
- const num = sh[1];
156
- // Bold spans with digits that are not section pointers or bracketed citations.
157
- const reported = (
158
- sub
159
- .replace(/§\s*\d+(\.\d+)*/g, "")
160
- .replace(/\[[\d,\s–-]+\]/g, "")
161
- .match(/\*\*[^*]*\d[^*]*\*\*/g) || []
162
- ).length;
163
- if (!reported) continue;
164
- const named = new RegExp(
165
- `§\\s*${num.replace(".", "\\.")}\\b|\\b${num.replace(".", "\\.")}\\b`,
166
- ).test(index);
167
- if (!named)
168
- findings.push(
169
- `§${num} reports ${reported} bolded figure(s) and is named NOWHERE in ` +
170
- `${indexPath.replace(dir + "/", "")} — the released bundle may hold no data for it`,
171
- );
172
- }
173
- }
174
-
175
- // ---- 2. every path the index names actually exists -------------------------------------------
176
- // Backtick-quoted things that look like a path into the bundle. A promise to a file that is not
177
- // there is worse than no promise: it reads as released.
178
- const seen = new Set();
179
- for (const m of index.matchAll(/`([A-Za-z0-9_./-]+\/[A-Za-z0-9_.*/-]+)`/g)) {
180
- let p = m[1];
181
- if (seen.has(p) || p.includes("*") || p.startsWith("http")) continue;
182
- seen.add(p);
183
- // A paper about repositories names a LOT of `owner/repo`, and those are not paths. Require a file
184
- // extension: it is the one signal that separates `census/rows.tsv` from `saleor/apps`. Caught on
185
- // the first run, when three GitHub repositories were reported as missing directories.
186
- if (!/\.[a-z0-9]{1,5}$/i.test(p)) continue;
187
- // `repro/` is the WORKING tree, deliberately not in the anonymised bundle — the whole reason the
188
- // bundle exists separately is that repro holds real names in fixtures.
189
- if (p.startsWith("repro/")) continue;
190
- // A directory prefix is enough: the index names files inside runs that may be regenerated.
191
- const head = p.split("/")[0];
192
- if (!existsSync(join(bundle, head)) && !readdirSync(bundle).includes(head))
193
- findings.push(
194
- `the index names \`${p}\` and \`${head}\` is not in the bundle`,
195
- );
196
- }
197
-
198
- // ---- 3. reverse: a result on disk that neither the paper nor the index mentions ---------------
199
- // The unit is an EXPERIMENT DIRECTORY under `repro/` that holds a headline write-up — this
200
- // project's own convention, stated in `repro/README.md`: "you go into the folder, and there is a
201
- // RESULTS.md with the full write-up". A directory with a RESULTS.md is a run somebody finished and
202
- // wrote up.
203
- //
204
- // "Mentioned" is: the directory's name, or its name with a trailing -YYYY-MM-DD stripped, appears
205
- // in the paper OR in the released index. Matching is LITERAL on purpose. Prefix or fuzzy matching
206
- // would quietly absolve `gate-effect-spike-2026-08-05` on the strength of the shipped
207
- // `gate-effect/`, and a false NEGATIVE in a cherry-picking detector is the failure that costs
208
- // something. A rename is therefore a row in the ignore file, where it is visible and re-verified,
209
- // rather than an inference nobody can see.
210
- //
211
- // 🔴 THIS LEG READS EVERY INDEX FILE, NOT JUST THE FIRST. Check 1 above resolves `indexPath` to
212
- // NUMBERS.md *or* README.md and stops, which is right for it: it asks whether the map from numbers
213
- // to data exists. Reused here it produced three false positives on the first run —
214
- // `enforcement-surface`, `ladder-prototype` and `tier-a-prime` are all named in the bundle's
215
- // README.md and in no other file, so a reverse check reading only NUMBERS.md accused the bundle of
216
- // hiding three analyses it ships and documents. A cherry-picking detector that cries wolf gets
217
- // muted, and a muted detector is how the failure happens twice.
218
- const ledger = [];
219
- const HEADLINE = /^(RESULTS|SUMMARY|FINDINGS|METRICS)\.(md|json|tsv|csv|txt)$/i;
220
- const repro = join(dir, "repro");
221
- const bundleName = bundle
222
- .slice(dir.length + 1)
223
- .split("/")
224
- .pop();
225
- const indexAll = ["NUMBERS.md", "README.md"]
226
- .map((f) => join(bundle, f))
227
- .filter(existsSync)
228
- .map((f) => readFileSync(f, "utf8"))
229
- .join("\n");
230
-
231
- if (!existsSync(repro)) {
232
- ledger.push(
233
- " reverse check (results on disk nobody reports): SKIPPED — no repro/ directory",
234
- );
235
- } else {
236
- // Structural ignores. Each is a RULE, and each prints what it swallowed.
237
- const swallowed = new Map([
238
- [
239
- `the released bundle itself (${bundleName}/) — it is the shipped copy of these same analyses, ` +
240
- "so counting it would report every one of them a second time",
241
- [],
242
- ],
243
- [
244
- "dot-directories and build/dependency trees (node_modules, __pycache__)",
245
- [],
246
- ],
247
- ]);
248
- const allowPath = join(repro, "unreported-grandfathered.txt");
249
- const allowed = new Map();
250
- if (existsSync(allowPath)) {
251
- for (const line of readFileSync(allowPath, "utf8").split("\n")) {
252
- const row = line.split("#")[0].trimEnd();
253
- if (!row.trim()) continue;
254
- const [name, reason, ...rest] = row.split("\t").map((c) => c.trim());
255
- if (name)
256
- allowed.set(
257
- name,
258
- [reason, ...rest].filter(Boolean).join(" — ") || "(NO REASON GIVEN)",
259
- );
260
- }
261
- }
262
-
263
- const candidates = [];
264
- for (const name of readdirSync(repro)) {
265
- const p = join(repro, name);
266
- if (!statSync(p).isDirectory()) continue;
267
- if (name === bundleName) {
268
- swallowed.get([...swallowed.keys()][0]).push(name);
269
- continue;
270
- }
271
- if (
272
- name.startsWith(".") ||
273
- name === "node_modules" ||
274
- name === "__pycache__"
275
- ) {
276
- swallowed.get([...swallowed.keys()][1]).push(name);
277
- continue;
278
- }
279
- const heads = readdirSync(p).filter((f) => HEADLINE.test(f));
280
- if (heads.length) candidates.push({ name, heads });
281
- }
282
-
283
- // 🔴 WHOLE-TOKEN, AND NEVER ON A SHORT SLUG. `includes()` was the first version and it acquitted
284
- // a directory called `a-2026-01-01` on the strength of the letter "a" appearing in the prose —
285
- // caught by the harness case that asserts the stated finding count. Substring matching in a
286
- // detector like this fails in the dangerous direction: it invents evidence that a result was
287
- // mentioned. Hyphens count as part of the token, so `gate-effect` does not match inside
288
- // `gate-effect-spike`, and a slug shorter than four characters is not used for matching at all.
289
- const esc = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
290
- const mentions = (t, s) =>
291
- s.length >= 4 && new RegExp(`(?<![\\w-])${esc(s)}(?![\\w-])`).test(t);
292
-
293
- const reported = [],
294
- ignored = [],
295
- unreported = [];
296
- for (const c of candidates) {
297
- const slug = c.name.replace(/-\d{4}-\d{2}-\d{2}$/, "");
298
- const named = (t) => mentions(t, c.name) || mentions(t, slug);
299
- if (named(paper)) {
300
- reported.push(`${c.name} (named in the paper)`);
301
- continue;
302
- }
303
- if (named(indexAll)) {
304
- reported.push(`${c.name} (named in the index)`);
305
- continue;
306
- }
307
- if (allowed.has(c.name)) {
308
- ignored.push([c.name, allowed.get(c.name)]);
309
- continue;
310
- }
311
- unreported.push(c);
312
- }
313
-
314
- // A `ships-as:` reason carries a witness, so it is checked rather than believed. Everything else
315
- // is prose and is reprinted verbatim for a human to disbelieve.
316
- const stale = [];
317
- for (const [name, reason] of ignored) {
318
- const m = /^ships-as:(\S+)/.exec(reason);
319
- if (m && !existsSync(join(bundle, m[1])))
320
- stale.push(
321
- `the ignore row for \`${name}\` claims it ships as \`${m[1]}\`, and that path is ` +
322
- "not in the bundle — the allowance has rotted and is now hiding a real absence",
323
- );
324
- }
325
- for (const name of allowed.keys())
326
- if (!candidates.some((c) => c.name === name))
327
- stale.push(
328
- `the ignore row for \`${name}\` names no directory under repro/ that holds a ` +
329
- "headline result — retire the row rather than leaving it to grow stale",
330
- );
331
- findings.push(...stale);
332
-
333
- for (const c of unreported)
334
- findings.push(
335
- `DISK_UNREPORTED repro/${c.name}/ holds ${c.heads.join(", ")} and is named neither in the ` +
336
- "paper nor in the released index — an analysis that ran and whose result a reader cannot reach",
337
- );
338
-
339
- ledger.push(
340
- "",
341
- ` ── what the reverse check ignored, in full (${candidates.length} candidate analyses under repro/) ──`,
342
- " An ignore set applied in silence would make this a cherry-picking detector that cherry-picks,",
343
- " so it is printed whether or not anything was found. Same discipline as check_grandfather in",
344
- " repro/paper_numbers.py: the uncovered set is a list you can count, not an absence of warnings.",
345
- );
346
- for (const [rule, names] of swallowed)
347
- ledger.push(
348
- ` • ${names.length} before they were counted — ${rule}`,
349
- ...(names.length ? [` ${names.join(", ")}`] : []),
350
- );
351
- ledger.push(
352
- ` • ${reported.length} reported: ${reported.join(" · ") || "(none)"}`,
353
- );
354
- ledger.push(
355
- ` • ${ignored.length} in repro/unreported-grandfathered.txt — every row, every reason:`,
356
- );
357
- for (const [name, reason] of ignored)
358
- ledger.push(` ${name} → ${reason}`);
359
- if (!ignored.length)
360
- ledger.push(" (the file is empty — nothing is being waved through)");
361
- ledger.push(
362
- ` • ${unreported.length} left over, and those are the findings above.`,
363
- ` Ratio: ${unreported.length} reported as unreported, ${ignored.length} deliberately ignored, ` +
364
- `${reported.length} reachable. If the ignored column ever dwarfs the other two, this check has`,
365
- " become a formality and the honest move is to say so rather than to keep running it.",
366
- "",
367
- " 🔴 WHAT THIS LEG IS SILENT ABOUT, stated so nobody reads its silence as coverage:",
368
- " — an analysis SHIPPED in the bundle but never discussed in prose is counted as reported here,",
369
- " because the index is treated as the paper's delegate. MedSci's DISK_UNREPORTED is scoped to",
370
- " the manuscript alone and would flag those; ours would not.",
371
- " — a result that was never written up (no RESULTS.md) is invisible. Deleting the write-up is a",
372
- " one-command way past this check, and nothing here can tell that from an experiment nobody",
373
- " finished.",
374
- ' — nothing reads the CONTENT of a result, so "mentioned" and "reported honestly" are different',
375
- " things and only the first is checked.",
376
- );
377
- }
378
-
379
- const header = `📦 artifact coverage — ${findings.length} finding(s) in ${dir}`;
380
- if (findings.length) {
381
- const out = flagsOnly ? console.error : console.log;
382
- out(header);
383
- findings.forEach((f) => out(" " + f));
384
- out(
385
- " absence is invisible to the leak gate, the numbers gate and the artifact runner:",
386
- );
387
- out(" nothing leaks, nothing mismatches, and what IS there still runs.");
388
- ledger.forEach((l) => out(l));
389
- process.exit(1);
390
- }
391
- if (!flagsOnly) {
392
- console.log(header);
393
- console.log(
394
- " every number-reporting section is indexed, every named path exists",
395
- );
396
- ledger.forEach((l) => console.log(l));
397
- }
@@ -1,86 +0,0 @@
1
- /**
2
- * artifact-coverage.mjs on the inputs its harness does not build: no argument, no markdown paper,
3
- * a free-section heading on the paper's very first line, a numbered section without subsections,
4
- * an index naming the same path twice / a glob / a URL / a repro path, no repro/ at all, a
5
- * grandfather row without a reason, and a plain file among the repro/ entries.
6
- */
7
- import assert from "node:assert/strict";
8
- import { dirname, join } from "node:path";
9
- import { fileURLToPath } from "node:url";
10
- import { test } from "vitest";
11
- import { runNode, useTempDir, writeTree } from "../../../test/support.ts";
12
-
13
- const SCRIPT = join(
14
- dirname(fileURLToPath(import.meta.url)),
15
- "artifact-coverage.mjs",
16
- );
17
- const root = useTempDir("artifact-coverage-");
18
- const run = (...args: string[]) => runNode(SCRIPT, args);
19
-
20
- test("no argument prints the usage; a paper with no markdown source is a named skip", () => {
21
- assert.deepEqual(run(), {
22
- status: 0,
23
- stdout: "",
24
- stderr: "usage: node artifact-coverage.mjs <paper-dir> [--flags-only]\n",
25
- });
26
- const tex = writeTree(join(root, "tex"), { "paper.tex": "x" });
27
- const r = run(tex);
28
- assert.equal(r.status, 0);
29
- assert.match(
30
- r.stderr,
31
- /artifact coverage SKIPPED for .* — no paper\.md\/draft\.md/,
32
- );
33
- });
34
-
35
- test("a section without subsections is checked against its own number; the index's odd paths are passed over", () => {
36
- const dir = writeTree(join(root, "odd"), {
37
- "paper.md": "## 3. Results\n\nWe measured **42 %** of runs.\n",
38
- "artifact/README.md": [
39
- "Section 7 only.",
40
- "`data/rows.tsv` and again `data/rows.tsv`",
41
- "`runs/*/out.json`",
42
- "`https://example.org/x.json`",
43
- "`repro/work/tmp.json`",
44
- ].join("\n"),
45
- });
46
- const r = run(dir);
47
- assert.match(
48
- r.stdout,
49
- /§3 reports 1 bolded figure\(s\) and is named NOWHERE/,
50
- );
51
- assert.match(
52
- r.stdout,
53
- /the index names `data\/rows\.tsv` and `data` is not in the bundle/,
54
- );
55
- assert.equal(r.stdout.match(/rows\.tsv/g)?.length, 1);
56
- assert.doesNotMatch(r.stdout, /out\.json|example\.org|tmp\.json/);
57
- assert.match(
58
- r.stdout,
59
- /reverse check \(results on disk nobody reports\): SKIPPED — no repro\/ directory/,
60
- );
61
- });
62
-
63
- test("a free-section heading on the first line does not end the body at offset 0", () => {
64
- const dir = writeTree(join(root, "first-line"), {
65
- "paper.md": "## Limitations\n\nNone.\n\n## 2. Method\n\nIt took **9 s**.\n",
66
- "artifact/README.md": "nothing\n",
67
- });
68
- assert.match(run(dir).stdout, /§2 reports 1 bolded figure\(s\)/);
69
- });
70
-
71
- test("repro/: a plain file is not a candidate, a dot-directory is swallowed and named; a grandfather row with no reason says so", () => {
72
- const dir = writeTree(join(root, "grandfathered"), {
73
- "paper.md": "## 1. Intro\n\nText.\n",
74
- "artifact/README.md": "§1\n",
75
- "repro/notes.txt": "a file, not an analysis",
76
- "repro/.cache/RESULTS.md": "a dot-directory, swallowed before counting",
77
- "repro/old-run/RESULTS.md": "done\n",
78
- "repro/unreported-grandfathered.txt": "old-run\n",
79
- });
80
- const out = run(dir).stdout;
81
- assert.match(out, / {7}old-run {2}→ {2}\(NO REASON GIVEN\)/);
82
- assert.match(
83
- out,
84
- /• 1 before they were counted — dot-directories and build\/dependency trees[^\n]*\n {7}\.cache/,
85
- );
86
- });
@@ -1,184 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * check-provenance — does the paper have a build-time numbers gate, and what does it still NOT
4
- * cover.
5
- *
6
- * 🔴 THREE OF THE FIVE CHECKS MOVED INTO ESLint RULES ON 2026-08-26
7
- * (`eslint-rules/paper-registry.mjs`):
8
- *
9
- * arm-mismatch → `paper/number-arm-mismatch`
10
- * untraced-number → `paper/number-untraced`
11
- * arm-unlabelled → `paper/number-arm-unlabelled`
12
- *
13
- * All three spoke about a SENTENCE IN THE PAPER, while the provenance files (`NOTES.md`,
14
- * `CLAIMS.md`, `repro/artifact-anon/NUMBERS.md`, `NUMBER-ARMS.tsv`) were CONFIGURATION for them —
15
- * what sets which sentence counts as defective. Exactly like a venue profile for `pdf/profile`.
16
- * Parity was proved BEFORE the deletion: the real paper (5 `arm-mismatch` + 5 `untraced` numbers,
17
- * the sets matched) plus 17 fixtures. The write-up is in
18
- * `the author's private research notes`.
19
- *
20
- * WHAT IS LEFT HERE, AND WHY THAT IS NOT LAZINESS. The two remaining checks **do not read the body
21
- * of the paper at all** — all they are given is the directory:
22
- *
23
- * no-numbers-gate the paper has no `repro/paper_numbers.py`, that is, every printed quantity
24
- * here is a hand-typed digit whose only link to its data file is somebody's
25
- * memory.
26
- * numbers-coverage how many quantities are sourced and how many are still raw (the
27
- * `numbers-grandfathered.txt` ratchet). This is NOT A FINDING AT ALL, it is a
28
- * coverage measurement — ESLint has no "for information" severity, and throwing
29
- * it away would mean losing the only line that answers "does the gate cover what
30
- * a reviewer will check?".
31
- *
32
- * A check that does not read the file being linted cannot be a rule about it — that discriminator
33
- * decided both.
34
- *
35
- * ═══ THE FAILURE STORY that made this file exist at all (kept verbatim) ═══
36
- * 2026-08-05, 29 hours before the deadline. The paper's headline read "147 real rules files exactly
37
- * as their projects committed them, 22 fail admission". In the artifact's aggregate
38
- * `.all.annotated.failedContradicted = 22` — on the files AS COMMITTED nothing fails. 22 is the
39
- * counterfactual arm, where the extractor's guesses are written in as if the author had written
40
- * them.
41
- *
42
- * Not one check in the pipeline saw this, and the reason is structural: each looks at ONE side.
43
- * verify-citations checks that a reference is real. The claims-preservation diff checks that a
44
- * claim did not grow (and it did not grow, 22 was always 22). The artifact reviewer recomputes the
45
- * number from the data, and it agrees, because the number is CORRECT. The panel reads the prose and
46
- * takes "unmodified" as a fact. The defect lives only in the JOINT between the sentence and the
47
- * path to the field, and nobody held both sides at once. `paper/number-arm-mismatch` holds them
48
- * now.
49
- *
50
- * Advisory. Exit 0 always; findings on stdout.
51
- */
52
- import { readFileSync, existsSync } from "node:fs";
53
- import { join, resolve } from "node:path";
54
- import { isMain } from "./consumer.mjs";
55
-
56
- // 🔴 THERE IS NO `markdown.mjs` IMPORT ANY MORE, AND THAT IS A CONSEQUENCE OF THE MOVE, NOT A
57
- // SIMPLIFICATION. There is nothing left here to parse markup with and no reason to: both remaining
58
- // checks look at the DIRECTORY, not at the text. The bibliography boundaries (`## References`) that
59
- // `requireMarkdown()` stood for are now needed by the rules in `eslint-rules/paper-registry.mjs`,
60
- // and there ESLint itself provides them.
61
-
62
- /**
63
- * Has this paper adopted the build-time numbers gate, and how far?
64
- *
65
- * SUPERSEDED, 2026-08-05, same day it was written. The registry below (`NUMBER-ARMS.tsv`) keyed its
66
- * rules on the NUMBER — "wherever 22 appears, one of these phrases must be near it". That is wrong
67
- * in a way that shows up immediately in a real paper: `<paper-a>` prints two unrelated 22s
68
- * (22 files that fail admission in the counterfactual arm, and 8 of 22 abstentions in a paired
69
- * sample), and a number-keyed rule cannot tell them apart. It fires on the innocent one and, worse,
70
- * would go on passing if the guilty one were reworded.
71
- *
72
- * The replacement keys on the OCCURRENCE: the paper writes `{{annotated.failedContradicted}}`, not
73
- * `22`, so each printed quantity carries its own identity and its own guard, and the build refuses
74
- * a PDF where the value has gone stale or the surrounding prose claims the wrong arm. See
75
- * `<paper>/repro/paper_numbers.py`. That gate is per-paper because the data layout is per-paper;
76
- * what belongs HERE is the generic question: does this paper have one, and what does it still leave
77
- * uncovered?
78
- *
79
- * ⚠️ Reading `NUMBER-ARMS.tsv` (the `arm-unlabelled` finding) moved on 2026-08-26 into the rule
80
- * `paper/number-arm-unlabelled`; the registry still works for a paper that has one.
81
- */
82
- function checkNumbersGate(dir) {
83
- const out = [];
84
- const gate = join(dir, "repro", "paper_numbers.py");
85
- const registry = join(dir, "repro", "numbers.tsv");
86
- if (!existsSync(gate)) {
87
- out.push({
88
- kind: "no-numbers-gate",
89
- msg:
90
- "no repro/paper_numbers.py — every printed figure here is a hand-typed digit whose only " +
91
- "link to its data file is somebody's memory. Copy the gate from a paper that already has one; the " +
92
- "defect it catches (right number, wrong experimental arm) is invisible to every other check",
93
- });
94
- return out;
95
- }
96
- if (!existsSync(registry)) return out;
97
- const rows = readFileSync(registry, "utf8")
98
- .split("\n")
99
- .filter((l) => l.trim() && !l.startsWith("#")).length;
100
- const allow = join(dir, "repro", "numbers-grandfathered.txt");
101
- const raw = existsSync(allow)
102
- ? readFileSync(allow, "utf8")
103
- .split("\n")
104
- .filter((l) => l.split("#")[0].trim()).length
105
- : 0;
106
- // Not a finding — a fact the scorecard should carry, because "we have a gate" and "the gate
107
- // covers the numbers a reviewer will check" are different claims and only the second matters.
108
- out.push({
109
- kind: "numbers-coverage",
110
- msg:
111
- `${rows} quantities are sourced and guarded, ${raw} are still raw and grandfathered — ` +
112
- `the grandfathered ones are an admission, not a clearance`,
113
- });
114
- return out;
115
- }
116
-
117
- /**
118
- * Why the guards were pulled out INTO A SEPARATE function instead of being left as early
119
- * `return`s inside `checkProvenance` — measured 2026-08-26.
120
- *
121
- * 🔴 The script printed `🔢 check-provenance — clean` for `<paper-b>` and `<paper-c>` without
122
- * checking anything at all: both papers are in LaTeX, they have no `paper.md`, `find(existsSync)`
123
- * returned `undefined`, and the function exited with an empty array. That is, **"you have no
124
- * numbers gate" was muted on exactly those papers that do not have one** — `repro/paper_numbers.py`
125
- * exists only in `<paper-a>`. A confident green on three papers out of four.
126
- *
127
- * A skip indistinguishable from a pass is the same failure as "0 checks" versus "checks passed" in
128
- * the PR interface. The neighbours in this directory (`artifact-coverage.mjs`,
129
- * `population-map.mjs`) have long printed `⏭️ SKIPPED … absence of findings here is absence of
130
- * checking, not a clean bill`; this file was the only one that stayed silent.
131
- *
132
- * The guard is ONE for both consumers (the findings list and the CLI output), so that the reason
133
- * for the skip cannot drift apart from the skip itself.
134
- */
135
- export function provenanceScope(dir) {
136
- const paper = ["paper.md", "draft.md"]
137
- .map((f) => join(dir, f))
138
- .find(existsSync);
139
- if (!paper) {
140
- return {
141
- covered: false,
142
- reason:
143
- "no paper.md/draft.md — this checker reads markdown only, a .tex paper is NOT covered",
144
- };
145
- }
146
- const prov = [
147
- "NOTES.md",
148
- "CLAIMS.md",
149
- join("repro", "artifact-anon", "NUMBERS.md"),
150
- ]
151
- .map((f) => join(dir, f))
152
- .filter(existsSync)
153
- .map((f) => readFileSync(f, "utf8"))
154
- .join("\n");
155
- if (!prov) {
156
- return {
157
- covered: false,
158
- reason:
159
- "no non-empty NOTES.md / CLAIMS.md / repro/artifact-anon/NUMBERS.md",
160
- };
161
- }
162
- return { covered: true, reason: null };
163
- }
164
-
165
- export function checkProvenance(dir) {
166
- if (!provenanceScope(dir).covered) return [];
167
- return [...checkNumbersGate(dir)];
168
- }
169
-
170
- if (isMain(import.meta.url)) {
171
- const dir = resolve(process.argv[2] ?? ".");
172
- const f = checkProvenance(dir);
173
- if (f.length) {
174
- console.log(`🔢 check-provenance — ${f.length} finding(s):`);
175
- for (const x of f) console.log(` [${x.kind}] ${x.msg}`);
176
- } else {
177
- // A skip and a pass print as DIFFERENT lines — see the comment on `provenanceScope`.
178
- const scope = provenanceScope(dir);
179
- if (scope.covered) console.log("🔢 check-provenance — clean");
180
- else
181
- console.log(`⏭️ check-provenance SKIPPED for ${dir} — ${scope.reason}`);
182
- }
183
- process.exit(0);
184
- }