paperlint 2.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 (762) hide show
  1. package/.github/dependabot.yml +72 -0
  2. package/.github/workflows/ci.yml +297 -0
  3. package/.github/workflows/dependabot-automerge.yml +70 -0
  4. package/.github/workflows/pr-title.yml +59 -0
  5. package/.github/workflows/release.yml +54 -0
  6. package/CLAUDE.md +598 -0
  7. package/CONTRIBUTING.md +159 -0
  8. package/LICENSE +21 -0
  9. package/README.md +240 -0
  10. package/action.harness.mjs +287 -0
  11. package/action.mutations.mjs +162 -0
  12. package/action.yml +138 -0
  13. package/bin/rpp.mjs +43 -0
  14. package/dist/action-ref.d.ts +12 -0
  15. package/dist/action-ref.d.ts.map +1 -0
  16. package/dist/action-ref.js +16 -0
  17. package/dist/action-ref.js.map +1 -0
  18. package/dist/adapters/banal/failure.d.ts +73 -0
  19. package/dist/adapters/banal/failure.d.ts.map +1 -0
  20. package/dist/adapters/banal/failure.js +58 -0
  21. package/dist/adapters/banal/failure.js.map +1 -0
  22. package/dist/adapters/banal/index.d.ts +17 -0
  23. package/dist/adapters/banal/index.d.ts.map +1 -0
  24. package/dist/adapters/banal/index.js +56 -0
  25. package/dist/adapters/banal/index.js.map +1 -0
  26. package/dist/adapters/banal/install.d.ts +26 -0
  27. package/dist/adapters/banal/install.d.ts.map +1 -0
  28. package/dist/adapters/banal/install.js +15 -0
  29. package/dist/adapters/banal/install.js.map +1 -0
  30. package/dist/adapters/banal/invocation.d.ts +48 -0
  31. package/dist/adapters/banal/invocation.d.ts.map +1 -0
  32. package/dist/adapters/banal/invocation.js +43 -0
  33. package/dist/adapters/banal/invocation.js.map +1 -0
  34. package/dist/adapters/banal/locate.d.ts +50 -0
  35. package/dist/adapters/banal/locate.d.ts.map +1 -0
  36. package/dist/adapters/banal/locate.js +34 -0
  37. package/dist/adapters/banal/locate.js.map +1 -0
  38. package/dist/adapters/banal/output.d.ts +27 -0
  39. package/dist/adapters/banal/output.d.ts.map +1 -0
  40. package/dist/adapters/banal/output.js +112 -0
  41. package/dist/adapters/banal/output.js.map +1 -0
  42. package/dist/adapters/banal/pin.d.ts +19 -0
  43. package/dist/adapters/banal/pin.d.ts.map +1 -0
  44. package/dist/adapters/banal/pin.js +15 -0
  45. package/dist/adapters/banal/pin.js.map +1 -0
  46. package/dist/adapters/banal/probe.d.ts +12 -0
  47. package/dist/adapters/banal/probe.d.ts.map +1 -0
  48. package/dist/adapters/banal/probe.js +27 -0
  49. package/dist/adapters/banal/probe.js.map +1 -0
  50. package/dist/adapters/banal/run.d.ts +89 -0
  51. package/dist/adapters/banal/run.d.ts.map +1 -0
  52. package/dist/adapters/banal/run.js +104 -0
  53. package/dist/adapters/banal/run.js.map +1 -0
  54. package/dist/adapters/banal/settings.d.ts +18 -0
  55. package/dist/adapters/banal/settings.d.ts.map +1 -0
  56. package/dist/adapters/banal/settings.js +29 -0
  57. package/dist/adapters/banal/settings.js.map +1 -0
  58. package/dist/adapters/banal/xml.d.ts +48 -0
  59. package/dist/adapters/banal/xml.d.ts.map +1 -0
  60. package/dist/adapters/banal/xml.js +67 -0
  61. package/dist/adapters/banal/xml.js.map +1 -0
  62. package/dist/adapters/curl/download.io.d.ts +14 -0
  63. package/dist/adapters/curl/download.io.d.ts.map +1 -0
  64. package/dist/adapters/curl/download.io.js +69 -0
  65. package/dist/adapters/curl/download.io.js.map +1 -0
  66. package/dist/adapters/curl/index.d.ts +6 -0
  67. package/dist/adapters/curl/index.d.ts.map +1 -0
  68. package/dist/adapters/curl/index.js +6 -0
  69. package/dist/adapters/curl/index.js.map +1 -0
  70. package/dist/adapters/memory/index.d.ts +43 -0
  71. package/dist/adapters/memory/index.d.ts.map +1 -0
  72. package/dist/adapters/memory/index.js +79 -0
  73. package/dist/adapters/memory/index.js.map +1 -0
  74. package/dist/adapters/node/files.io.d.ts +3 -0
  75. package/dist/adapters/node/files.io.d.ts.map +1 -0
  76. package/dist/adapters/node/files.io.js +31 -0
  77. package/dist/adapters/node/files.io.js.map +1 -0
  78. package/dist/adapters/node/host.io.d.ts +3 -0
  79. package/dist/adapters/node/host.io.d.ts.map +1 -0
  80. package/dist/adapters/node/host.io.js +14 -0
  81. package/dist/adapters/node/host.io.js.map +1 -0
  82. package/dist/adapters/node/index.d.ts +25 -0
  83. package/dist/adapters/node/index.d.ts.map +1 -0
  84. package/dist/adapters/node/index.js +14 -0
  85. package/dist/adapters/node/index.js.map +1 -0
  86. package/dist/adapters/node/process.io.d.ts +14 -0
  87. package/dist/adapters/node/process.io.d.ts.map +1 -0
  88. package/dist/adapters/node/process.io.js +41 -0
  89. package/dist/adapters/node/process.io.js.map +1 -0
  90. package/dist/adapters/node/workspace.io.d.ts +4 -0
  91. package/dist/adapters/node/workspace.io.d.ts.map +1 -0
  92. package/dist/adapters/node/workspace.io.js +33 -0
  93. package/dist/adapters/node/workspace.io.js.map +1 -0
  94. package/dist/adapters/pdfjs/fill.d.ts +42 -0
  95. package/dist/adapters/pdfjs/fill.d.ts.map +1 -0
  96. package/dist/adapters/pdfjs/fill.js +91 -0
  97. package/dist/adapters/pdfjs/fill.js.map +1 -0
  98. package/dist/build-engine.d.ts +48 -0
  99. package/dist/build-engine.d.ts.map +1 -0
  100. package/dist/build-engine.js +148 -0
  101. package/dist/build-engine.js.map +1 -0
  102. package/dist/build.d.ts +163 -0
  103. package/dist/build.d.ts.map +1 -0
  104. package/dist/build.js +575 -0
  105. package/dist/build.js.map +1 -0
  106. package/dist/cli.d.ts +151 -0
  107. package/dist/cli.d.ts.map +1 -0
  108. package/dist/cli.js +951 -0
  109. package/dist/cli.js.map +1 -0
  110. package/dist/doctor.d.ts +42 -0
  111. package/dist/doctor.d.ts.map +1 -0
  112. package/dist/doctor.js +280 -0
  113. package/dist/doctor.js.map +1 -0
  114. package/dist/domain/geometry.d.ts +71 -0
  115. package/dist/domain/geometry.d.ts.map +1 -0
  116. package/dist/domain/geometry.js +35 -0
  117. package/dist/domain/geometry.js.map +1 -0
  118. package/dist/domain/host.d.ts +16 -0
  119. package/dist/domain/host.d.ts.map +1 -0
  120. package/dist/domain/host.js +8 -0
  121. package/dist/domain/host.js.map +1 -0
  122. package/dist/domain/page-layout.d.ts +34 -0
  123. package/dist/domain/page-layout.d.ts.map +1 -0
  124. package/dist/domain/page-layout.js +8 -0
  125. package/dist/domain/page-layout.js.map +1 -0
  126. package/dist/domain/paths.d.ts +5 -0
  127. package/dist/domain/paths.d.ts.map +1 -0
  128. package/dist/domain/paths.js +2 -0
  129. package/dist/domain/paths.js.map +1 -0
  130. package/dist/domain/result.d.ts +23 -0
  131. package/dist/domain/result.d.ts.map +1 -0
  132. package/dist/domain/result.js +10 -0
  133. package/dist/domain/result.js.map +1 -0
  134. package/dist/domain/sha256.d.ts +7 -0
  135. package/dist/domain/sha256.d.ts.map +1 -0
  136. package/dist/domain/sha256.js +14 -0
  137. package/dist/domain/sha256.js.map +1 -0
  138. package/dist/domain/text.d.ts +6 -0
  139. package/dist/domain/text.d.ts.map +1 -0
  140. package/dist/domain/text.js +7 -0
  141. package/dist/domain/text.js.map +1 -0
  142. package/dist/engine.d.ts +93 -0
  143. package/dist/engine.d.ts.map +1 -0
  144. package/dist/engine.js +119 -0
  145. package/dist/engine.js.map +1 -0
  146. package/dist/exit-code.d.ts +22 -0
  147. package/dist/exit-code.d.ts.map +1 -0
  148. package/dist/exit-code.js +10 -0
  149. package/dist/exit-code.js.map +1 -0
  150. package/dist/facts-file.d.ts +96 -0
  151. package/dist/facts-file.d.ts.map +1 -0
  152. package/dist/facts-file.js +134 -0
  153. package/dist/facts-file.js.map +1 -0
  154. package/dist/hooks-settings.d.ts +141 -0
  155. package/dist/hooks-settings.d.ts.map +1 -0
  156. package/dist/hooks-settings.js +306 -0
  157. package/dist/hooks-settings.js.map +1 -0
  158. package/dist/init.d.ts +201 -0
  159. package/dist/init.d.ts.map +1 -0
  160. package/dist/init.js +579 -0
  161. package/dist/init.js.map +1 -0
  162. package/dist/latex-log.d.ts +80 -0
  163. package/dist/latex-log.d.ts.map +1 -0
  164. package/dist/latex-log.js +187 -0
  165. package/dist/latex-log.js.map +1 -0
  166. package/dist/latex-loop.d.ts +129 -0
  167. package/dist/latex-loop.d.ts.map +1 -0
  168. package/dist/latex-loop.js +113 -0
  169. package/dist/latex-loop.js.map +1 -0
  170. package/dist/link-skills.d.ts +51 -0
  171. package/dist/link-skills.d.ts.map +1 -0
  172. package/dist/link-skills.js +199 -0
  173. package/dist/link-skills.js.map +1 -0
  174. package/dist/new-paper.d.ts +48 -0
  175. package/dist/new-paper.d.ts.map +1 -0
  176. package/dist/new-paper.js +110 -0
  177. package/dist/new-paper.js.map +1 -0
  178. package/dist/pdf-facts.d.ts +44 -0
  179. package/dist/pdf-facts.d.ts.map +1 -0
  180. package/dist/pdf-facts.js +239 -0
  181. package/dist/pdf-facts.js.map +1 -0
  182. package/dist/pdf-geometry.d.ts +170 -0
  183. package/dist/pdf-geometry.d.ts.map +1 -0
  184. package/dist/pdf-geometry.js +158 -0
  185. package/dist/pdf-geometry.js.map +1 -0
  186. package/dist/ports/download.d.ts +9 -0
  187. package/dist/ports/download.d.ts.map +1 -0
  188. package/dist/ports/download.js +2 -0
  189. package/dist/ports/download.js.map +1 -0
  190. package/dist/ports/files.d.ts +11 -0
  191. package/dist/ports/files.d.ts.map +1 -0
  192. package/dist/ports/files.js +2 -0
  193. package/dist/ports/files.js.map +1 -0
  194. package/dist/ports/measure-geometry.d.ts +8 -0
  195. package/dist/ports/measure-geometry.d.ts.map +1 -0
  196. package/dist/ports/measure-geometry.js +2 -0
  197. package/dist/ports/measure-geometry.js.map +1 -0
  198. package/dist/ports/process.d.ts +45 -0
  199. package/dist/ports/process.d.ts.map +1 -0
  200. package/dist/ports/process.js +2 -0
  201. package/dist/ports/process.js.map +1 -0
  202. package/dist/ports/tool-installer.d.ts +29 -0
  203. package/dist/ports/tool-installer.d.ts.map +1 -0
  204. package/dist/ports/tool-installer.js +2 -0
  205. package/dist/ports/tool-installer.js.map +1 -0
  206. package/dist/ports/workspace.d.ts +18 -0
  207. package/dist/ports/workspace.d.ts.map +1 -0
  208. package/dist/ports/workspace.js +2 -0
  209. package/dist/ports/workspace.js.map +1 -0
  210. package/dist/rules-config.d.ts +34 -0
  211. package/dist/rules-config.d.ts.map +1 -0
  212. package/dist/rules-config.js +132 -0
  213. package/dist/rules-config.js.map +1 -0
  214. package/dist/structure.d.ts +34 -0
  215. package/dist/structure.d.ts.map +1 -0
  216. package/dist/structure.js +149 -0
  217. package/dist/structure.js.map +1 -0
  218. package/dist/tex-requirements.d.ts +43 -0
  219. package/dist/tex-requirements.d.ts.map +1 -0
  220. package/dist/tex-requirements.js +127 -0
  221. package/dist/tex-requirements.js.map +1 -0
  222. package/dist/toolchain.d.ts +159 -0
  223. package/dist/toolchain.d.ts.map +1 -0
  224. package/dist/toolchain.js +542 -0
  225. package/dist/toolchain.js.map +1 -0
  226. package/dist/types.d.ts +110 -0
  227. package/dist/types.d.ts.map +1 -0
  228. package/dist/types.js +2 -0
  229. package/dist/types.js.map +1 -0
  230. package/docs/configuration.md +235 -0
  231. package/docs/e2e.md +152 -0
  232. package/docs/incidents.md +59 -0
  233. package/docs/install.md +170 -0
  234. package/docs/optional-rules.md +107 -0
  235. package/docs/package-shape-options.md +262 -0
  236. package/docs/prior-art/README.md +76 -0
  237. package/docs/prior-art/blocking-vs-advisory.md +83 -0
  238. package/docs/prior-art/content-delivery.md +124 -0
  239. package/docs/prior-art/multi-mode-tools.md +106 -0
  240. package/docs/prior-art/nondeterministic-checks.md +99 -0
  241. package/docs/prior-art/package-location.md +422 -0
  242. package/docs/prior-art/paper-folder-scaffolding.md +538 -0
  243. package/docs/prior-art/readme-structure.md +69 -0
  244. package/docs/prior-art/repro/README.md +92 -0
  245. package/docs/prior-art/repro/claim1-allowedtools.mjs +66 -0
  246. package/docs/prior-art/repro/claim1-at2.mjs +40 -0
  247. package/docs/prior-art/repro/claim1-crosschannel.mjs +54 -0
  248. package/docs/prior-art/repro/claim1-frontmatter.mjs +76 -0
  249. package/docs/prior-art/repro/claim1-hook-payload-reporter.mjs +10 -0
  250. package/docs/prior-art/repro/claim1-plugin-frontmatter.mjs +27 -0
  251. package/docs/prior-art/repro/claim1-plugin-skill.mjs +52 -0
  252. package/docs/prior-art/repro/claim1-project-skill.mjs +81 -0
  253. package/docs/prior-art/repro/claim2-marketplace-flat-asclaimed.json +1 -0
  254. package/docs/prior-art/repro/claim2-marketplace-negative-control.json +1 -0
  255. package/docs/prior-art/repro/claim2-marketplace-nested-exact.json +9 -0
  256. package/docs/prior-art/repro/claim2-marketplace-nested-noversion.json +9 -0
  257. package/docs/prior-art/repro/claim2-marketplace-nested-range.json +1 -0
  258. package/docs/prior-art/repro/claim3-imports.mjs +50 -0
  259. package/docs/prior-art/repro/claim4-find-package-json.mjs +8 -0
  260. package/docs/prior-art/repro/claim4-package-dir.mjs +39 -0
  261. package/docs/prior-art/repro/claim4-parent-arg.mjs +17 -0
  262. package/docs/prior-art/repro/claim4-resolve-apis.mjs +21 -0
  263. package/docs/prior-art/repro/claim4-setup-consumers.mjs +45 -0
  264. package/docs/prior-art/repro/claim4-yarn-pnp.mjs +70 -0
  265. package/docs/prior-art/repro/claim5-bin-launch.mjs +39 -0
  266. package/docs/prior-art/repro/claim5-exports-mutation.mjs +57 -0
  267. package/docs/prior-art/repro/claim5-resolved-location-and-bin.mjs +33 -0
  268. package/docs/prior-art/repro/claim6-candidate-ambiguity.mjs +17 -0
  269. package/docs/prior-art/repro/claim6-doc-path-candidates.mjs +27 -0
  270. package/docs/prior-art/test-tooling.md +131 -0
  271. package/docs/rules.md +58 -0
  272. package/docs/texlive-install-decision.md +230 -0
  273. package/docs/toolchain.md +152 -0
  274. package/eslint-rules/doc-fields.harness.mjs +336 -0
  275. package/eslint-rules/doc-fields.mjs +186 -0
  276. package/eslint-rules/doc-fields.mutations.mjs +96 -0
  277. package/eslint-rules/install-path-literals.harness.mjs +121 -0
  278. package/eslint-rules/install-path-literals.mjs +108 -0
  279. package/eslint-rules/install-path-literals.mutations.mjs +62 -0
  280. package/eslint-rules/latex-language.harness.mjs +599 -0
  281. package/eslint-rules/latex-language.mjs +591 -0
  282. package/eslint-rules/latex-language.mutations.mjs +196 -0
  283. package/eslint-rules/paper-research-question.harness.mjs +146 -0
  284. package/eslint-rules/paper-research-question.mjs +180 -0
  285. package/eslint-rules/paper-research-question.mutations.mjs +127 -0
  286. package/eslint-rules/paper-stages.harness.mjs +356 -0
  287. package/eslint-rules/paper-stages.mjs +455 -0
  288. package/eslint-rules/paper-stages.mutations.mjs +157 -0
  289. package/eslint-rules/paper-typography.harness.mjs +291 -0
  290. package/eslint-rules/paper-typography.mjs +313 -0
  291. package/eslint-rules/paper-typography.mutations.mjs +131 -0
  292. package/eslint-rules/papers.harness.mjs +259 -0
  293. package/eslint-rules/papers.mjs +166 -0
  294. package/eslint-rules/papers.mutations.mjs +186 -0
  295. package/eslint-rules/pdf-last-page-balance.harness.mjs +206 -0
  296. package/eslint-rules/pdf-last-page-balance.mjs +208 -0
  297. package/eslint-rules/review-findings-cause.harness.mjs +228 -0
  298. package/eslint-rules/review-findings-cause.mjs +135 -0
  299. package/eslint-rules/review-findings-cause.mutations.mjs +72 -0
  300. package/eslint-rules/temp-root-realpath.harness.mjs +176 -0
  301. package/eslint-rules/temp-root-realpath.mjs +129 -0
  302. package/eslint-rules/temp-root-realpath.mutations.mjs +99 -0
  303. package/eslint-rules/tex-build.harness.mjs +753 -0
  304. package/eslint-rules/tex-build.mjs +322 -0
  305. package/eslint-rules/tex-build.mutations.mjs +258 -0
  306. package/eslint.config.mjs +521 -0
  307. package/fixtures/build-e2e/acmart/PIPELINE-STATUS.md +3 -0
  308. package/fixtures/build-e2e/acmart/paper.tex +11 -0
  309. package/fixtures/build-e2e/acmart/venue.json +1 -0
  310. package/fixtures/build-e2e/broken/PIPELINE-STATUS.md +3 -0
  311. package/fixtures/build-e2e/broken/paper.tex +7 -0
  312. package/fixtures/build-e2e/cite/PIPELINE-STATUS.md +3 -0
  313. package/fixtures/build-e2e/cite/build.sh +5 -0
  314. package/fixtures/build-e2e/cite/paper.tex +10 -0
  315. package/fixtures/build-e2e/cite/refs.bib +9 -0
  316. package/fixtures/build-e2e/empty/PIPELINE-STATUS.md +3 -0
  317. package/fixtures/build-e2e/empty/paper.tex +6 -0
  318. package/fixtures/build-e2e/fallback/PIPELINE-STATUS.md +3 -0
  319. package/fixtures/build-e2e/fallback/paper.tex +11 -0
  320. package/fixtures/build-e2e/guards/PIPELINE-STATUS.md +3 -0
  321. package/fixtures/build-e2e/guards/paper.tex +10 -0
  322. package/fixtures/build-e2e/no-source/PIPELINE-STATUS.md +3 -0
  323. package/fixtures/build-e2e/unbalanced/PIPELINE-STATUS.md +3 -0
  324. package/fixtures/build-e2e/unbalanced/paper.tex +28 -0
  325. package/fixtures/build-e2e/unbalanced/refs.bib +269 -0
  326. package/fixtures/install-path-literals/clean.fixture.mjs +3 -0
  327. package/fixtures/install-path-literals/clean.md +15 -0
  328. package/fixtures/install-path-literals/defect.fixture.mjs +3 -0
  329. package/fixtures/install-path-literals/defect.md +14 -0
  330. package/fixtures/latex-language/clean.tex +50 -0
  331. package/fixtures/latex-language/defect.tex +52 -0
  332. package/fixtures/paper-research-question/comment-only/PIPELINE-STATUS.md +9 -0
  333. package/fixtures/paper-research-question/comment-only/paper.tex +7 -0
  334. package/fixtures/paper-research-question/declared-not-in-paper/PIPELINE-STATUS.md +10 -0
  335. package/fixtures/paper-research-question/declared-not-in-paper/paper.tex +6 -0
  336. package/fixtures/paper-research-question/draft/PIPELINE-STATUS.md +6 -0
  337. package/fixtures/paper-research-question/draft/paper.tex +2 -0
  338. package/fixtures/paper-research-question/markdown-no-rq/PIPELINE-STATUS.md +9 -0
  339. package/fixtures/paper-research-question/markdown-no-rq/paper.md +4 -0
  340. package/fixtures/paper-research-question/shipped-no-rq/PIPELINE-STATUS.md +12 -0
  341. package/fixtures/paper-research-question/shipped-no-rq/paper.tex +3 -0
  342. package/fixtures/paper-research-question/shipped-with-rq/PIPELINE-STATUS.md +10 -0
  343. package/fixtures/paper-research-question/shipped-with-rq/paper.tex +2 -0
  344. package/fixtures/paper-stages/authors-ran/PIPELINE-STATUS.md +16 -0
  345. package/fixtures/paper-stages/marker-in-prose/PIPELINE-STATUS.md +17 -0
  346. package/fixtures/paper-stages/nofile/PIPELINE-STATUS.md +8 -0
  347. package/fixtures/paper-stages/noheader/PIPELINE-STATUS.md +1 -0
  348. package/fixtures/paper-stages/noheader/versions/2026-07-22-submitted.pdf +0 -0
  349. package/fixtures/paper-stages/nothing/PIPELINE-STATUS.md +3 -0
  350. package/fixtures/paper-stages/ok/PIPELINE-STATUS.md +9 -0
  351. package/fixtures/paper-stages/ok/versions/2026-07-22-submitted.pdf +0 -0
  352. package/fixtures/paper-stages/stale/PIPELINE-STATUS.md +1 -0
  353. package/fixtures/paper-stages/stale/versions/2026-07-22-submitted.STALE-WRONG-FILE.pdf +0 -0
  354. package/fixtures/paper-stages/twice/PIPELINE-STATUS.md +14 -0
  355. package/fixtures/paper-stages/twice/versions/2026-08-06-submitted.pdf +0 -0
  356. package/fixtures/paper-stages/twice/versions/2026-10-24-submitted.pdf +0 -0
  357. package/fixtures/paper-stages/undeclared/PIPELINE-STATUS.md +8 -0
  358. package/fixtures/paper-stages/undeclared/versions/2026-07-22-submitted.pdf +0 -0
  359. package/fixtures/paper-stages/undeclared/versions/2026-08-29-camera-ready.pdf +0 -0
  360. package/fixtures/paper-stages/wrongsize/PIPELINE-STATUS.md +8 -0
  361. package/fixtures/paper-stages/wrongsize/versions/2026-07-22-submitted.pdf +0 -0
  362. package/fixtures/paper-typography/clean-paper/paper.tex +29 -0
  363. package/fixtures/paper-typography/messy-paper/paper.tex +27 -0
  364. package/fixtures/pdf-facts/README.md +22 -0
  365. package/fixtures/pdf-facts/corrupt-font.pdf +0 -0
  366. package/fixtures/pdf-facts/encrypted.pdf +0 -0
  367. package/fixtures/pdf-facts/hidden-text.pdf +0 -0
  368. package/fixtures/pdf-facts/hidden-text.tex +28 -0
  369. package/fixtures/pdf-facts/t3-all.pdf +0 -0
  370. package/fixtures/pdf-facts/t3-all.tex +8 -0
  371. package/fixtures/pdf-facts/t3-mixed.pdf +0 -0
  372. package/fixtures/pdf-facts/t3-mixed.tex +9 -0
  373. package/fixtures/pdf-facts/ttf.pdf +2240 -1
  374. package/fixtures/pdf-facts/ttf.tex +6 -0
  375. package/fixtures/real-markdown-paper/baseline.json +24 -0
  376. package/fixtures/real-markdown-paper/baseline.mjs +48 -0
  377. package/fixtures/render-paper/build-clean.sh +25 -0
  378. package/fixtures/render-paper/build-defect.sh +15 -0
  379. package/fixtures/review-findings-cause/clean.md +17 -0
  380. package/fixtures/review-findings-cause/defect.md +14 -0
  381. package/fixtures/review-findings-cause/old-debt.md +14 -0
  382. package/fixtures/review-findings-cause/quiet-in-fence.md +16 -0
  383. package/fixtures/tex-build/clean.tex +21 -0
  384. package/fixtures/tex-build/defect.tex +24 -0
  385. package/fixtures/tex-build/frontmatter-clean.tex +25 -0
  386. package/fixtures/tex-build/frontmatter-defect.tex +23 -0
  387. package/fixtures/toolchain-mirror/catalog.txt +5 -0
  388. package/fixtures/toolchain-mirror/install-tl +27 -0
  389. package/fixtures/toolchain-mirror/release-texlive.txt +3 -0
  390. package/fixtures/toolchain-mirror/release-year +1 -0
  391. package/fixtures/toolchain-mirror/stub-kpsewhich +8 -0
  392. package/fixtures/toolchain-mirror/stub-pdflatex +3 -0
  393. package/fixtures/toolchain-mirror/stub-tlmgr +44 -0
  394. package/hooks/hooks.harness.mjs +713 -0
  395. package/hooks/hooks.mutations.mjs +337 -0
  396. package/hooks/paper-edit-guard.hook.d.mts +13 -0
  397. package/hooks/paper-edit-guard.hook.mjs +457 -0
  398. package/hooks/paper-skills-nudge.hook.mjs +136 -0
  399. package/hooks/paper-status-gates.hook.mjs +156 -0
  400. package/hooks/paper-status-gates.sh +91 -0
  401. package/lib/agent-cli-version.harness.mjs +165 -0
  402. package/lib/agent-cli-version.mjs +106 -0
  403. package/lib/agent-cli-version.mutations.mjs +109 -0
  404. package/lib/markdown.mjs +386 -0
  405. package/lib/mutation-driver.harness.mjs +227 -0
  406. package/lib/mutation-driver.mjs +397 -0
  407. package/lib/mutation-driver.mutations.mjs +68 -0
  408. package/lib/paper-config.d.mts +34 -0
  409. package/lib/paper-config.harness.mjs +286 -0
  410. package/lib/paper-config.mjs +142 -0
  411. package/lib/paper-config.mutations.mjs +143 -0
  412. package/lib/skill-checks.mjs +701 -0
  413. package/lib/skill-corpus.mjs +403 -0
  414. package/lib/skill-eval-fixture.mjs +63 -0
  415. package/lib/skill-eval-kit.mjs +257 -0
  416. package/lib/skill-trigger-cases.harness.mjs +170 -0
  417. package/lib/skill-trigger-cases.mjs +446 -0
  418. package/lib/skill-trigger-cases.mutations.mjs +65 -0
  419. package/lib/trigger-ledger.mjs +215 -0
  420. package/package.json +97 -0
  421. package/plugin/.claude-plugin/plugin.json +8 -0
  422. package/plugin/hooks/hooks.json +30 -0
  423. package/scripts/check.harness.mjs +177 -0
  424. package/scripts/check.mjs +239 -0
  425. package/scripts/check.mutations.mjs +110 -0
  426. package/scripts/eslint-report-guard.mjs +82 -0
  427. package/scripts/exclusive.mjs +138 -0
  428. package/scripts/harness-api.frozen.json +76 -0
  429. package/scripts/harness-api.test.ts +175 -0
  430. package/scripts/layer-legacy-frozen.d.mts +28 -0
  431. package/scripts/layer-legacy-frozen.mjs +152 -0
  432. package/scripts/layer-legacy-frozen.test.ts +115 -0
  433. package/scripts/layer-legacy.frozen.json +50 -0
  434. package/scripts/mutation-batteries-frozen.harness.mjs +204 -0
  435. package/scripts/mutation-batteries-frozen.mjs +238 -0
  436. package/scripts/mutation-batteries.frozen.json +117 -0
  437. package/scripts/release-config.test.ts +90 -0
  438. package/scripts/rules-are-content-only.harness.mjs +113 -0
  439. package/scripts/rules-are-content-only.mjs +138 -0
  440. package/scripts/rules-are-content-only.mutations.mjs +81 -0
  441. package/scripts/rules-see-files.harness.mjs +115 -0
  442. package/scripts/rules-see-files.mjs +99 -0
  443. package/scripts/rules-see-files.mutations.mjs +131 -0
  444. package/scripts/run-mutations.mjs +100 -0
  445. package/scripts/semantic-release-plugins.d.ts +16 -0
  446. package/skills/README.md +15 -0
  447. package/skills/analyze-sibling-paper/SKILL.md +170 -0
  448. package/skills/analyze-sibling-paper/SKILL.md.spec.ts +186 -0
  449. package/skills/analyze-sibling-paper/analyze-sibling-paper.eval.mjs +19 -0
  450. package/skills/analyze-sibling-paper/analyze-sibling-paper.harness.mjs +23 -0
  451. package/skills/argument-arc/SKILL.md +177 -0
  452. package/skills/argument-arc/SKILL.md.spec.ts +192 -0
  453. package/skills/argument-arc/argument-arc.eval.mjs +19 -0
  454. package/skills/argument-arc/argument-arc.harness.mjs +23 -0
  455. package/skills/build-benchmark/SKILL.md +213 -0
  456. package/skills/build-benchmark/SKILL.md.spec.ts +220 -0
  457. package/skills/build-benchmark/build-benchmark.eval.mjs +19 -0
  458. package/skills/build-benchmark/build-benchmark.harness.mjs +23 -0
  459. package/skills/build-benchmark/references/adversarial-cold-repro.md +68 -0
  460. package/skills/camera-ready/SKILL.md +148 -0
  461. package/skills/camera-ready/SKILL.md.spec.ts +164 -0
  462. package/skills/camera-ready/camera-ready.eval.mjs +19 -0
  463. package/skills/camera-ready/camera-ready.harness.mjs +23 -0
  464. package/skills/cold-read-diff/SKILL.md +160 -0
  465. package/skills/cold-read-diff/SKILL.md.spec.ts +166 -0
  466. package/skills/cold-read-diff/cold-read-diff.eval.mjs +19 -0
  467. package/skills/cold-read-diff/cold-read-diff.harness.mjs +23 -0
  468. package/skills/draft-paper/SKILL.md +152 -0
  469. package/skills/draft-paper/SKILL.md.spec.ts +169 -0
  470. package/skills/draft-paper/draft-paper.eval.mjs +19 -0
  471. package/skills/draft-paper/draft-paper.harness.mjs +23 -0
  472. package/skills/extend-paper/SKILL.md +99 -0
  473. package/skills/extend-paper/SKILL.md.spec.ts +116 -0
  474. package/skills/extend-paper/extend-paper.eval.mjs +19 -0
  475. package/skills/extend-paper/extend-paper.harness.mjs +23 -0
  476. package/skills/find-venue/SKILL.md +128 -0
  477. package/skills/find-venue/SKILL.md.spec.ts +145 -0
  478. package/skills/find-venue/find-venue.eval.mjs +19 -0
  479. package/skills/find-venue/find-venue.harness.mjs +23 -0
  480. package/skills/grade-paper-writing/SKILL.md +436 -0
  481. package/skills/grade-paper-writing/SKILL.md.spec.ts +453 -0
  482. package/skills/grade-paper-writing/fixtures/control_gopen.txt +1 -0
  483. package/skills/grade-paper-writing/fixtures/control_human_paper.txt +1 -0
  484. package/skills/grade-paper-writing/fixtures/rewrite.txt +1 -0
  485. package/skills/grade-paper-writing/fixtures/specimen.txt +1 -0
  486. package/skills/grade-paper-writing/fixtures/structure-checks.md +22 -0
  487. package/skills/grade-paper-writing/grade-paper-writing.eval.mjs +19 -0
  488. package/skills/grade-paper-writing/grade-paper-writing.harness.mjs +23 -0
  489. package/skills/grade-paper-writing/prose-lint.mjs +713 -0
  490. package/skills/harden-paper/SKILL.md +318 -0
  491. package/skills/harden-paper/SKILL.md.spec.ts +336 -0
  492. package/skills/harden-paper/check-numbers.sh +33 -0
  493. package/skills/harden-paper/check-release-claims.sh +35 -0
  494. package/skills/harden-paper/fixtures/uncited-assertions-sample.md +43 -0
  495. package/skills/harden-paper/fixtures/uncited-assertions-sample.tex +77 -0
  496. package/skills/harden-paper/harden-paper.eval.mjs +19 -0
  497. package/skills/harden-paper/harden-paper.harness.mjs +23 -0
  498. package/skills/map-prior-work/SKILL.md +211 -0
  499. package/skills/map-prior-work/SKILL.md.spec.ts +227 -0
  500. package/skills/map-prior-work/map-prior-work.eval.mjs +19 -0
  501. package/skills/map-prior-work/map-prior-work.harness.mjs +23 -0
  502. package/skills/osf-artifact-upload/SKILL.md +52 -0
  503. package/skills/osf-artifact-upload/SKILL.md.spec.ts +59 -0
  504. package/skills/osf-artifact-upload/osf-artifact-upload.eval.mjs +22 -0
  505. package/skills/osf-artifact-upload/osf-artifact-upload.harness.mjs +103 -0
  506. package/skills/paper-adversarial-review/SKILL.md +126 -0
  507. package/skills/paper-adversarial-review/SKILL.md.spec.ts +142 -0
  508. package/skills/paper-adversarial-review/paper-adversarial-review.eval.mjs +19 -0
  509. package/skills/paper-adversarial-review/paper-adversarial-review.harness.mjs +23 -0
  510. package/skills/paper-pipeline/PIPELINE-MAP.md +371 -0
  511. package/skills/paper-pipeline/SKILL.md +499 -0
  512. package/skills/paper-pipeline/SKILL.md.spec.ts +517 -0
  513. package/skills/paper-pipeline/description-language.eval.mjs +347 -0
  514. package/skills/paper-pipeline/framing-vs-vocabulary.eval.mjs +891 -0
  515. package/skills/paper-pipeline/grade-paper-writing-ablation.eval.mjs +1254 -0
  516. package/skills/paper-pipeline/paper-pipeline.eval.mjs +22 -0
  517. package/skills/paper-pipeline/paper-pipeline.harness.mjs +143 -0
  518. package/skills/paper-pipeline/pipeline-firing.baseline.json +270 -0
  519. package/skills/paper-pipeline/pipeline-firing.eval.mjs +664 -0
  520. package/skills/paper-pipeline/pipeline-language.eval.mjs +672 -0
  521. package/skills/paper-pipeline/references/acceptance-gate.md +329 -0
  522. package/skills/paper-pipeline/references/acl-venue-rules.md +142 -0
  523. package/skills/paper-pipeline/references/anonymization.md +68 -0
  524. package/skills/paper-pipeline/references/artifact-checklist.md +93 -0
  525. package/skills/paper-pipeline/references/body-vs-appendix.md +97 -0
  526. package/skills/paper-pipeline/references/credit-criteria.md +69 -0
  527. package/skills/paper-pipeline/references/occupancy-2026-08-06-probe/README.md +35 -0
  528. package/skills/paper-pipeline/references/occupancy-2026-08-06-probe/run_retext.mjs +24 -0
  529. package/skills/paper-pipeline/references/occupancy-2026-08-06-probe/sentences.txt +11 -0
  530. package/skills/paper-pipeline/references/occupancy-2026-08-06-probe/test_sentences.py +25 -0
  531. package/skills/paper-pipeline/references/occupancy-2026-08-06-prose-checkers.md +538 -0
  532. package/skills/paper-pipeline/references/occupancy-2026-08-06-reproducible-tooling.md +431 -0
  533. package/skills/paper-pipeline/references/occupancy-2026-08-06-staleness-and-orchestration.md +592 -0
  534. package/skills/paper-pipeline/references/pipeline-status-template.md +162 -0
  535. package/skills/paper-pipeline/references/review-ratchet.md +36 -0
  536. package/skills/paper-pipeline/references/sweep-2026-08-09-ideal-pipeline.md +585 -0
  537. package/skills/paper-pipeline/references/writing-craft.md +448 -0
  538. package/skills/paper-pipeline/repro/2026-08-07-description-language-control.log +63 -0
  539. package/skills/paper-pipeline/repro/2026-08-07-fork-check.log +52 -0
  540. package/skills/paper-pipeline/repro/2026-08-07-fork-check2.log +33 -0
  541. package/skills/paper-pipeline/repro/2026-08-07-language-eval-pilot.log +33 -0
  542. package/skills/paper-pipeline/repro/2026-08-07-language-eval-raw.log +166 -0
  543. package/skills/paper-pipeline/repro/2026-08-08-framing-vs-vocabulary-oracle.json +338 -0
  544. package/skills/paper-pipeline/repro/2026-08-08-framing-vs-vocabulary-oracle.log +118 -0
  545. package/skills/paper-pipeline/repro/2026-08-08-framing-vs-vocabulary-raw.log +245 -0
  546. package/skills/paper-pipeline/repro/2026-08-08-framing-vs-vocabulary.json +776 -0
  547. package/skills/paper-pipeline/repro/2026-08-08-grade-paper-writing-ablation-A6-oracle.log +53 -0
  548. package/skills/paper-pipeline/repro/2026-08-08-grade-paper-writing-ablation-A6-raw.log +89 -0
  549. package/skills/paper-pipeline/repro/2026-08-08-grade-paper-writing-ablation-oracle.json +450 -0
  550. package/skills/paper-pipeline/repro/2026-08-08-grade-paper-writing-ablation-oracle.log +136 -0
  551. package/skills/paper-pipeline/repro/2026-08-08-grade-paper-writing-ablation-raw.log +242 -0
  552. package/skills/paper-pipeline/repro/2026-08-08-grade-paper-writing-ablation-setupdiff.log +59 -0
  553. package/skills/paper-pipeline/repro/2026-08-08-grade-paper-writing-ablation.json +1032 -0
  554. package/skills/paper-pipeline/repro/2026-08-08-parent-replication-gpw.json +139 -0
  555. package/skills/paper-pipeline/repro/2026-08-08-parent-replication-gpw.log +98 -0
  556. package/skills/paper-pipeline/repro/2026-08-08-parent-replication.mjs +92 -0
  557. package/skills/paper-pipeline/repro/README.md +129 -0
  558. package/skills/paper-pipeline/repro/analyze-language-eval.py +116 -0
  559. package/skills/paper-pipeline/scripts/README.md +344 -0
  560. package/skills/paper-pipeline/scripts/announce.mjs +67 -0
  561. package/skills/paper-pipeline/scripts/artifact-coverage.harness.mjs +496 -0
  562. package/skills/paper-pipeline/scripts/artifact-coverage.mjs +397 -0
  563. package/skills/paper-pipeline/scripts/artifact-coverage.mutations.mjs +218 -0
  564. package/skills/paper-pipeline/scripts/check-provenance.mjs +184 -0
  565. package/skills/paper-pipeline/scripts/consumer.d.mts +32 -0
  566. package/skills/paper-pipeline/scripts/consumer.harness.mjs +562 -0
  567. package/skills/paper-pipeline/scripts/consumer.mjs +535 -0
  568. package/skills/paper-pipeline/scripts/consumer.mutations.mjs +190 -0
  569. package/skills/paper-pipeline/scripts/extract-ref-facts.harness.mjs +457 -0
  570. package/skills/paper-pipeline/scripts/extract-ref-facts.mjs +656 -0
  571. package/skills/paper-pipeline/scripts/extract-ref-facts.mutations.mjs +54 -0
  572. package/skills/paper-pipeline/scripts/fixtures/clean/PIPELINE-STATUS.md +51 -0
  573. package/skills/paper-pipeline/scripts/fixtures/dirty/PIPELINE-STATUS.md +52 -0
  574. package/skills/paper-pipeline/scripts/fixtures/dirty/paper.md +6 -0
  575. package/skills/paper-pipeline/scripts/fixtures/real-bib/refs.bib +153 -0
  576. package/skills/paper-pipeline/scripts/generated-code.harness.mjs +466 -0
  577. package/skills/paper-pipeline/scripts/generated-code.mjs +338 -0
  578. package/skills/paper-pipeline/scripts/generated-code.mutations.mjs +254 -0
  579. package/skills/paper-pipeline/scripts/ledger.mjs +623 -0
  580. package/skills/paper-pipeline/scripts/ledger.selftest.mjs +286 -0
  581. package/skills/paper-pipeline/scripts/pipeline-check.harness.mjs +389 -0
  582. package/skills/paper-pipeline/scripts/pipeline-check.mjs +737 -0
  583. package/skills/paper-pipeline/scripts/pipeline-check.mutations.mjs +54 -0
  584. package/skills/paper-pipeline/scripts/pipeline-edges.mjs +169 -0
  585. package/skills/paper-pipeline/scripts/population-map.harness.mjs +178 -0
  586. package/skills/paper-pipeline/scripts/population-map.mjs +181 -0
  587. package/skills/paper-pipeline/scripts/population-map.mutations.mjs +65 -0
  588. package/skills/paper-pipeline/scripts/population-map.selftest.mjs +122 -0
  589. package/skills/paper-pipeline/scripts/provenance.harness.mjs +240 -0
  590. package/skills/paper-pipeline/scripts/provenance.mutations.mjs +59 -0
  591. package/skills/paper-pipeline/scripts/round-diff.harness.mjs +881 -0
  592. package/skills/paper-pipeline/scripts/round-diff.mjs +576 -0
  593. package/skills/paper-pipeline/scripts/round-diff.mutations.mjs +276 -0
  594. package/skills/paper-pipeline/scripts/run-mechanical.mjs +633 -0
  595. package/skills/paper-pipeline/scripts/status.mjs +295 -0
  596. package/skills/paper-status/SKILL.md +183 -0
  597. package/skills/paper-status/SKILL.md.spec.ts +190 -0
  598. package/skills/paper-status/paper-status.eval.mjs +22 -0
  599. package/skills/paper-status/paper-status.harness.mjs +25 -0
  600. package/skills/pc-panel-review/SKILL.md +263 -0
  601. package/skills/pc-panel-review/SKILL.md.spec.ts +280 -0
  602. package/skills/pc-panel-review/pc-panel-review.eval.mjs +19 -0
  603. package/skills/pc-panel-review/pc-panel-review.harness.mjs +23 -0
  604. package/skills/plan-paper-timeline/SKILL.md +182 -0
  605. package/skills/plan-paper-timeline/SKILL.md.spec.ts +200 -0
  606. package/skills/plan-paper-timeline/fixtures/fake-google-calendar.mjs +239 -0
  607. package/skills/plan-paper-timeline/plan-paper-timeline.effects.harness.mjs +431 -0
  608. package/skills/plan-paper-timeline/plan-paper-timeline.effects.mutations.mjs +65 -0
  609. package/skills/plan-paper-timeline/plan-paper-timeline.eval.mjs +19 -0
  610. package/skills/plan-paper-timeline/plan-paper-timeline.harness.mjs +23 -0
  611. package/skills/render-paper/SKILL.md +159 -0
  612. package/skills/render-paper/SKILL.md.spec.ts +166 -0
  613. package/skills/render-paper/check-render.sh +419 -0
  614. package/skills/render-paper/checkers-requirements.txt +55 -0
  615. package/skills/render-paper/ensure-checkers.sh +69 -0
  616. package/skills/render-paper/extract-pdf-facts.harness.mjs +166 -0
  617. package/skills/render-paper/extract-pdf-facts.mjs +144 -0
  618. package/skills/render-paper/render-paper.eval.mjs +19 -0
  619. package/skills/render-paper/render-paper.harness.mjs +339 -0
  620. package/skills/research-ideate/SKILL.md +136 -0
  621. package/skills/research-ideate/SKILL.md.spec.ts +152 -0
  622. package/skills/research-ideate/research-ideate.eval.mjs +19 -0
  623. package/skills/research-ideate/research-ideate.harness.mjs +23 -0
  624. package/skills/skill-contract.mutations.mjs +179 -0
  625. package/skills/study-accepted-papers/SKILL.md +206 -0
  626. package/skills/study-accepted-papers/SKILL.md.spec.ts +223 -0
  627. package/skills/study-accepted-papers/study-accepted-papers.eval.mjs +19 -0
  628. package/skills/study-accepted-papers/study-accepted-papers.harness.mjs +23 -0
  629. package/skills/submit-paper/SKILL.md +182 -0
  630. package/skills/submit-paper/SKILL.md.spec.ts +199 -0
  631. package/skills/submit-paper/check-deanon.sh +149 -0
  632. package/skills/submit-paper/references/publishers/acm.md +92 -0
  633. package/skills/submit-paper/references/venues/agenticdev.jsonc +108 -0
  634. package/skills/submit-paper/references/venues/agenticdev.md +139 -0
  635. package/skills/submit-paper/references/venues/agenticdev.tex +19 -0
  636. package/skills/submit-paper/references/venues/aisec.jsonc +101 -0
  637. package/skills/submit-paper/references/venues/aisec.md +105 -0
  638. package/skills/submit-paper/references/venues/paper-guards.tex +41 -0
  639. package/skills/submit-paper/references/venues/realm.jsonc +81 -0
  640. package/skills/submit-paper/references/venues/realm.md +155 -0
  641. package/skills/submit-paper/references/venues/tex-base.jsonc +50 -0
  642. package/skills/submit-paper/references/venues/venue-profile.schema.json +74 -0
  643. package/skills/submit-paper/submit-paper.eval.mjs +19 -0
  644. package/skills/submit-paper/submit-paper.harness.mjs +23 -0
  645. package/skills/sweep-design-space/SKILL.md +269 -0
  646. package/skills/sweep-design-space/SKILL.md.spec.ts +285 -0
  647. package/skills/sweep-design-space/sweep-design-space.eval.mjs +19 -0
  648. package/skills/sweep-design-space/sweep-design-space.harness.mjs +23 -0
  649. package/skills/tighten-paper/SKILL.md +368 -0
  650. package/skills/tighten-paper/SKILL.md.spec.ts +384 -0
  651. package/skills/tighten-paper/structure.mjs +371 -0
  652. package/skills/tighten-paper/tighten-paper.eval.mjs +19 -0
  653. package/skills/tighten-paper/tighten-paper.harness.mjs +23 -0
  654. package/skills/verify-citations/SKILL.md +328 -0
  655. package/skills/verify-citations/SKILL.md.spec.ts +345 -0
  656. package/skills/verify-citations/scripts/bib-authors.mjs +479 -0
  657. package/skills/verify-citations/scripts/bib-authors.test.mjs +175 -0
  658. package/skills/verify-citations/scripts/verify-cites.mjs +1108 -0
  659. package/skills/verify-citations/scripts/verify-cites.test.mjs +735 -0
  660. package/skills/verify-citations/verify-citations.eval.mjs +19 -0
  661. package/skills/verify-citations/verify-citations.harness.mjs +23 -0
  662. package/src/CLAUDE.md +51 -0
  663. package/src/action-ref.test.ts +26 -0
  664. package/src/action-ref.ts +15 -0
  665. package/src/adapters/banal/failure.test.ts +63 -0
  666. package/src/adapters/banal/failure.ts +118 -0
  667. package/src/adapters/banal/index.test.ts +119 -0
  668. package/src/adapters/banal/index.ts +100 -0
  669. package/src/adapters/banal/install.test.ts +20 -0
  670. package/src/adapters/banal/install.ts +41 -0
  671. package/src/adapters/banal/invocation.test.ts +74 -0
  672. package/src/adapters/banal/invocation.ts +95 -0
  673. package/src/adapters/banal/locate.test.ts +52 -0
  674. package/src/adapters/banal/locate.ts +84 -0
  675. package/src/adapters/banal/output.test.ts +140 -0
  676. package/src/adapters/banal/output.ts +141 -0
  677. package/src/adapters/banal/pin.ts +30 -0
  678. package/src/adapters/banal/probe.ts +35 -0
  679. package/src/adapters/banal/run.test.ts +191 -0
  680. package/src/adapters/banal/run.ts +244 -0
  681. package/src/adapters/banal/settings.test.ts +31 -0
  682. package/src/adapters/banal/settings.ts +55 -0
  683. package/src/adapters/banal/xml.test.ts +111 -0
  684. package/src/adapters/banal/xml.ts +112 -0
  685. package/src/adapters/curl/download.io.ts +73 -0
  686. package/src/adapters/curl/download.test.ts +55 -0
  687. package/src/adapters/curl/index.ts +5 -0
  688. package/src/adapters/memory/index.ts +131 -0
  689. package/src/adapters/node/files.io.ts +39 -0
  690. package/src/adapters/node/files.test.ts +28 -0
  691. package/src/adapters/node/host.io.ts +15 -0
  692. package/src/adapters/node/index.ts +36 -0
  693. package/src/adapters/node/process.io.ts +49 -0
  694. package/src/adapters/node/process.test.ts +46 -0
  695. package/src/adapters/node/workspace.io.ts +40 -0
  696. package/src/adapters/node/workspace.test.ts +58 -0
  697. package/src/adapters/pdfjs/fill.test.ts +111 -0
  698. package/src/adapters/pdfjs/fill.ts +141 -0
  699. package/src/build-engine.harness.mjs +314 -0
  700. package/src/build-engine.ts +219 -0
  701. package/src/build.harness.mjs +631 -0
  702. package/src/build.mutations.mjs +195 -0
  703. package/src/build.ts +793 -0
  704. package/src/cli.harness.mjs +2007 -0
  705. package/src/cli.mutations.mjs +448 -0
  706. package/src/cli.ts +1189 -0
  707. package/src/doctor.harness.mjs +396 -0
  708. package/src/doctor.mutations.mjs +175 -0
  709. package/src/doctor.ts +356 -0
  710. package/src/domain/geometry.ts +108 -0
  711. package/src/domain/host.ts +23 -0
  712. package/src/domain/page-layout.ts +32 -0
  713. package/src/domain/paths.ts +5 -0
  714. package/src/domain/result.test.ts +26 -0
  715. package/src/domain/result.ts +29 -0
  716. package/src/domain/sha256.test.ts +12 -0
  717. package/src/domain/sha256.ts +21 -0
  718. package/src/domain/text.ts +11 -0
  719. package/src/engine.harness.mjs +252 -0
  720. package/src/engine.ts +176 -0
  721. package/src/exit-code.test.ts +21 -0
  722. package/src/exit-code.ts +38 -0
  723. package/src/facts-file.test.ts +240 -0
  724. package/src/facts-file.ts +241 -0
  725. package/src/hooks-settings.harness.mjs +386 -0
  726. package/src/hooks-settings.mutations.mjs +116 -0
  727. package/src/hooks-settings.ts +434 -0
  728. package/src/init.ts +900 -0
  729. package/src/latex-log.harness.mjs +226 -0
  730. package/src/latex-log.ts +234 -0
  731. package/src/latex-loop.harness.mjs +449 -0
  732. package/src/latex-loop.ts +211 -0
  733. package/src/link-skills.harness.mjs +273 -0
  734. package/src/link-skills.mutations.mjs +136 -0
  735. package/src/link-skills.ts +258 -0
  736. package/src/new-paper.harness.mjs +216 -0
  737. package/src/new-paper.mutations.mjs +79 -0
  738. package/src/new-paper.ts +158 -0
  739. package/src/pdf-facts.harness.mjs +188 -0
  740. package/src/pdf-facts.ts +327 -0
  741. package/src/pdf-geometry.harness.mjs +254 -0
  742. package/src/pdf-geometry.ts +300 -0
  743. package/src/ports/download.ts +10 -0
  744. package/src/ports/files.ts +11 -0
  745. package/src/ports/measure-geometry.ts +8 -0
  746. package/src/ports/process.ts +46 -0
  747. package/src/ports/tool-installer.ts +33 -0
  748. package/src/ports/workspace.ts +20 -0
  749. package/src/rules-config.harness.mjs +114 -0
  750. package/src/rules-config.ts +178 -0
  751. package/src/structure.harness.mjs +179 -0
  752. package/src/structure.mutations.mjs +83 -0
  753. package/src/structure.ts +166 -0
  754. package/src/tex-requirements.harness.mjs +238 -0
  755. package/src/tex-requirements.ts +181 -0
  756. package/src/toolchain.harness.mjs +651 -0
  757. package/src/toolchain.ts +755 -0
  758. package/src/types.ts +106 -0
  759. package/templates/paper/PIPELINE-STATUS.md +72 -0
  760. package/templates/paper/paper.md +4 -0
  761. package/templates/paper/paper.tex +8 -0
  762. package/tsconfig.json +23 -0
@@ -0,0 +1,633 @@
1
+ // run-mechanical.mjs — run every check that is a SCRIPT, and record what each said.
2
+ //
3
+ // node .claude/skills/paper-pipeline/scripts/run-mechanical.mjs <papers-root>/<paper-a>
4
+ //
5
+ // WHY THIS EXISTS, and why it is separate from the judgement gates. Half this pipeline is a
6
+ // program and half is a reading. The program half needs no agent, no model and no author — it
7
+ // needs someone to type the command, and that was the whole failure: `structure.mjs` shipped on
8
+ // 2026-08-05 wired to nothing, its findings reachable only by a person who remembered it existed.
9
+ // A check nobody runs and a check that does not exist differ only in the disappointment.
10
+ //
11
+ // So this runs them all, in one command, and writes each row to the ledger. What it does NOT
12
+ // do is decide anything: the exit code is the worst check's, and a BLOCKING finding here means a
13
+ // fact is wrong, never that a judgement is unfavourable. Judgement gates (cold-read-diff,
14
+ // tighten-paper, the panels) are read by a person or an agent and record themselves — they are
15
+ // not in this file, and putting them here would be the category error the CI workflow just got
16
+ // fixed for.
17
+ //
18
+ // ── 2026-08-10: ONE CHECK, ONE ROW ───────────────────────────────────────────────────────────
19
+ //
20
+ // 🔴 THE KEY USED TO BE `skill`, AND THAT WAS THE DEFECT. `status()` takes the last row per key,
21
+ // which is right across repeated runs of one check and wrong across two different checks. Filed
22
+ // under one skill name, two checks took turns being the answer: a clean run of the second erased
23
+ // a finding of the first from every derived view, while `runs.jsonl` still honestly held both.
24
+ //
25
+ // The cost was measurable and was paid. `repro/arm_permutation.py` and `repro/delivered_pdf.py`
26
+ // were LEFT OUT of this file for exactly that reason — their natural owner is `build-benchmark`
27
+ // and `check-provenance.mjs` was already sitting on that row, so wiring them in would have made
28
+ // three checks take turns. Both are wired in below now that each carries its own `check` id.
29
+ //
30
+ // There is no `PASS` verdict any more (see ledger.mjs). A check that runs and finds nothing
31
+ // records `ABSTAINED no-witness`, and "the paper is clean" is an inference the reader makes from
32
+ // the absence of findings — never a value this file writes down.
33
+
34
+ import { record, ABSTENTIONS } from "./ledger.mjs";
35
+ import { execFileSync } from "node:child_process";
36
+ import {
37
+ existsSync,
38
+ mkdirSync,
39
+ readFileSync,
40
+ readdirSync,
41
+ writeFileSync,
42
+ } from "node:fs";
43
+ import { resolve, join, dirname } from "node:path";
44
+ import { fileURLToPath, pathToFileURL } from "node:url";
45
+ import { isMain, settingsOf } from "./consumer.mjs";
46
+ import { consumerRoot } from "./consumer.mjs";
47
+ const HERE = dirname(fileURLToPath(import.meta.url));
48
+
49
+ // 🔴 2026-08-26 — THIS USED TO BE `'..', '..'`, AND IT BROKE THE WHOLE FILE SILENTLY. The file
50
+ // moved on 15.08 from `.claude/pipeline/` to `.claude/skills/paper-pipeline/scripts/`, that is, two
51
+ // levels deeper, while the count of `..` did not change: `ROOT` resolved to `.claude/skills`, and
52
+ // EVERY command in `GATES` (all paths in them are from the repo root) was looked up under
53
+ // `.claude/skills/.claude/skills/…`.
54
+ //
55
+ // Measurement before the fix, `run-mechanical.mjs <papers-root>/<paper-a>`: eight checks out of
56
+ // thirteen failed with `Cannot find module '.claude/skills/.claude/skills/…'`, and not one
57
+ // was recorded as `crashed` — `e.code === 'ENOENT'` catches a missing INTERPRETER, and node was
58
+ // found and printed the stack trace to stderr itself. The `flags` mode counted its lines: **six
59
+ // `FINDING` rows with findings: 16** went into the ledger, where sixteen is the height of the stack
60
+ // trace. One of them (`verify-cites`, `read: 'exit'`) was `blocking: true` on top of that, i.e. the
61
+ // run "found a fact".
62
+ //
63
+ // This is exactly the "four forms of a reference" class from `CLAUDE.md`: a file that moves changes
64
+ // the DEPTH of `../` to the root, and a grep over the path does not show it. All fifteen neighbours
65
+ // in the directory (`*.harness.mjs`, `*.mutations.mjs`) got four `..` during the resettlement; this
66
+ // file did not, because it was the only one that was not a test and nobody ran it.
67
+ const ROOT = consumerRoot();
68
+
69
+ // The structure rule names are taken FROM THE MODULE ITSELF, not rewritten here as a list. A list
70
+ // written by hand rots the day a thirteenth rule is added to `paper-structure.mjs` — and it rots
71
+ // toward silence: the new rule simply does not reach the filter, while the ledger row goes on
72
+ // looking like it works. `CLAUDE.md`: everything that can be derived, derive.
73
+ // Both citation checkers live in ONE place and are pointed at each paper's build; a copy per
74
+ // paper is four files that have to be kept in agreement.
75
+ // 🔴 A DECLARATION, NOT AN ADDRESS. The citation checkers (`report-submission.py`,
76
+ // `uncited_refs.py`) are python that lives AT THE CONSUMER: the pipeline calls them but does not
77
+ // ship them. What stood here was a path into one specific paper of the first consumer — in an
78
+ // extracted package it would both point into somebody else's tree and name somebody else's work.
79
+ //
80
+ // ⚠️ THERE IS NO DEFAULT DELIBERATELY, and that is what distinguishes this key from
81
+ // `papersDir`/`ledger`. There a default is meaningful (the `papers` directory, a file next to it);
82
+ // here any guessed path would be wrong for everyone but one. Not declared — three `GATES` rows
83
+ // simply do not run: `needs()` below checks the directory on disk, so the absence reads as
84
+ // ABSTAINED `input-missing`, not as a crash and not as a clean run.
85
+ const CITE_CHECKS =
86
+ process.env.PIPELINE_CITE_CHECKS ||
87
+ settingsOf(JSON.parse(readFileSync(join(ROOT, "package.json"), "utf8")))
88
+ ?.citeChecks ||
89
+ null;
90
+ // The citation-checker directory exists and is declared. It sits in the `needs()` of every row
91
+ // that calls it: without that a wrong or undeclared path would fail with a python stack trace, and
92
+ // in `flags` mode a stack trace is counted line by line and goes into the ledger as a FINDING whose
93
+ // count is the height of the trace — exactly what already happened here on 26.08 with the wrong
94
+ // `..` depth.
95
+ const hasCiteChecks = () =>
96
+ CITE_CHECKS !== null && existsSync(join(ROOT, CITE_CHECKS));
97
+ // 🔴 A CHECKER THAT IS NOT INSTALLED IS `input-missing`, NEVER A RUN. Some rows shell out to
98
+ // scripts belonging to OTHER skills of the pipeline, which a consumer may not have installed.
99
+ // Without this the command fails, node prints a stack trace, and `read: "flags"` COUNTS ITS LINES
100
+ // — a missing checker arrives in the ledger as a FINDING whose count is the height of the trace.
101
+ // That is not hypothetical: it happened here on 2026-08-26 (see the note on ROOT above), six rows,
102
+ // one of them blocking. `needs()` is the only place where «I could not run» stays distinct from
103
+ // «I ran and saw something».
104
+ const hasScript = (rel) => existsSync(join(ROOT, rel));
105
+ // The only layout difference: some papers render into `build/`, the rest are built in place.
106
+ const buildOf = (d) => (existsSync(join(d, "build")) ? join(d, "build") : d);
107
+ const hasBuilt = (d, ext) =>
108
+ existsSync(buildOf(d)) &&
109
+ readdirSync(buildOf(d)).some((f) => f.endsWith(ext));
110
+ // `.body-limit` declares the venue's page limit — that is, a paper that has a submission, and so
111
+ // a point to the blocks about anonymity/URLs/pages. The marker lives IN THE PAPER ITSELF, not as a
112
+ // name in the script, so a new paper is picked up on its own.
113
+ const hasVenue = (d) => existsSync(join(d, ".body-limit"));
114
+
115
+ // 🔴 THE CONSUMER'S RULE MODULE, RESOLVED FROM THE CONSUMER ROOT — not by counting `..` from
116
+ // this file. It was `../../../../eslint-rules/paper-structure.mjs`, which was a distance, and a
117
+ // distance stops being true the moment the file moves; from inside `node_modules` those four
118
+ // levels land two directories above the repository. The names are still READ FROM THE MODULE
119
+ // rather than copied here — a hand-written list rots toward silence the day a rule is added.
120
+ //
121
+ // Absence is tolerated and REPORTED, because this module belongs to the consumer: a repository
122
+ // that lints its papers some other way has no such file, and the one gate that filters on these
123
+ // names simply does not run. `needs()` on that gate asks the same question, so the absence
124
+ // arrives as ABSTAINED `input-missing` rather than as a clean run.
125
+ const STRUCTURE_RULES_FILE = join(ROOT, "eslint-rules", "paper-structure.mjs");
126
+ const STRUCTURE_RULES = existsSync(STRUCTURE_RULES_FILE)
127
+ ? new Set(
128
+ Object.keys(
129
+ (await import(pathToFileURL(STRUCTURE_RULES_FILE).href)).default,
130
+ ).map((r) => `paper/${r}`),
131
+ )
132
+ : new Set();
133
+
134
+ // skill → the skill this check belongs to (its SKILL.md is what `skillHash` keys on)
135
+ // check → THE ROW KEY, unique per check. Two checks under one skill are two rows.
136
+ // cmd → argv, run from the repo root
137
+ // read → how to read the result:
138
+ // `exit` — the script's own exit code is the answer (a FACT check); a non-zero
139
+ // exit is a BLOCKING finding.
140
+ // `flags` — non-empty output is a finding and never blocking (a JUDGEMENT check).
141
+ // `json` — stdout is a JSON array of findings; its length is the count.
142
+ // `eslint` — stdout is ESLint's `--format json` (an array of FILES, each with its
143
+ // own `messages`), so the count is the number of messages whose `ruleId`
144
+ // is in this gate's `rules` set — never the length of the array, which is
145
+ // the number of files and would read 1 on a paper with fifty findings.
146
+ // rules → `read: 'eslint'` only: the rule ids this row owns. One ESLint run carries every
147
+ // rule that matched the file, and the rows here are per-check, so each row takes its
148
+ // own and leaves the rest to the row that owns them.
149
+ // Exported FOR THE HARNESS. `main()` runs only on a direct call (bottom of the file), so importing
150
+ // this module has no side effects, and a test gets the ability to take a row and run ITS COMMAND —
151
+ // not its own copy of the command, which would drift from this one at the first edit.
152
+ export const GATES = [
153
+ {
154
+ skill: "render-paper",
155
+ check: "report-submission",
156
+ cmd: (d) => [
157
+ "python3",
158
+ join(CITE_CHECKS, "report-submission.py"),
159
+ buildOf(d),
160
+ ],
161
+ needs: (d) => hasCiteChecks() && hasBuilt(d, ".log") && hasVenue(d),
162
+ read: "exit",
163
+ note: "body pages, overfull boxes, unresolved refs, dropped characters, anonymity, artifact URLs",
164
+ },
165
+ {
166
+ // The same program, a narrow slice: for a paper with no declared venue limit the anonymity
167
+ // and URL blocks answer somebody else's question (measured 28.08 on the ACCEPTED agenticdev: 18
168
+ // anonymity findings, not one of them a defect — the camera-ready is de-anonymized on purpose).
169
+ // A gate that is red for reasons the reader is obliged to ignore stops being read.
170
+ skill: "render-paper",
171
+ check: "report-submission-citations",
172
+ cmd: (d) => [
173
+ "python3",
174
+ join(CITE_CHECKS, "report-submission.py"),
175
+ buildOf(d),
176
+ "--only",
177
+ "citations",
178
+ ],
179
+ needs: (d) => hasCiteChecks() && hasBuilt(d, ".log") && !hasVenue(d),
180
+ read: "exit",
181
+ note: "a \\cite with no bibliography entry, from the pdflatex log",
182
+ },
183
+ {
184
+ // 🔴 2026-08-26 — THIS ROW WAS A GREEN-AND-DEAD GATE FOR EXACTLY ONE DAY, AND THAT IS MEASURED.
185
+ // It used to be `node .claude/skills/tighten-paper/structure.mjs --flags-only`. On the same day
186
+ // all sixteen checks from there moved into ESLint rules (`eslint-rules/paper-structure.mjs`),
187
+ // and the `--flags-only` mode started to BE SILENT ALWAYS and exit 0 — deliberately, so that the
188
+ // pointer "see eslint" would not turn into a permanent false finding. But `read: 'flags'` reads
189
+ // silence as "no findings", so the ledger recorded `ABSTAINED no-witness` BY SILENCE, NOT BY
190
+ // CHECKING. Measured on `<paper-a>/paper.md`: `--flags-only` — 0 lines, exit 0; the same
191
+ // bytes through ESLint — **6 findings** (`subsection-size` ×2 · `section-lead` ·
192
+ // `free-section-size` ×2 · `block-ungraded`). Six findings the consumer learned nothing about.
193
+ //
194
+ // WHY THE ROW WAS REPOINTED AND NOT DELETED. For a CI step deleting it was the right call —
195
+ // there `npx eslint … .` over the whole repo already sits next to it, and it prints the
196
+ // structural findings. Here there is no duplicate: ESLint writes NOTHING into the ledger, and
197
+ // the ledger is the only place where "somebody looked at the structure of this paper on these
198
+ // bytes" is kept together with the hash of the input. Removing the row would trade silence for
199
+ // absence.
200
+ //
201
+ // The filter on `STRUCTURE_RULES` is deliberate: ESLint gives 30 findings on this file, 24 of
202
+ // them prose and craft, which have THEIR OWN row in the ledger. Without the filter one row
203
+ // would take somebody else's findings, and that is exactly "two checks in turn become the
204
+ // answer", to cure which `check` was made the key of a row.
205
+ skill: "tighten-paper",
206
+ check: "structure",
207
+ cmd: (d) => [
208
+ "node_modules/.bin/eslint",
209
+ "--no-config-lookup",
210
+ "--config",
211
+ "eslint.config.mjs",
212
+ "--format",
213
+ "json",
214
+ join(d, "paper.md"),
215
+ ],
216
+ // A binary that is not installed is `input-missing` (the checking tool is an input of the
217
+ // run), not `crashed`. The same logic and the same shape as `textidote` below.
218
+ // Plus the rule module itself: without it `rules` is empty, and a filter over an empty set
219
+ // would leave zero findings out of thirty — a confident green run instead of an honest "no
220
+ // input".
221
+ needs: () =>
222
+ existsSync(join(ROOT, "node_modules/.bin/eslint")) &&
223
+ STRUCTURE_RULES.size > 0,
224
+ read: "eslint",
225
+ rules: STRUCTURE_RULES,
226
+ note: "section weights, the free half vs the half under a page limit, unjustified blocks",
227
+ },
228
+ {
229
+ skill: "grade-paper-writing",
230
+ check: "prose-lint",
231
+ cmd: (d) => [
232
+ "node",
233
+ ".claude/skills/grade-paper-writing/prose-lint.mjs",
234
+ "--flags-only",
235
+ join(d, "paper.md"),
236
+ ],
237
+ needs: () => hasScript(".claude/skills/grade-paper-writing/prose-lint.mjs"),
238
+ read: "flags",
239
+ note: "thresholds with published baselines; judgement, so it never fails the run",
240
+ },
241
+ {
242
+ skill: "verify-citations",
243
+ check: "verify-cites",
244
+ cmd: () => [
245
+ "node",
246
+ ".claude/skills/verify-citations/scripts/verify-cites.test.mjs",
247
+ ],
248
+ needs: () =>
249
+ hasScript(
250
+ ".claude/skills/verify-citations/scripts/verify-cites.test.mjs",
251
+ ),
252
+ read: "exit",
253
+ note: "the mechanical half only — the fetching half needs the network and a reader",
254
+ },
255
+ {
256
+ skill: "harden-paper",
257
+ check: "artifact-coverage",
258
+ cmd: (d) => ["node", join(HERE, "artifact-coverage.mjs"), d],
259
+ read: "flags",
260
+ note: "does the released bundle hold data for what the paper points at",
261
+ },
262
+ {
263
+ skill: "build-benchmark",
264
+ check: "check-provenance",
265
+ cmd: (d) => ["node", join(HERE, "check-provenance.mjs"), d],
266
+ read: "flags",
267
+ note: "bolded figures with no row in any provenance file",
268
+ },
269
+ // 🔴 THE TWO THAT HAD NOWHERE TO FILE. Both are `build-benchmark` checks; both were unwired
270
+ // until the row key became the check, and until then their only runner was their own harness.
271
+ {
272
+ skill: "build-benchmark",
273
+ check: "arm-permutation",
274
+ cmd: (d) => [
275
+ "python3",
276
+ join(d, "repro/arm_permutation.py"),
277
+ "--repro",
278
+ join(d, "repro"),
279
+ "--paper",
280
+ join(d, "paper.md"),
281
+ "--json",
282
+ ],
283
+ needs: (d) => existsSync(join(d, "repro/arm_permutation.py")),
284
+ read: "json",
285
+ note: "a quantity that does not move when its arm label moves was transcribed, not computed",
286
+ },
287
+ {
288
+ skill: "build-benchmark",
289
+ check: "delivered-pdf",
290
+ cmd: (d) => [
291
+ "python3",
292
+ join(d, "repro/delivered_pdf.py"),
293
+ "--repro",
294
+ join(d, "repro"),
295
+ "--paper",
296
+ join(d, "paper.md"),
297
+ "--pdf",
298
+ join(d, "build/acl_latex.pdf"),
299
+ "--json",
300
+ ],
301
+ needs: (d) =>
302
+ existsSync(join(d, "repro/delivered_pdf.py")) &&
303
+ existsSync(join(d, "build/acl_latex.pdf")),
304
+ read: "json",
305
+ note: "every registered quantity reached the delivered page beside the prose that claims it",
306
+ },
307
+ {
308
+ // Ported from MedSci Skills' `check_generated_code.py` (arXiv:2606.09500v4). Filed under
309
+ // `build-benchmark` because that is the pass that builds the reproduction artifact, and this
310
+ // asks whether the scripts inside it run the same way twice on somebody else's disk.
311
+ //
312
+ // 🔴 NOT a duplicate of `check-anon.sh` category 5, which greps the same four path prefixes.
313
+ // That one reads the SHIPPED BUNDLE and asks whether it leaks the author's machine layout;
314
+ // this one reads the WORKING TREE before the scripts run and asks whether they are portable at
315
+ // all. The trees they scan are disjoint by construction — the bundle is excluded here — and the
316
+ // exclusion is printed in the check's own ignore ledger rather than hidden in a glob.
317
+ skill: "build-benchmark",
318
+ check: "generated-code",
319
+ cmd: (d) => ["node", join(HERE, "generated-code.mjs"), d],
320
+ read: "flags",
321
+ note: "analysis scripts that never seed randomness, hard-code an absolute path, or clobber their own input",
322
+ },
323
+ // 🔴 TWO EXTERNAL CHECKERS, ADDED 2026-08-10. Both cover a surface every check in this file
324
+ // reads past: one has no dictionary, the other never looks at the citation graph backwards.
325
+ {
326
+ // Filed under `grade-paper-writing` DELIBERATELY, not under `verify-citations` or a skill of
327
+ // its own. That skill already owns `prose-lint`, which is the paper's PROSE surface; spelling
328
+ // and grammar are the same surface, judged by a different instrument. A separate skill would
329
+ // have meant a 23rd SKILL.md to keep wired, and filing it under `render-paper` (which owns the
330
+ // built artifact) would have put a source-text check on the row for the check that reads PDFs.
331
+ skill: "grade-paper-writing",
332
+ check: "textidote",
333
+ cmd: (d) => [
334
+ "python3",
335
+ join(d, "repro/textidote_check.py"),
336
+ "--paper",
337
+ join(d, "paper.md"),
338
+ "--baseline",
339
+ join(d, "repro/textidote-grandfathered.txt"),
340
+ "--json",
341
+ ],
342
+ // The jar is 224 MB and is NOT committed, so its absence is normal on a fresh container and
343
+ // must read as `input-missing` rather than as silence. `crashed` would be the other honest
344
+ // answer; `input-missing` is chosen because the checker is an input the run did not have.
345
+ // 🔴 MIRRORS `find_jar()` IN THE CHECKER, INCLUDING ITS ONE RULE: an explicit $TEXTIDOTE_JAR is
346
+ // AUTHORITATIVE, so naming a missing file means "absent" rather than "fall back to /opt". The
347
+ // first version of this line used `.some()` over both candidates and disagreed with the checker
348
+ // — the gate would have called the input present and the checker would then have refused, so an
349
+ // absent jar was recorded as `crashed` instead of `input-missing`. Two answers to one question,
350
+ // which is the whole failure mode of a gate that does not read what it gates.
351
+ needs: (d) =>
352
+ existsSync(join(d, "repro/textidote_check.py")) &&
353
+ existsSync(process.env.TEXTIDOTE_JAR || "/opt/textidote/textidote.jar"),
354
+ read: "json",
355
+ note: "a spelling or grammar warning with no row in repro/textidote-grandfathered.txt",
356
+ },
357
+ {
358
+ // `verify-citations` owns the bibliography, and this is that surface read in the one direction
359
+ // its own check cannot see: verify-cites asks whether an ENTRY names a real work, and this asks
360
+ // whether anything points AT the entry. Same file, opposite arrow, so the same skill.
361
+ skill: "verify-citations",
362
+ check: "uncited-refs",
363
+ cmd: (d) => [
364
+ "python3",
365
+ join(CITE_CHECKS, "uncited_refs.py"),
366
+ "--build",
367
+ buildOf(d),
368
+ "--json",
369
+ ],
370
+ needs: (d) => hasCiteChecks() && hasBuilt(d, ".aux"),
371
+ read: "json",
372
+ note: "a bibliography entry no \\cite in the paper points at",
373
+ },
374
+ {
375
+ skill: "draft-paper",
376
+ check: "population-map",
377
+ cmd: (d) => ["node", join(HERE, "population-map.mjs"), d],
378
+ read: "flags",
379
+ note: "can the reader tell WHICH SET each number counts",
380
+ },
381
+ {
382
+ // `tighten-paper` owns this row because it is the pass told to pay the ratchet back: a review
383
+ // round that only ever ADDS text is this project's oldest recorded drift, and the one gate whose
384
+ // job is to remove text is the right place to weigh it. Four of the thirteen finding kinds read
385
+ // the WHOLE body at both revisions rather than the round's declared scope — coverage that is a
386
+ // function of what happened to be edited degrades silently, which is documented upstairs.
387
+ skill: "tighten-paper",
388
+ check: "round-diff",
389
+ cmd: (d) => ["node", join(HERE, "round-diff.mjs"), d, "--json"],
390
+ needs: (d) => existsSync(join(d, "rounds")),
391
+ read: "json",
392
+ note: "a review round may only change what it declared; the whole body is weighed every round",
393
+ },
394
+ ];
395
+
396
+ /**
397
+ * Write the check's own output where a reader can open it, and hand back the repo-relative path.
398
+ *
399
+ * 🔴 THIS IS NOT DECORATION. Before today this file recorded findings with NO report path, and
400
+ * `record()` now refuses that — correctly, and for a reason measured on real rows: four of six
401
+ * runs on 2026-08-07 stored a count and dropped the location, leaving thirty-six findings that
402
+ * were a number and nothing else. A count with no location is a rumour, and a rumour gets
403
+ * re-derived from scratch next session. The frontmatter states the count so the ledger and the
404
+ * report can be checked against each other, which `record()` does.
405
+ */
406
+ function writeReport(dir, g, count, body) {
407
+ const rel = join("reviews", "mechanical", `${g.skill}--${g.check}.md`);
408
+ const abs = join(dir, rel);
409
+ mkdirSync(dirname(abs), { recursive: true });
410
+ writeFileSync(
411
+ abs,
412
+ [
413
+ "---",
414
+ `title: "${g.skill}/${g.check} — mechanical check output"`,
415
+ `created: ${new Date().toISOString().slice(0, 10)}`,
416
+ `findings: ${count}`,
417
+ "---",
418
+ "",
419
+ `Written by \`run-mechanical.mjs\`. What this check looks at: ${g.note}`,
420
+ "",
421
+ "```",
422
+ body.trimEnd() || "(the check produced no output)",
423
+ "```",
424
+ "",
425
+ ].join("\n"),
426
+ );
427
+ return rel;
428
+ }
429
+
430
+ function main(argv) {
431
+ const dir = resolve(argv[2] || ".");
432
+ if (!existsSync(join(dir, "paper.md"))) {
433
+ console.error(`no paper.md in ${dir}`);
434
+ return 2;
435
+ }
436
+
437
+ // A duplicate key would silently restore the exact defect this file was refactored to remove,
438
+ // so it is checked here rather than trusted to review. Cheap, and it cannot be forgotten.
439
+ const keys = GATES.map((g) => `${g.skill}/${g.check}`);
440
+ const dupe = keys.find((k, i) => keys.indexOf(k) !== i);
441
+ if (dupe) {
442
+ console.error(
443
+ `two checks share the row key "${dupe}" — they would take turns being the answer`,
444
+ );
445
+ return 2;
446
+ }
447
+
448
+ let worst = 0;
449
+ for (const g of GATES) {
450
+ const key = `${g.skill}/${g.check}`;
451
+ const abstain = (reason, note) => {
452
+ console.log(
453
+ `\n⬜ ${key} — abstained (${reason}): ${ABSTENTIONS.get(reason)}`,
454
+ );
455
+ record({
456
+ skill: g.skill,
457
+ check: g.check,
458
+ paper: dir,
459
+ kind: "ABSTAINED",
460
+ reason,
461
+ note: note ?? g.note,
462
+ });
463
+ };
464
+
465
+ if (g.needs && !g.needs(dir)) {
466
+ abstain("input-missing");
467
+ continue;
468
+ }
469
+
470
+ // `out` with no initial value: try assigns it, catch assigns it below (2026-08-28)
471
+ let out,
472
+ code = 0,
473
+ crashed = false;
474
+ try {
475
+ out = execFileSync(g.cmd(dir)[0], g.cmd(dir).slice(1), {
476
+ cwd: ROOT,
477
+ encoding: "utf8",
478
+ stdio: ["ignore", "pipe", "pipe"],
479
+ });
480
+ } catch (e) {
481
+ if (e.code === "ENOENT") {
482
+ crashed = true;
483
+ }
484
+ code = e.status ?? 1;
485
+ out = (e.stdout || "") + (e.stderr || "");
486
+ }
487
+ if (crashed) {
488
+ abstain("crashed", `${g.cmd(dir)[0]} is not installed`);
489
+ continue;
490
+ }
491
+
492
+ const lines = out.split("\n").filter((l) => l.trim()).length;
493
+ let count,
494
+ blocking = false;
495
+ if (g.read === "exit") {
496
+ count = code === 0 ? 0 : Math.max(1, lines);
497
+ blocking = code !== 0;
498
+ } else if (g.read === "eslint") {
499
+ // Three ways in which ESLint can NOT check a file, and all three look like a clean run if
500
+ // you read only the length of the filtered list. Each is routed into its own ledger row,
501
+ // because "did not check" and "checked and found nothing" are different facts.
502
+ let parsed;
503
+ try {
504
+ parsed = JSON.parse(out);
505
+ } catch {
506
+ parsed = null;
507
+ }
508
+ // 1. not JSON at all: a broken config, a glob without a single file (`No files matching the
509
+ // pattern … were found`, exit 2), a missing binary. All of that goes to stderr.
510
+ if (!Array.isArray(parsed)) {
511
+ abstain(
512
+ "crashed",
513
+ `eslint did not return JSON: ${(out.trim().split("\n")[0] || "(empty output)").slice(0, 160)}`,
514
+ );
515
+ continue;
516
+ }
517
+ const msgs = parsed.flatMap((f) =>
518
+ f.messages.map((m) => ({ ...m, filePath: f.filePath })),
519
+ );
520
+ // 2. a rule itself crashed on this file. `fatal` arrives with `ruleId: null`, that is, after
521
+ // the filter it disappears without a trace and reads as "no findings".
522
+ const fatal = msgs.find((m) => m.fatal);
523
+ if (fatal) {
524
+ abstain("crashed", `eslint crashed on the file: ${fatal.message}`);
525
+ continue;
526
+ }
527
+ // 3. NOT A SINGLE rule applied to the file ("File ignored because outside of base path" is
528
+ // how a fixture in a temporary directory looks, "…because no matching configuration" is a
529
+ // file outside the config's glob). Zero findings here means "nobody looked".
530
+ const ignored = msgs.find(
531
+ (m) => !m.ruleId && /^File ignored/.test(m.message),
532
+ );
533
+ if (ignored) {
534
+ abstain(
535
+ "input-missing",
536
+ `eslint applied not a single rule to the file: "${ignored.message}"`,
537
+ );
538
+ continue;
539
+ }
540
+ const mine = msgs.filter((m) => g.rules.has(m.ruleId));
541
+ count = mine.length;
542
+ // severity 2 means the rules declared binary in `eslint.config.mjs` ("a reference either
543
+ // resolves or it does not"), that is, a FACT in this file's vocabulary. Thresholds chosen by
544
+ // a human are declared `warn` and stay judgemental.
545
+ blocking = mine.some((m) => m.severity === 2);
546
+ out = mine
547
+ .map(
548
+ (m) =>
549
+ `${m.filePath}:${m.line}:${m.column} ${m.severity === 2 ? "error" : "warning"} ${m.ruleId} ${m.message}`,
550
+ )
551
+ .join("\n");
552
+ } else if (g.read === "json") {
553
+ let parsed;
554
+ try {
555
+ parsed = JSON.parse(out);
556
+ } catch {
557
+ parsed = null;
558
+ }
559
+ if (!Array.isArray(parsed)) {
560
+ abstain(
561
+ "crashed",
562
+ "output was not the JSON array of findings it documents",
563
+ );
564
+ continue;
565
+ }
566
+ count = parsed.length;
567
+ out = JSON.stringify(parsed, null, 2);
568
+ } else {
569
+ // `flags` mode prints a header line even when clean, so one line is not a finding.
570
+ //
571
+ // 🔴 PREFER THE CHECK'S OWN COUNT WHEN IT STATES ONE. Counting output lines is what this file
572
+ // did before 2026-08-10, and writing the report out made the consequence visible on the page:
573
+ // `check-provenance` printed "7 finding(s)" in a report whose frontmatter said 13, because
574
+ // thirteen was the number of LINES. The ledger and the report agreed with each other and both
575
+ // disagreed with the checker — which is the exact "two numbers, one already wrong" state the
576
+ // evidence contract exists to catch, sitting one level up where the contract could not see it.
577
+ // Lines remain the fallback for checks that state nothing.
578
+ const stated = /—\s*(\d+)\s+finding\(s\)/.exec(out);
579
+ count = stated ? Number(stated[1]) : lines > 1 ? lines : 0;
580
+ }
581
+
582
+ if (count === 0) {
583
+ // 🔴 NOT a pass. The check ran and produced no finding, and it has no witness for the
584
+ // negative — so it abstains and the reader derives cleanliness from the absence, if they
585
+ // want to claim it.
586
+ console.log(`\n🤍 ${key} — no finding recorded (abstained: no-witness)`);
587
+ console.log(` ${g.note}`);
588
+ record({
589
+ skill: g.skill,
590
+ check: g.check,
591
+ paper: dir,
592
+ kind: "ABSTAINED",
593
+ reason: "no-witness",
594
+ note: g.note,
595
+ });
596
+ continue;
597
+ }
598
+
599
+ const report = writeReport(dir, g, count, out);
600
+ console.log(
601
+ `\n${blocking ? "🔴" : "🟠"} ${key} — ${count} finding(s) → ${report}`,
602
+ );
603
+ console.log(` ${g.note}`);
604
+ console.log(
605
+ out
606
+ .split("\n")
607
+ .slice(0, 12)
608
+ .map((l) => " " + l)
609
+ .join("\n"),
610
+ );
611
+ record({
612
+ skill: g.skill,
613
+ check: g.check,
614
+ paper: dir,
615
+ kind: "FINDING",
616
+ findings: count,
617
+ report,
618
+ blocking,
619
+ note: g.note,
620
+ });
621
+ if (blocking) worst = 1;
622
+ }
623
+
624
+ console.log(
625
+ `\n${worst === 0 ? "🟢 no FACT check recorded a blocking finding" : "🔴 a FACT check found something — see above"}`,
626
+ );
627
+ console.log(
628
+ ` Judgement checks report and never fail the run; read them, and never grep this for green.\n`,
629
+ );
630
+ return worst;
631
+ }
632
+
633
+ if (isMain(import.meta.url)) process.exit(main(process.argv));