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,881 @@
1
+ /**
2
+ * round-diff.harness.mjs — plant the defect each check in `round-diff.mjs` claims to catch, assert
3
+ * it says no with the RIGHT WORDS, and assert it stays silent on the clean case. `npx vigiles test`.
4
+ *
5
+ * WHAT IS UNDER TEST. `.claude/skills/paper-pipeline/scripts/round-diff.mjs`: a review round may only
6
+ * change what its manifest declared, and the whole paper is weighed against a declared budget every
7
+ * round. Taken from ARIS's edit whitelist plus its per-round diff gate (`wanshuiyin/
8
+ * Auto-claude-code-research-in-sleep`, MIT, HEAD 2a23847), adapted from LaTeX+`forbidden_operations`
9
+ * to a markdown source. The failure it is aimed at is ours and recorded: one session's panel plus
10
+ * seven overclaim fixes deposited about a page of hedging that nobody had authorised
11
+ * (`paper-pipeline/references/review-ratchet.md`).
12
+ *
13
+ * 🔴 WHY A HARNESS AT ALL. `status.mjs` flags any gate that has run and never returned a negative,
14
+ * on the mutation-testing principle that a test which kills no mutant is not a test. On the live
15
+ * paper this check reports one finding (`no-round-ledger` — there are no rounds on file yet), so the
16
+ * live corpus cannot distinguish "the other twelve checks work" from "the other twelve checks are
17
+ * dead code". Its evidence of life is here, and its evidence that THIS FILE has teeth is
18
+ * `round-diff.mutations.mjs`.
19
+ *
20
+ * 🔴 ASSERTIONS RUN AT MODULE TOP LEVEL. `vigiles test` imports the file and treats "did not throw"
21
+ * as a pass; an earlier harness in this directory exported a `tests` object, nothing ran, and the
22
+ * runner printed ✓ over `assert.equal(1, 2)`. Straight-line code that throws.
23
+ *
24
+ * 🔴 THIS CLAIMED «known-red failures are deferred via `soft()` and re-thrown at the end, so one
25
+ * open finding will not slip past the checks behind it» — AND THAT WAS FALSE (found via
26
+ * `no-unused-vars` 2026-08-28). `soft()` was declared and NEVER CALLED ONCE: `deferred` is always
27
+ * empty, the re-throw block at the end is unreachable, and the first failed assertion cuts off the rest —
28
+ * exactly the behaviour the paragraph claimed was eliminated. The mechanism was deleted along with the
29
+ * promise; putting it back means wrapping the assertions themselves in `soft()` (490 lines), and that is
30
+ * separate work, not a side effect of the lint fix. The hole is named, not swept under.
31
+ *
32
+ * 🔴 EVERY ASSERTION NAMES A MESSAGE, NOT ONLY A KIND. Two vacuous assertions were found in this
33
+ * repository in one week, and one of them was exactly this: a check was deleted, a later branch
34
+ * produced the same finding KIND, and the test stayed green. `only()` below therefore demands the
35
+ * exact finding set and `msg()` demands the substring that only the intended branch can produce.
36
+ *
37
+ * FIXTURES ARE REAL GIT REPOSITORIES in a temp dir. The gate resolves its base with
38
+ * `git show <rev>:<path>`, so a fixture that faked that would test a different program. Nothing here
39
+ * touches the repository this file lives in.
40
+ */
41
+ import assert from "node:assert/strict";
42
+ import { spawnSync } from "node:child_process";
43
+ import {
44
+ mkdtempSync,
45
+ mkdirSync,
46
+ writeFileSync,
47
+ rmSync,
48
+ realpathSync,
49
+ } from "node:fs";
50
+ import { tmpdir } from "node:os";
51
+ import { join, dirname } from "node:path";
52
+ import { fileURLToPath } from "node:url";
53
+ const HERE = dirname(fileURLToPath(import.meta.url));
54
+
55
+ const SCRIPT = join(HERE, "round-diff.mjs");
56
+ const tmp = realpathSync(mkdtempSync(join(tmpdir(), "round-diff-harness-")));
57
+
58
+ // ── the fixture paper ────────────────────────────────────────────────────────────────────────
59
+ //
60
+ // Small, but shaped like the real one: frontmatter, HTML-comment bookkeeping between sections, a
61
+ // numbered body, a `## References` boundary and an appendix after it. The boundary is load-bearing
62
+ // — the ratchet must not charge for words moved into an appendix.
63
+
64
+ const P = (o = {}) => `---
65
+ title: "A fixture"
66
+ ---
67
+
68
+ # A fixture
69
+
70
+ ## Abstract
71
+
72
+ ${o.abstract ?? "We measure a thing and report **41.0%** of cases."}
73
+
74
+ ## 1. Introduction
75
+
76
+ ${o.s1 ?? "Repositories ban things in prose. Nobody checks whether the ban is switched on [7]."}
77
+
78
+ ## 2. Method
79
+
80
+ <!-- TIGHTEN 2026-08-01 · KEEP · carries: the method -->
81
+
82
+ ${o.s2 ?? "We read 134 configurations and compared each against its own rules file."}
83
+
84
+ ### 2.1 The reader
85
+
86
+ ${o.s21 ?? "The reader resolves the configuration the project's linter would resolve."}
87
+
88
+ ## 3. Results
89
+
90
+ ${o.s3 ?? "The direction holds across all four rules."}
91
+
92
+ ## Limitations
93
+
94
+ ${o.lim ?? "The sample is one ecosystem."}
95
+
96
+ ## References
97
+
98
+ 1. A. Author. *A Title.* arXiv:2101.00001, 2021.
99
+ ${o.refs ?? ""}
100
+ ## Appendix A. Extra
101
+
102
+ ${o.appendix ?? "Detail that did not fit."}
103
+ `;
104
+
105
+ let n = 0;
106
+ /** A git repo with `paper.md` committed at HEAD, plus whatever the round wants on top. */
107
+ function fixture({
108
+ base = P(),
109
+ now = null,
110
+ rounds = {},
111
+ commitRounds = false,
112
+ } = {}) {
113
+ const dir = join(tmp, `f${++n}`, "paper-x");
114
+ mkdirSync(dir, { recursive: true });
115
+ const g = (...a) => spawnSync("git", a, { cwd: dir, encoding: "utf8" });
116
+ g("init", "-q", "-b", "main");
117
+ g("config", "user.email", "h@example.com");
118
+ g("config", "user.name", "harness");
119
+ writeFileSync(join(dir, "paper.md"), base);
120
+ g("add", "-A");
121
+ g("commit", "-qm", "base");
122
+ const head = g("rev-parse", "HEAD").stdout.trim();
123
+
124
+ const write = () => {
125
+ if (Object.keys(rounds).length) {
126
+ mkdirSync(join(dir, "rounds"), { recursive: true });
127
+ for (const [f, body] of Object.entries(rounds)) {
128
+ writeFileSync(
129
+ join(dir, "rounds", f),
130
+ body.replaceAll("__BASE__", head),
131
+ );
132
+ }
133
+ }
134
+ };
135
+ if (commitRounds) {
136
+ write();
137
+ g("add", "-A");
138
+ g("commit", "-qm", "rounds");
139
+ }
140
+ if (now !== null) writeFileSync(join(dir, "paper.md"), now);
141
+ if (!commitRounds) write();
142
+ return { dir, head };
143
+ }
144
+
145
+ /** Round manifest text. `touches` is required by the gate, so it is required here too. */
146
+ const M = ({
147
+ round = 1,
148
+ touches = ["3"],
149
+ allows,
150
+ budget,
151
+ hedge,
152
+ closed,
153
+ base = "__BASE__",
154
+ } = {}) =>
155
+ `---
156
+ round: ${round}
157
+ opened: 2026-08-10
158
+ base: ${base}
159
+ touches: [${touches.map((t) => `"${t}"`).join(", ")}]
160
+ ${allows ? `allows: [${allows.join(", ")}]\n` : ""}${budget !== undefined ? `budget: ${budget}\n` : ""}${hedge !== undefined ? `hedge-budget: ${hedge}\n` : ""}${closed ? `closed: ${closed}\n` : ""}---
161
+
162
+ Round ${round}.
163
+ `;
164
+
165
+ function run(dir, ...args) {
166
+ const json = spawnSync("node", [SCRIPT, dir, "--json", ...args], {
167
+ encoding: "utf8",
168
+ });
169
+ const text = spawnSync("node", [SCRIPT, dir, ...args], { encoding: "utf8" });
170
+ assert.equal(
171
+ json.status,
172
+ 0,
173
+ `the gate must always exit 0 (advisory); it exited ${json.status}:\n${json.stderr}`,
174
+ );
175
+ let findings;
176
+ try {
177
+ findings = JSON.parse(json.stdout);
178
+ } catch {
179
+ throw new Error(
180
+ `--json did not produce JSON.\nstdout:\n${json.stdout}\nstderr:\n${json.stderr}`,
181
+ );
182
+ }
183
+ return {
184
+ findings,
185
+ kinds: findings.map((f) => f.kind),
186
+ text: text.stdout + text.stderr,
187
+ };
188
+ }
189
+
190
+ /** Exactly these kinds, in any order — never `.includes`, which is how a deleted check stays green. */
191
+ const only = (r, expected, why) =>
192
+ assert.deepEqual(
193
+ [...r.kinds].sort(),
194
+ [...expected].sort(),
195
+ `${why}\n got: ${JSON.stringify(r.findings, null, 2)}`,
196
+ );
197
+
198
+ /** The finding of this kind must say this. A kind alone does not prove which branch produced it. */
199
+ const msg = (r, kind, needle, why) => {
200
+ const f = r.findings.find((x) => x.kind === kind);
201
+ assert.ok(
202
+ f,
203
+ `${why} — no finding of kind ${kind}. got: ${r.kinds.join(", ") || "none"}`,
204
+ );
205
+ assert.ok(
206
+ f.msg.includes(needle),
207
+ `${why} — the ${kind} message must contain ${JSON.stringify(needle)}.\n got: ${f.msg}`,
208
+ );
209
+ };
210
+
211
+ // ═══ 1. the clean case: a round that did what it said, and the gate says nothing ══════════════
212
+ //
213
+ // First and load-bearing. A check that fires on correct input is muted inside a day, which is worse
214
+ // than one that misses — and every assertion below is worthless if this one does not hold, because
215
+ // then the findings prove only that the gate is noisy.
216
+ {
217
+ const f = fixture({
218
+ now: P({
219
+ s3: "The direction holds across all four rules. It also holds when we drop the largest repository.",
220
+ }),
221
+ rounds: { "01.md": M({ touches: ["3"], budget: 20 }) },
222
+ });
223
+ const r = run(f.dir);
224
+ only(
225
+ r,
226
+ [],
227
+ "a round that changed only its declared section, inside budget, with no new cite or number, must be silent",
228
+ );
229
+ assert.match(
230
+ r.text,
231
+ /SILENT ABOUT/,
232
+ "the run must always print what it cannot see. A gate that speaks only when it fires teaches the reader to hear silence as coverage — the exact failure of 2026-08-05, where four defects sat under four tools that were all silent by construction.",
233
+ );
234
+ assert.match(
235
+ r.text,
236
+ /body \d+w \(\+\d+\)/,
237
+ "the census prints the whole-document body delta on every run, findings or none",
238
+ );
239
+ }
240
+
241
+ // ═══ 2. no ledger at all — the live state of every paper in this repo ═════════════════════════
242
+ {
243
+ const f = fixture({ now: P({ s3: "Changed." }) });
244
+ const r = run(f.dir);
245
+ only(
246
+ r,
247
+ ["no-round-ledger"],
248
+ "a paper with no rounds/ directory must say so rather than pass",
249
+ );
250
+ msg(
251
+ r,
252
+ "no-round-ledger",
253
+ "measured against nothing",
254
+ "the message must name what is unmeasured, not merely that a directory is absent",
255
+ );
256
+ msg(
257
+ r,
258
+ "no-round-ledger",
259
+ "body words",
260
+ "and it must carry the current census, so the first round has a number to budget from",
261
+ );
262
+ }
263
+
264
+ // ═══ 3. undeclared-section: the round touched what it did not declare ════════════════════════
265
+ {
266
+ const f = fixture({
267
+ now: P({
268
+ s3: "The direction holds across all four rules.",
269
+ lim: "The sample is one ecosystem. It is also one language.",
270
+ }),
271
+ rounds: { "01.md": M({ touches: ["3"], budget: 50 }) },
272
+ });
273
+ const r = run(f.dir);
274
+ only(
275
+ r,
276
+ ["undeclared-section"],
277
+ "a change to Limitations under a manifest declaring only §3 is the whole point of the gate",
278
+ );
279
+ msg(
280
+ r,
281
+ "undeclared-section",
282
+ "Limitations",
283
+ "the finding must name the section, or nobody can act on it",
284
+ );
285
+ msg(
286
+ r,
287
+ "undeclared-section",
288
+ "+5 words",
289
+ "and the size of the change, so a typo fix reads differently from a paragraph",
290
+ );
291
+ }
292
+
293
+ // ═══ 4. a new section, and a deleted one, are both undeclared changes ════════════════════════
294
+ {
295
+ const f = fixture({
296
+ now: P().replace(
297
+ "## Limitations",
298
+ "## Threats to validity\n\nNew.\n\n## Limitations",
299
+ ),
300
+ rounds: { "01.md": M({ touches: ["3"], budget: 50 }) },
301
+ });
302
+ const r = run(f.dir);
303
+ only(r, ["undeclared-section"], "an added section is an undeclared change");
304
+ msg(
305
+ r,
306
+ "undeclared-section",
307
+ "is new",
308
+ "an added section must read as added, not as a word delta against nothing",
309
+ );
310
+ }
311
+ {
312
+ const f = fixture({
313
+ now: P().replace(/## Limitations\n\nThe sample is one ecosystem\.\n\n/, ""),
314
+ rounds: { "01.md": M({ touches: ["3"], budget: 50 }) },
315
+ });
316
+ const r = run(f.dir);
317
+ only(
318
+ r,
319
+ ["undeclared-section"],
320
+ "a REMOVED section must be caught — a check that only walks the current document cannot see a deletion, which is the direction a ratchet gate is least expected to look",
321
+ );
322
+ msg(
323
+ r,
324
+ "undeclared-section",
325
+ "was removed",
326
+ "and it must say so in those words",
327
+ );
328
+ }
329
+
330
+ // ═══ 5. the declaration's number prefixes nest ═══════════════════════════════════════════════
331
+ {
332
+ const f = fixture({
333
+ now: P({
334
+ s21: "The reader resolves the configuration the project's own linter would resolve.",
335
+ }),
336
+ rounds: { "01.md": M({ touches: ["2"], budget: 50 }) },
337
+ });
338
+ only(
339
+ run(f.dir),
340
+ [],
341
+ "`touches: 2` must cover §2.1 — otherwise every round has to enumerate leaves and nobody will",
342
+ );
343
+ }
344
+ {
345
+ const f = fixture({
346
+ now: P({ s21: "Rewritten entirely." }),
347
+ rounds: { "01.md": M({ touches: ["3"], budget: 50 }) },
348
+ });
349
+ assert.ok(
350
+ run(f.dir).kinds.includes("undeclared-section"),
351
+ "`touches: 3` must NOT cover §2.1 — if prefix matching were substring-only, `3` would match any heading containing a 3 and the declaration would authorise the document",
352
+ );
353
+ }
354
+
355
+ // ═══ 6. ratchet: the whole body against the declared budget ══════════════════════════════════
356
+ {
357
+ const long =
358
+ "The direction holds across all four rules. " +
359
+ "We add a qualifying clause here as well. ".repeat(12);
360
+ const f = fixture({
361
+ now: P({ s3: long }),
362
+ rounds: { "01.md": M({ touches: ["3"], budget: 10 }) },
363
+ });
364
+ const r = run(f.dir);
365
+ only(
366
+ r,
367
+ ["ratchet"],
368
+ "growth past the declared budget is the finding this gate exists for",
369
+ );
370
+ msg(
371
+ r,
372
+ "ratchet",
373
+ "against a declared budget of +10",
374
+ "the finding must quote the budget it was measured against, or it is an opinion about length",
375
+ );
376
+ msg(
377
+ r,
378
+ "ratchet",
379
+ "the appendix moved 0",
380
+ "and it must report where the words did NOT go, because moving text to an appendix is the sanctioned way to pay",
381
+ );
382
+ }
383
+ {
384
+ const long =
385
+ "The direction holds across all four rules. " +
386
+ "We add a qualifying clause here as well. ".repeat(12);
387
+ const f = fixture({
388
+ now: P({ s3: long }),
389
+ rounds: { "01.md": M({ touches: ["3"], budget: 200 }) },
390
+ });
391
+ only(
392
+ run(f.dir),
393
+ [],
394
+ "the same growth under a budget that declared it is not a finding — a budget nobody can spend is a budget nobody will write",
395
+ );
396
+ }
397
+
398
+ // ═══ 7. words moved into the appendix are not growth ═════════════════════════════════════════
399
+ {
400
+ const bulk = "A sentence that will move into the appendix later on. ".repeat(
401
+ 10,
402
+ );
403
+ const base = P({ s3: "The direction holds across all four rules. " + bulk });
404
+ const now = P({
405
+ s3: "The direction holds across all four rules.",
406
+ appendix: "Detail that did not fit. " + bulk,
407
+ });
408
+ const f = fixture({
409
+ base,
410
+ now,
411
+ rounds: { "01.md": M({ touches: ["3", "Appendix"], budget: 0 }) },
412
+ });
413
+ const r = run(f.dir);
414
+ only(
415
+ r,
416
+ [],
417
+ "moving a passage into an appendix must not read as growth: paying by relocation is the rule this project wrote after paying by shaving a neighbouring sentence",
418
+ );
419
+ assert.match(
420
+ r.text,
421
+ /appendix \d+w \(\+\d+\)/,
422
+ "the appendix delta is still reported as a fact, because a paper that empties its body into an appendix should be visible even when it is legal",
423
+ );
424
+ }
425
+ {
426
+ // The other half of the same rule, and the half a MOVE cannot test: text that only ever existed in
427
+ // the appendix. A move keeps the total constant, so a checker that had no body/appendix boundary
428
+ // at all would still pass the fixture above. This one grows the appendix and nothing else.
429
+ const bulk = "A sentence that lives only in the appendix. ".repeat(10);
430
+ const f = fixture({
431
+ now: P({ appendix: "Detail that did not fit. " + bulk }),
432
+ rounds: { "01.md": M({ touches: ["Appendix"], budget: 0 }) },
433
+ });
434
+ only(
435
+ run(f.dir),
436
+ [],
437
+ "appendix growth under a zero body budget must be free — the page limit is a limit on the BODY, and an appendix is where this project is told to pay from",
438
+ );
439
+ }
440
+
441
+ // ═══ 8. 🔴 ratchet-cumulative: the finding a per-round check CANNOT produce ═══════════════════
442
+ //
443
+ // Three rounds of 20 words each. Round 1 overspent (+30) and nobody made it pay the difference back;
444
+ // rounds 2 and 3 were model citizens at +18. Today's open round is inside its budget, so `ratchet`
445
+ // — which sees only this round — has nothing to say, and the paper is 66 words heavier than the
446
+ // ledger ever authorised. This is ARIS's named case ("a small softening at round 1 + another small
447
+ // softening at round 2 can compound into a meaningful framing change") and it is the assertion that
448
+ // proves this gate is not diff-scoped.
449
+ //
450
+ // 🔴 Note the arithmetic, because the first version of this fixture was WRONG and silently proved
451
+ // nothing: with uniform steps, cumulative growth can never exceed the sum of the per-round budgets
452
+ // (n·step ≤ n·budget). A compounding overrun REQUIRES an earlier round to have overspent and never
453
+ // paid it back — which is exactly the failure the cumulative check exists for, and exactly what a
454
+ // per-round gate forgets the moment the round is closed.
455
+ {
456
+ const grow = (k) =>
457
+ P({
458
+ s3:
459
+ "The direction holds across all four rules. " +
460
+ "Another qualifying clause. ".repeat(k),
461
+ });
462
+ const dir = join(tmp, "cumulative", "paper-x");
463
+ mkdirSync(dir, { recursive: true });
464
+ const g = (...a) => spawnSync("git", a, { cwd: dir, encoding: "utf8" });
465
+ g("init", "-q", "-b", "main");
466
+ g("config", "user.email", "h@e.com");
467
+ g("config", "user.name", "h");
468
+ const shas = [];
469
+ for (const k of [0, 10, 16]) {
470
+ writeFileSync(join(dir, "paper.md"), grow(k));
471
+ g("add", "-A");
472
+ g("commit", "-qm", `r${k}`);
473
+ shas.push(g("rev-parse", "HEAD").stdout.trim());
474
+ }
475
+ writeFileSync(join(dir, "paper.md"), grow(22));
476
+ mkdirSync(join(dir, "rounds"), { recursive: true });
477
+ for (const [i, sha] of shas.entries()) {
478
+ writeFileSync(
479
+ join(dir, "rounds", `0${i + 1}.md`),
480
+ M({
481
+ round: i + 1,
482
+ touches: ["3"],
483
+ budget: 20,
484
+ base: sha,
485
+ closed: i < 2 ? "2026-08-1" + i : undefined,
486
+ }),
487
+ );
488
+ }
489
+ const r = run(dir);
490
+ assert.ok(
491
+ r.kinds.includes("ratchet-cumulative"),
492
+ "three rounds each inside a 20-word budget compounding to +66 must be caught. This is the only finding here that ignores what the current round did, and it is the reason this gate is not a diff checker.",
493
+ );
494
+ assert.ok(
495
+ !r.kinds.includes("ratchet"),
496
+ "and the per-round check must be SILENT in this fixture — round 3 spent 18 of its 20. If `ratchet` also fired, this assertion would prove nothing about scope, only that the paper is long.",
497
+ );
498
+ msg(
499
+ r,
500
+ "ratchet-cumulative",
501
+ "across 3 rounds",
502
+ "the finding must name how many rounds it accumulated over",
503
+ );
504
+ msg(
505
+ r,
506
+ "ratchet-cumulative",
507
+ "of declared budget",
508
+ "and the total budget, so the overspend is arithmetic rather than an impression",
509
+ );
510
+ msg(
511
+ r,
512
+ "ratchet-cumulative",
513
+ "inside its own budget",
514
+ "and it must say plainly that every round may have been legal — otherwise it reads as a duplicate of `ratchet` and gets dismissed as one",
515
+ );
516
+ }
517
+
518
+ // ═══ 9. hedge mass over the whole body ═══════════════════════════════════════════════════════
519
+ {
520
+ const hedged =
521
+ "The direction may hold across all four rules, and it could arguably generalise, though it might possibly depend on the sample and perhaps somewhat on the ecosystem.";
522
+ const f = fixture({
523
+ now: P({ s3: hedged }),
524
+ rounds: { "01.md": M({ touches: ["3"], budget: 60 }) },
525
+ });
526
+ const r = run(f.dir);
527
+ only(
528
+ r,
529
+ ["hedge-mass"],
530
+ "hedges deposited by a review round, inside the word budget, must still be caught — that is precisely how a page of hedging arrives unnoticed",
531
+ );
532
+ msg(
533
+ r,
534
+ "hedge-mass",
535
+ "per 1000 body words",
536
+ "the metric must be a DENSITY over the whole body, not a count in the diff",
537
+ );
538
+ msg(
539
+ r,
540
+ "hedge-mass",
541
+ "TIGHTEN → CUT → Threats → inline hedge",
542
+ "and it must name the fix order, because the default response to this finding is to add one more caveat",
543
+ );
544
+ }
545
+ {
546
+ const hedged =
547
+ "The direction may hold across all four rules, and it could arguably generalise, though it might possibly depend on the sample and perhaps somewhat on the ecosystem.";
548
+ // The budget is large because the fixture body is ~60 words, so seven hedges is a swing of ~91 per
549
+ // 1000. On the real paper the same seven would move the density by about 0.8. The number is a
550
+ // property of the fixture, not of the rule.
551
+ const f = fixture({
552
+ now: P({ s3: hedged }),
553
+ rounds: { "01.md": M({ touches: ["3"], budget: 60, hedge: 200 }) },
554
+ });
555
+ only(
556
+ run(f.dir),
557
+ [],
558
+ "a round that declared its hedge budget in advance has paid for it",
559
+ );
560
+ }
561
+
562
+ // ═══ 10. forbidden operations: a citation that entered during a review round ══════════════════
563
+ {
564
+ const f = fixture({
565
+ now: P({
566
+ s3: "The direction holds across all four rules [42].",
567
+ refs: "42. B. Body. *Another Title.* arXiv:2202.00002, 2022.\n",
568
+ }),
569
+ rounds: { "01.md": M({ touches: ["3", "References"], budget: 40 }) },
570
+ });
571
+ const r = run(f.dir);
572
+ only(
573
+ r,
574
+ ["unauthorised-citation"],
575
+ "a cite added mid-round is ARIS's `new_cite`, and it is the operation most likely to skip verify-citations",
576
+ );
577
+ msg(
578
+ r,
579
+ "unauthorised-citation",
580
+ "(42)",
581
+ "the finding must name WHICH citation, or checking it costs a re-read of the diff",
582
+ );
583
+ msg(
584
+ r,
585
+ "unauthorised-citation",
586
+ "verify-citations",
587
+ "and it must name the queue the cite skipped — nine fabricated titles with correct arXiv ids came in exactly this way",
588
+ );
589
+ }
590
+ {
591
+ const f = fixture({
592
+ now: P({
593
+ s3: "The direction holds across all four rules [42].",
594
+ refs: "42. B. Body. *Another Title.* arXiv:2202.00002, 2022.\n",
595
+ }),
596
+ rounds: {
597
+ "01.md": M({
598
+ touches: ["3", "References"],
599
+ budget: 40,
600
+ allows: ["new-citation"],
601
+ }),
602
+ },
603
+ });
604
+ only(
605
+ run(f.dir),
606
+ [],
607
+ "a declared `new-citation` is authorised — the whitelist has to be spendable",
608
+ );
609
+ }
610
+ {
611
+ // ARIS lists `new_bibitem` as a forbidden operation SEPARATE from `new_cite`, and this is why: an
612
+ // entry can enter the bibliography with no inline marker anywhere in the prose. A check that only
613
+ // scanned prose would pass it, and it is the harder case — nothing in the text points at it, so
614
+ // nobody re-reads it either.
615
+ const f = fixture({
616
+ now: P({ refs: "42. B. Body. *Another Title.* arXiv:2202.00002, 2022.\n" }),
617
+ rounds: { "01.md": M({ touches: ["References"], budget: 40 }) },
618
+ });
619
+ const r = run(f.dir);
620
+ assert.ok(
621
+ r.kinds.includes("unauthorised-citation"),
622
+ "a bibliography entry with NO inline marker is still a citation this round added",
623
+ );
624
+ msg(
625
+ r,
626
+ "unauthorised-citation",
627
+ "(42)",
628
+ "and it must be named, since the prose gives no clue it is there",
629
+ );
630
+ }
631
+
632
+ // ═══ 11. forbidden operations: a number that entered during a review round ════════════════════
633
+ {
634
+ const f = fixture({
635
+ now: P({
636
+ s3: "The direction holds across all four rules, at 73.2% of the sample.",
637
+ }),
638
+ rounds: { "01.md": M({ touches: ["3"], budget: 40 }) },
639
+ });
640
+ const r = run(f.dir);
641
+ only(
642
+ r,
643
+ ["unauthorised-number"],
644
+ "ARIS's `numerical_claim`: a figure that appears during a revision round has no provenance row and no registry key",
645
+ );
646
+ msg(
647
+ r,
648
+ "unauthorised-number",
649
+ "73.2%",
650
+ "the finding must print the literal, because the response is to trace it or delete it",
651
+ );
652
+ msg(
653
+ r,
654
+ "unauthorised-number",
655
+ "2026-08-05",
656
+ "and it must connect to the defect class it belongs to, or it reads as pedantry about digits",
657
+ );
658
+ }
659
+ {
660
+ const f = fixture({
661
+ now: P({
662
+ s3: "The direction holds across all four rules. Recall that 41.0% is the headline.",
663
+ }),
664
+ rounds: { "01.md": M({ touches: ["3"], budget: 40 }) },
665
+ });
666
+ only(
667
+ run(f.dir),
668
+ [],
669
+ "RESTATING a number already in the paper must be free. Without this the check fires on every legitimate cross-reference, is muted in a day, and the whole gate dies with it",
670
+ );
671
+ }
672
+ {
673
+ const f = fixture({
674
+ now: P({
675
+ s3: "The direction holds across all four rules, at 73.2% of the sample.",
676
+ }),
677
+ rounds: {
678
+ "01.md": M({ touches: ["3"], budget: 40, allows: ["new-number"] }),
679
+ },
680
+ });
681
+ only(run(f.dir), [], "a declared `new-number` is authorised");
682
+ }
683
+
684
+ // ═══ 12. a base that does not resolve must never read as a clean run ═════════════════════════
685
+ {
686
+ const f = fixture({
687
+ now: P({ s3: "Changed." }),
688
+ rounds: {
689
+ "01.md": M({
690
+ touches: ["3"],
691
+ base: "0000000000000000000000000000000000000000",
692
+ }),
693
+ },
694
+ });
695
+ const r = run(f.dir);
696
+ only(
697
+ r,
698
+ ["unresolvable-base"],
699
+ "a round whose base does not resolve is a round with no gate",
700
+ );
701
+ msg(
702
+ r,
703
+ "unresolvable-base",
704
+ "must not read as a clean run",
705
+ "and the message has to say that outright — an unresolvable base produces zero comparisons, which looks identical to zero problems",
706
+ );
707
+ }
708
+
709
+ // ═══ 13. edits with every round closed — the state that deposited a page of hedging ══════════
710
+ {
711
+ const f = fixture({
712
+ now: P({ s3: "Changed with nothing open." }),
713
+ rounds: {
714
+ "01.md": M({
715
+ touches: ["3"],
716
+ budget: 500,
717
+ hedge: 5,
718
+ closed: "2026-08-10",
719
+ }),
720
+ },
721
+ commitRounds: true,
722
+ });
723
+ const r = run(f.dir);
724
+ assert.ok(
725
+ r.kinds.includes("no-open-round"),
726
+ "a dirty paper.md with every manifest closed means edits authorised by nothing — the literal shape of the recorded failure",
727
+ );
728
+ msg(
729
+ r,
730
+ "no-open-round",
731
+ "authorised by nothing",
732
+ "and it must say so, since 'no open round' alone reads like bookkeeping",
733
+ );
734
+ }
735
+
736
+ // ═══ 14. two open rounds ═════════════════════════════════════════════════════════════════════
737
+ {
738
+ const f = fixture({
739
+ now: P({ s3: "Changed." }),
740
+ rounds: {
741
+ "01.md": M({ round: 1, touches: ["3"], budget: 99 }),
742
+ "02.md": M({ round: 2, touches: ["3"], budget: 99 }),
743
+ },
744
+ });
745
+ const r = run(f.dir);
746
+ assert.ok(
747
+ r.kinds.includes("two-open-rounds"),
748
+ "two open manifests silently pick one base and check every budget against the wrong revision",
749
+ );
750
+ msg(
751
+ r,
752
+ "two-open-rounds",
753
+ "round 1, round 2",
754
+ "and the finding must name them",
755
+ );
756
+ }
757
+
758
+ // ═══ 15. a manifest that declares nothing ════════════════════════════════════════════════════
759
+ {
760
+ const f = fixture({
761
+ now: P({ s3: "Changed." }),
762
+ rounds: { "01.md": '---\nround: 1\ntouches: ["3"]\n---\n' },
763
+ });
764
+ const r = run(f.dir);
765
+ msg(
766
+ r,
767
+ "unreadable-manifest",
768
+ "no `base:`",
769
+ "a manifest with no base must be reported, not skipped — a malformed declaration read as absent is how a gate becomes decorative",
770
+ );
771
+ }
772
+ {
773
+ const f = fixture({
774
+ now: P({ s3: "Changed." }),
775
+ rounds: { "01.md": "---\nround: 1\nbase: __BASE__\n---\n" },
776
+ });
777
+ const r = run(f.dir);
778
+ msg(
779
+ r,
780
+ "unreadable-manifest",
781
+ "authorises nothing",
782
+ "a manifest with no `touches:` list authorises nothing and must say so",
783
+ );
784
+ }
785
+
786
+ // ═══ 16. a declaration that covers the paper is not a declaration ════════════════════════════
787
+ {
788
+ const f = fixture({
789
+ now: P({ s3: "Changed." }),
790
+ rounds: {
791
+ "01.md": M({
792
+ touches: [
793
+ "Abstract",
794
+ "1",
795
+ "2",
796
+ "3",
797
+ "Limitations",
798
+ "References",
799
+ "Appendix",
800
+ ],
801
+ budget: 99,
802
+ }),
803
+ },
804
+ });
805
+ const r = run(f.dir);
806
+ assert.ok(
807
+ r.kinds.includes("overbroad-scope"),
808
+ "declaring most of the document defeats the declaration, and it is the obvious way to make this gate stop complaining",
809
+ );
810
+ msg(
811
+ r,
812
+ "overbroad-scope",
813
+ "of 9 sections",
814
+ "the finding must show the denominator — it is a whole-document count, not a judgement about the list's length",
815
+ );
816
+ }
817
+
818
+ // ═══ 17. HTML comments are bookkeeping, not prose ════════════════════════════════════════════
819
+ {
820
+ const base = P();
821
+ const now = base.replace(
822
+ "<!-- TIGHTEN 2026-08-01 · KEEP · carries: the method -->",
823
+ "<!-- TIGHTEN 2026-08-10 · KEEP · carries: the method, restated at much greater length than before -->",
824
+ );
825
+ const f = fixture({
826
+ base,
827
+ now,
828
+ rounds: { "01.md": M({ touches: ["3"], budget: 0 }) },
829
+ });
830
+ only(
831
+ run(f.dir),
832
+ [],
833
+ "rewriting a section's own TIGHTEN note must change nothing. The real paper carries hundreds of lines of these; if they counted, every round would light the board up and the gate would be turned off within a day",
834
+ );
835
+ }
836
+
837
+ // ═══ 18. a permission nothing reads ══════════════════════════════════════════════════════════
838
+ {
839
+ const f = fixture({
840
+ now: P({ s3: "The direction holds across all four rules." }),
841
+ rounds: { "01.md": M({ touches: ["3"], allows: ["new-theorem-env"] }) },
842
+ });
843
+ const r = run(f.dir);
844
+ assert.ok(
845
+ r.kinds.includes("unknown-op"),
846
+ "an `allows:` entry this gate does not implement must be reported. ARIS carries `new_theorem_env` and we do not; a manifest copied from theirs would otherwise read as granting something",
847
+ );
848
+ msg(
849
+ r,
850
+ "unknown-op",
851
+ "not a permission",
852
+ "and it must say the permission is inert, not merely unknown",
853
+ );
854
+ }
855
+
856
+ // ═══ 19. --since: the ad-hoc mode still weighs the whole document ═════════════════════════════
857
+ {
858
+ const long =
859
+ "The direction holds across all four rules. " +
860
+ "We add a qualifying clause here as well. ".repeat(12);
861
+ const f = fixture({ now: P({ s3: long }) });
862
+ const r = run(f.dir, `--since=${f.head}`);
863
+ assert.ok(
864
+ r.kinds.includes("ratchet"),
865
+ "`--since` must still measure: it is how a paper with no round ledger — every paper in this repo today — finds out what a round would have been charged",
866
+ );
867
+ assert.ok(
868
+ !r.kinds.includes("no-round-ledger"),
869
+ "and it must not also complain about the missing ledger it was explicitly told to work without",
870
+ );
871
+ assert.ok(
872
+ !r.kinds.includes("undeclared-section"),
873
+ "with no manifest there is no declaration to violate; reporting one would be the checker inventing a rule nobody wrote",
874
+ );
875
+ }
876
+
877
+ // ── report ───────────────────────────────────────────────────────────────────────────────────
878
+ rmSync(tmp, { recursive: true, force: true });
879
+ console.log(
880
+ "✓ round-diff.harness — 21 blocks; clean case silent, every check watched failing with its own words",
881
+ );