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,322 @@
1
+ /**
2
+ * tex-build — rules about the BUILT LaTeX paper: about what the preamble declares, not about
3
+ * how the prose is written.
4
+ *
5
+ * ── WHY ITS OWN NAMESPACE, AND NOT `paper/*` ────────────────────────────────
6
+ * In the source corpus the `paper/*` family is declared on BOTH `paper.md` and `paper.tex`,
7
+ * and that parity is pinned by an assertion: "a rule added for markdown must not silently
8
+ * fail to reach LaTeX (or the other way round)". The subject here is the BUILD MODE
9
+ * (`\documentclass[review]`, `printacmref=false`), and in markdown it does not exist: a draft
10
+ * has no mechanical sign saying "this build is the final one". Writing
11
+ * `paper/future-promise: "off"` on markdown would be an exemption justified by "there is no
12
+ * subject", and that same parity assertion requires the rule to be ALIVE on the other side —
13
+ * i.e. the machinery exists but is built for the opposite direction (off on `.tex`, alive on
14
+ * `.md`).
15
+ *
16
+ * Hence a namespace of its own, the same way an external word-counter gets one: it has a word
17
+ * count markdown does not have, we have a preamble markdown does not have. Where the config
18
+ * enumerates non-`paper/*` rules on `.tex`, the list is CLOSED and `tex/future-promise` is
19
+ * named explicitly, so that a new namespace arrives by decision rather than as a side effect
20
+ * of editing a config.
21
+ *
22
+ * ⚠️ THE MEASUREMENT THIS DECISION RESTS ON (source corpus, 2026-09-06 → 2026-09-07): had the
23
+ * rule been declared on `paper.md` it would produce ZERO there TODAY — not one match of
24
+ * `FUTURE_PROMISE_RE` in the markdown draft of the one paper that has one. So the choice is
25
+ * about semantics, not noise: markdown has no input distinguishing a draft from a shipped
26
+ * build, and the rule would fire on a draft's honest promise.
27
+ *
28
+ * ── THE RULE READS THE FILE FROM DISK, NOT THE PROJECTION. This is load-bearing ──
29
+ * The LaTeX language in `latex-language.mjs` hands rules a PROJECTION of `.tex` into a
30
+ * markdown-like text, and the first thing that projection does is blank the whole preamble
31
+ * (`blank(0, preEnd)`) plus `\documentclass` and `\settopmatter` as OPAQUE macros. Both signs
32
+ * of the build mode are therefore ERASED in the projection, and this check is inexpressible
33
+ * over `sourceCode.getText()`. `readFileSync(context.filename)` is the same thing sibling
34
+ * rules in the source corpus do when they read a registry file next to the paper; the only
35
+ * difference is that here the file being read is the linted file itself. The one side effect
36
+ * is named: in an editor with an unsaved buffer the rule judges the saved version. On a CLI
37
+ * run (and in CI) there is no such difference.
38
+ *
39
+ * ── WHAT THE MOVE TO A LINT RULE FIXED BESIDES THE FORM (measured 2026-09-07) ──
40
+ * 🔴 The previous implementation, a script, picked its input like this: look for `paper.md` /
41
+ * `draft.md`, and failing that take `readdirSync(dir).find((f) => f.endsWith(".tex"))` — the
42
+ * FIRST `.tex` in directory order. Measured across the three papers of the source corpus:
43
+ * paper A → a 536-BYTE file holding nine `\def`s with venue numbers. The check, added
44
+ * specifically BECAUSE of paper A, was not reading paper A;
45
+ * paper B → a four-month-old draft, one of SEVEN `.tex` files in that directory (the real
46
+ * `paper.tex` happened to get the same verdict — "review mode" — so the set of
47
+ * findings matched by accident, not by construction);
48
+ * paper C → `paper.tex`, the only `.tex` there; it matched.
49
+ * An ESLint rule runs over the glob declared in the config, so the address stopped depending
50
+ * on `readdirSync` order and on how many drafts happen to lie next to the paper.
51
+ */
52
+ import { readFileSync } from "node:fs";
53
+
54
+ /**
55
+ * A copy rather than an import, for the same reason the prose lexicons are copies: a rule may
56
+ * not depend on the script it is replacing, or removing the check from that script becomes
57
+ * impossible.
58
+ */
59
+ const FUTURE_PROMISE_RE =
60
+ /\b(?:at (?:the )?camera[-\s]?ready|upon acceptance|on acceptance|will be (?:released|published|made (?:publicly )?available|public|open[-\s]?sourced))\b/i;
61
+
62
+ /** The same two signs a render script uses to tell a review build from a camera-ready one. */
63
+ const REVIEW_MODE_RE =
64
+ /^[^%\n]*(?:\\documentclass\[[^\]]*\breview\b|printacmref=false)/m;
65
+
66
+ /** Is this an `acmart` build at all? Outside acmart none of the macros below mean anything. */
67
+ const ACMART_RE = /\\documentclass(?:\s*\[[^\]]*\])?\s*\{acmart\}/;
68
+
69
+ /**
70
+ * acmart's OWN escape hatch for "this is not going to an ACM venue": in `nonacm` mode the
71
+ * class disables the ACM Reference Format itself (`acmart.cls:110-121`, "in 'nonacm' mode we
72
+ * disable the \"ACM Reference Format\""). A build that declares it is not an ACM submission
73
+ * is exempt — that is the supported way to say so, and it is a class option rather than a
74
+ * hand-rolled blanking of the front matter.
75
+ */
76
+ const NONACM_RE = /\\documentclass\s*\[[^\]]*\bnonacm\b/;
77
+
78
+ /**
79
+ * The overrides whose ENTIRE effect is to remove furniture the template puts on page 1.
80
+ * Each entry names what disappears, because a message that only says "do not do this" gets
81
+ * read as style advice.
82
+ */
83
+ const FRONTMATTER_OVERRIDES = [
84
+ {
85
+ re: /\\setcopyright\s*\{\s*none\s*\}/g,
86
+ removes: "the copyright statement (acmart.cls:1857, copyright mode 0)",
87
+ },
88
+ {
89
+ re: /\\renewcommand\s*\*?\s*\{?\s*\\footnotetextcopyrightpermission/g,
90
+ removes:
91
+ "the permission/copyright footnote block on page 1 (acmart.cls:2180)",
92
+ },
93
+ {
94
+ re: /\\(?:this)?pagestyle\s*\{\s*(?:plain|empty)\s*\}/g,
95
+ removes:
96
+ "acmart's own page style, i.e. the running heads and folios it restores at " +
97
+ "\\begin{document} (acmart.cls:2876-2881)",
98
+ },
99
+ ];
100
+
101
+ // ═════════════════════════════════════════════════════════════════════════════
102
+ // future-promise — a shipped build promises what it has already handed over
103
+ // ═════════════════════════════════════════════════════════════════════════════
104
+ // 🔴 Added 2026-08-24, because it would have caught a real blocker hours earlier. An accepted
105
+ // paper printed "(full harness at camera-ready)" in BOTH of its most-read positions — the end
106
+ // of the abstract and the end of the conclusion — while its own Availability paragraph three
107
+ // lines below said the harness was already public. The document contradicted itself two
108
+ // against one, and the losing side was the abstract: exactly the part a digital library
109
+ // indexes and the only part most people read.
110
+ //
111
+ // Worse than a typo: a reviewer had listed it as a weakness — "The experimental harness is not
112
+ // yet available (it is expected for the camera-ready version)" — so the printed camera-ready
113
+ // kept asserting the very thing the reviewer complained about.
114
+ //
115
+ // ⚠️ Here is why it survived, and it generalises. The promise WAS swept out the same day — out
116
+ // of the ARTIFACT (`README:34`, `reproduce.py:166`). Nobody made the same pass over
117
+ // `paper.tex`. One class of defect, two carriers, one of them cleaned. A human pass goes where
118
+ // the human is looking; this check goes to both places, because it does not know where anyone
119
+ // is looking.
120
+ //
121
+ // The scope is DELIBERATELY NARROW, because a noisy gate is a silenced gate:
122
+ // - `.tex` only (see the file header: markdown has no sign of "this build is final");
123
+ // - a review-mode build is EXEMPT, and that is the whole point: during peer review
124
+ // "at camera-ready" is a true and ordinary promise. The defect is only a promise that
125
+ // outlived its own delivery;
126
+ // - LaTeX comments are blanked FIRST: author notes of the form `% camera-ready blocker`
127
+ // legitimately live in the source, and a finding on those would be precisely the false
128
+ // positive that kills a check.
129
+ //
130
+ // SEVERITY is `warn`, and that is an analysis rather than caution. The finding is NOT binary:
131
+ // a promise in a camera-ready is sometimes honest (something genuinely not released yet), and
132
+ // the verdict "does it contradict the Availability paragraph" is made by a human — which is
133
+ // what the message text says. There is also a demonstrable class of false positives: "their
134
+ // replication will be published in 2027" is a sentence about SOMEONE ELSE's work. An `error`
135
+ // that fails on a correct input gets switched off the same day, and after that the binary
136
+ // checks stop being read too.
137
+ const futurePromise = {
138
+ meta: {
139
+ type: "problem",
140
+ docs: {
141
+ description:
142
+ "the build is not in review mode, yet the text promises a future release of something already handed over",
143
+ },
144
+ schema: [],
145
+ messages: {
146
+ promise:
147
+ "this build is NOT in review mode, but the text promises a future release: " +
148
+ "«{{text}}». Check it against Availability: if the thing is already published, the " +
149
+ "paper contradicts itself, and the abstract is the side that loses — the abstract is " +
150
+ "what the digital library indexes. That is exactly how «(full harness at " +
151
+ "camera-ready)» survived a sweep that went through the artifact and never went " +
152
+ "through paper.tex",
153
+ },
154
+ },
155
+ create(context) {
156
+ return {
157
+ root() {
158
+ // 🔴 ONE GUARD, NOT THREE. The first draft had `if (!file || !existsSync(file))`
159
+ // BEFORE the read — and a mutation run on 2026-09-07 showed that removing that line is
160
+ // caught by NOTHING: the `try/catch` around the read covers exactly the same cases (no
161
+ // path, a `<text>` run through stdin, no permission). Two guards doing one job is a
162
+ // guard no mutation can kill, i.e. a dead assertion dressed as robustness.
163
+ // What is left is the read inside `try`: stay quiet rather than judge content we never
164
+ // saw.
165
+ let text;
166
+ try {
167
+ text = readFileSync(context.filename, "utf8");
168
+ } catch {
169
+ return;
170
+ }
171
+ if (REVIEW_MODE_RE.test(text)) return;
172
+ // Comments are blanked by CUTTING THE TAIL of the line, exactly as the previous
173
+ // implementation did. Columns do not shift: a match is only possible to the LEFT of
174
+ // `%`, and the left part of the line stays in place character for character. An
175
+ // escaped `\%` is not a comment.
176
+ const lines = text
177
+ .split("\n")
178
+ .map((l) => l.replace(/(^|[^\\])%.*$/, "$1"));
179
+ lines.forEach((line, i) => {
180
+ const m = line.match(FUTURE_PROMISE_RE);
181
+ if (!m) return;
182
+ const column = m.index + 1;
183
+ context.report({
184
+ loc: {
185
+ start: { line: i + 1, column },
186
+ end: { line: i + 1, column: column + m[0].length },
187
+ },
188
+ messageId: "promise",
189
+ data: { text: m[0] },
190
+ });
191
+ });
192
+ },
193
+ };
194
+ },
195
+ };
196
+
197
+ // ══════════════════════════════════════════════════════════════════════════════
198
+ // acm-frontmatter-override — the build blanks template furniture off page 1
199
+ // ══════════════════════════════════════════════════════════════════════════════
200
+ // 🔴 Added 2026-09-11, from a DESK REJECT. A submission was rejected before any content
201
+ // review with the reason "substantially deviating from the specified template… front page
202
+ // lacks elements included in the template". Its preamble carried three lines whose entire
203
+ // effect is to take elements off the front page:
204
+ //
205
+ // \setcopyright{none}
206
+ // \renewcommand\footnotetextcopyrightpermission[1]{}
207
+ // \pagestyle{plain}
208
+ //
209
+ // The same three lines sit COMMENTED OUT in a sibling paper of the same corpus — the one that
210
+ // was accepted. The safer pattern was already known in the same directory and was not carried
211
+ // over. That is the shape this check exists for: a defect that is invisible in the source,
212
+ // visible only on the rendered first page, and fatal before anyone reads a word.
213
+ //
214
+ // ⚠️ THE RULE IS NOT CONDITIONED ON REVIEW MODE, AND THAT IS A MEASUREMENT, NOT A CHOICE.
215
+ // The obvious framing — "flag these when `\documentclass[review]`, because review mode already
216
+ // handles the front matter" — does not survive reading `acmart.cls`. The `review` option does
217
+ // exactly two things (`acmart.cls:93-99`): it turns on line numbers and sets
218
+ // `\@ACM@printfoliostrue`. It does not touch the copyright statement, the permission footnote
219
+ // or the page style. So the overrides are not fighting review mode; they strip the same
220
+ // furniture in EVERY mode — and stripping it in the camera-ready is worse, because that is
221
+ // the version the digital library keeps.
222
+ //
223
+ // The guards are therefore about the CLASS, not the stage:
224
+ // - `acmart` only. Outside it `\pagestyle{plain}` is an ordinary, correct line;
225
+ // - `nonacm` exempts, because that is acmart's own supported way to say "not an ACM venue",
226
+ // and in that mode the class disables the ACM Reference Format itself (`acmart.cls:110`).
227
+ //
228
+ // 🔴 `printacmref=false` IS DELIBERATELY NOT IN THE SET, though it was in the rejected preamble
229
+ // and it too removes a block from page 1. Two reasons, and the first is structural: the
230
+ // sibling rule in this very file reads `printacmref=false` as a SIGN OF REVIEW MODE
231
+ // (`REVIEW_MODE_RE`). A module where one rule treats a token as a legitimate marker and
232
+ // another calls the same token a defect is a module that contradicts itself, and the reader
233
+ // cannot tell which half to believe. The second is that ACM sanctions it for preprints. If it
234
+ // ever needs catching, it needs catching together with a decision about `REVIEW_MODE_RE` —
235
+ // not as a fourth line in this array.
236
+ //
237
+ // SEVERITY is for the CONSUMER to set, and the two configs differ on purpose: this repository
238
+ // lints fixtures that are broken by construction, a consumer lints a real paper. The finding
239
+ // is binary — the macro is present or it is not — it has a named exemption, and the cost of a
240
+ // miss is a rejection with no content review. In a consumer that is `error`.
241
+ const acmFrontmatterOverride = {
242
+ meta: {
243
+ type: "problem",
244
+ docs: {
245
+ description:
246
+ "an acmart build overrides ACM's front-matter commands, removing template elements from page 1",
247
+ },
248
+ schema: [],
249
+ messages: {
250
+ override:
251
+ "«{{text}}» removes {{removes}} from the front page of an `acmart` build. A submission " +
252
+ "was desk-rejected for exactly this — «substantially deviating from the specified " +
253
+ "template… front page lacks elements included in the template» — before any content " +
254
+ "review happened. The `review` option does not suppress this furniture itself " +
255
+ "(acmart.cls:93-99 turns on line numbers and folios, nothing else), so the line is not " +
256
+ "compensating for the class: it is fighting the template. Delete it. If this paper " +
257
+ "genuinely is not going to an ACM venue, say so the supported way — " +
258
+ "`\\documentclass[…,nonacm]{acmart}` — and the class drops the ACM Reference Format " +
259
+ "on its own",
260
+ },
261
+ },
262
+ create(context) {
263
+ return {
264
+ root() {
265
+ // Same single guard as the sibling rule, and for the same measured reason: a separate
266
+ // `existsSync` before the read is covered by this `catch` in every case it could
267
+ // fire, i.e. it is an assertion no mutation can kill.
268
+ let text;
269
+ try {
270
+ text = readFileSync(context.filename, "utf8");
271
+ } catch {
272
+ return;
273
+ }
274
+ // Comments are blanked BEFORE the guards, not only before the search — unlike the
275
+ // sibling rule, which carries the `^[^%\n]*` prefix inside each pattern instead.
276
+ // It matters here: the accepted sibling paper of the source corpus keeps this exact
277
+ // block commented out as a record of what it used to do, and a `\documentclass` line
278
+ // quoted inside a comment must not arm the rule any more than a commented
279
+ // `\setcopyright{none}` must trip it. Columns do not shift: a match is only possible
280
+ // to the LEFT of `%`, and the left part of the line survives character for character.
281
+ // An escaped `\%` is not a comment.
282
+ const lines = text
283
+ .split("\n")
284
+ .map((l) => l.replace(/(^|[^\\])%.*$/, "$1"));
285
+ const code = lines.join("\n");
286
+ if (!ACMART_RE.test(code)) return;
287
+ if (NONACM_RE.test(code)) return;
288
+ lines.forEach((line, i) => {
289
+ for (const { re, removes } of FRONTMATTER_OVERRIDES) {
290
+ // 🔴 NO `re.lastIndex = 0` HERE, AND THAT IS A MEASUREMENT. These regexes are
291
+ // module-level and `/g`, so they carry `lastIndex` across files, and the obvious
292
+ // defensive line is to reset it. A mutation run on 2026-09-11 removed that line
293
+ // and the harness stayed GREEN — because the loop below always drains the regex,
294
+ // and a `/g` `exec` that returns `null` resets `lastIndex` to 0 by itself. The
295
+ // reset was a guard no mutation could kill, i.e. the same dead assertion dressed
296
+ // as robustness that the sibling rule's single-`try` comment describes. What
297
+ // keeps the property honest instead is an ASSERTION, not a line of code: the
298
+ // harness lints the same input twice in one process and requires the two runs to
299
+ // agree. Add a `break` to this loop and that assertion is what will fail.
300
+ let m;
301
+ while ((m = re.exec(line)) !== null) {
302
+ const column = m.index + 1;
303
+ context.report({
304
+ loc: {
305
+ start: { line: i + 1, column },
306
+ end: { line: i + 1, column: column + m[0].length },
307
+ },
308
+ messageId: "override",
309
+ data: { text: m[0], removes },
310
+ });
311
+ }
312
+ }
313
+ });
314
+ },
315
+ };
316
+ },
317
+ };
318
+
319
+ export default {
320
+ "future-promise": futurePromise,
321
+ "acm-frontmatter-override": acmFrontmatterOverride,
322
+ };
@@ -0,0 +1,258 @@
1
+ /**
2
+ * Mutation battery for `eslint-rules/tex-build.mjs`.
3
+ * Run: `node eslint-rules/tex-build.mutations.mjs` (not `vigiles test` — this is not a harness).
4
+ *
5
+ * 🔴 EVERY MUTATION CARRIES A "THE PATCH LANDED" ASSERTION: the file is re-read FROM DISK, the
6
+ * mutant must be in it, and after the rollback the contents must return byte for byte. In the
7
+ * repository this came from, a green mutation has FOUR TIMES meant not "the defence holds" but
8
+ * "the patch never landed" / "the test is looking somewhere else".
9
+ *
10
+ * WHAT A GREEN HARNESS UNDER A MUTATION MEANS: it is a finding about the TEST, not a conclusion
11
+ * about the defence. Rank the causes in this order — the test greps PROSE ABOUT a value instead
12
+ * of the value · the test looks in the wrong place · the test is the wrong test · the patch did
13
+ * not land · the set is too narrow · the run was already red BEFORE the mutation. The last one
14
+ * is closed by the baseline run below.
15
+ */
16
+ import { readFileSync, writeFileSync } from "node:fs";
17
+ import { execFileSync } from "node:child_process";
18
+
19
+ const RULES = "eslint-rules/tex-build.mjs";
20
+ const HARNESS = "eslint-rules/tex-build.harness.mjs";
21
+
22
+ // Coverage mode — see `scripts/run-mutations.mjs` guard 2 and the block in
23
+ // `lib/mutation-driver.mjs`. Says what this battery can kill, without touching a file.
24
+ if (process.env.MUTATIONS_REPORT_COVERAGE) {
25
+ console.log(`MUTATION-COVERS\t${HARNESS}`);
26
+ process.exit(0);
27
+ }
28
+
29
+ const PRISTINE = readFileSync(RULES, "utf8");
30
+
31
+ const M = [
32
+ [
33
+ "vocabulary/camera-ready",
34
+ "remove the «at camera-ready» branch — the very one the check was added for on 2026-08-24",
35
+ " /\\b(?:at (?:the )?camera[-\\s]?ready|upon acceptance|on acceptance|will be (?:released|published|made (?:publicly )?available|public|open[-\\s]?sourced))\\b/i;",
36
+ " /\\b(?:upon acceptance|on acceptance|will be (?:released|published|made (?:publicly )?available|public|open[-\\s]?sourced))\\b/i; /* MUT */",
37
+ ],
38
+ [
39
+ "vocabulary/will be …",
40
+ "keep only camera-ready — the «will be released/published/public» family dies",
41
+ " /\\b(?:at (?:the )?camera[-\\s]?ready|upon acceptance|on acceptance|will be (?:released|published|made (?:publicly )?available|public|open[-\\s]?sourced))\\b/i;",
42
+ " /\\b(?:at (?:the )?camera[-\\s]?ready)\\b/i; /* MUT */",
43
+ ],
44
+ [
45
+ "review/exemption",
46
+ "stop exempting a review build — a promise made during peer review becomes a finding",
47
+ " if (REVIEW_MODE_RE.test(text)) return;",
48
+ " if (false) return; /* MUT */",
49
+ ],
50
+ [
51
+ "review/anchor against comments",
52
+ "drop `^[^%\\n]*` — a COMMENTED-OUT `\\documentclass[review]` starts exempting the build " +
53
+ "(a live case in the source corpus: a paper carrying its previous class a few lines above the real one)",
54
+ " /^[^%\\n]*(?:\\\\documentclass\\[[^\\]]*\\breview\\b|printacmref=false)/m;",
55
+ " /(?:\\\\documentclass\\[[^\\]]*\\breview\\b|printacmref=false)/m; /* MUT */",
56
+ ],
57
+ [
58
+ "review/printacmref",
59
+ "remove the second sign of the mode — a build with `printacmref=false` stops being exempt",
60
+ " /^[^%\\n]*(?:\\\\documentclass\\[[^\\]]*\\breview\\b|printacmref=false)/m;",
61
+ " /^[^%\\n]*(?:\\\\documentclass\\[[^\\]]*\\breview\\b)/m; /* MUT */",
62
+ ],
63
+ [
64
+ "comments/blanking",
65
+ "stop cutting LaTeX comments — an author note `% camera-ready blocker` becomes a finding",
66
+ ' .map((l) => l.replace(/(^|[^\\\\])%.*$/, "$1"));\n lines.forEach((line, i) => {',
67
+ " .map((l) => l); /* MUT */\n lines.forEach((line, i) => {",
68
+ ],
69
+ [
70
+ "comments/escaped percent",
71
+ "treat `\\%` as the start of a comment — the rest of the line behind the percent goes invisible",
72
+ ' .map((l) => l.replace(/(^|[^\\\\])%.*$/, "$1"));\n lines.forEach((line, i) => {',
73
+ ' .map((l) => l.replace(/%.*$/, "")); /* MUT */\n lines.forEach((line, i) => {',
74
+ ],
75
+ [
76
+ "address/line",
77
+ "report every finding on line 1 — the address, which is what the move to a lint rule bought, is lost",
78
+ " loc: {\n start: { line: i + 1, column },",
79
+ " loc: {\n start: { line: 1, column: 1 }, /* MUT */",
80
+ ],
81
+ [
82
+ "address/column",
83
+ "report the start of the line instead of the start of the match",
84
+ " if (!m) return;\n const column = m.index + 1;",
85
+ " if (!m) return;\n const column = 1; /* MUT */",
86
+ ],
87
+ [
88
+ "capture/finding text",
89
+ "print the whole line in the message instead of the captured substring — «how many» would still match, «what» would not",
90
+ " data: { text: m[0] },",
91
+ " data: { text: line.trim() }, /* MUT */",
92
+ ],
93
+ // ── tex/acm-frontmatter-override ───────────────────────────────────────────
94
+ [
95
+ "frontmatter/acmart guard",
96
+ "drop the `acmart` guard — `\\pagestyle{plain}` in an ordinary `article` becomes a finding, " +
97
+ "which is the false positive that gets a rule switched off on its first day",
98
+ " if (!ACMART_RE.test(code)) return;",
99
+ " if (false) return; /* MUT */",
100
+ ],
101
+ [
102
+ "frontmatter/nonacm exemption",
103
+ "stop exempting `nonacm` — the rule punishes the very escape hatch its own message recommends",
104
+ " if (NONACM_RE.test(code)) return;",
105
+ " if (false) return; /* MUT */",
106
+ ],
107
+ [
108
+ "frontmatter/setcopyright",
109
+ "remove `\\setcopyright{none}` from the set — the first of the three lines of the " +
110
+ "desk-rejected preamble stops being caught",
111
+ " re: /\\\\setcopyright\\s*\\{\\s*none\\s*\\}/g,",
112
+ " re: /\\\\setcopyright\\s*\\{\\s*NEVER\\s*\\}/g, /* MUT */",
113
+ ],
114
+ [
115
+ "frontmatter/permission footnote",
116
+ "remove the `\\renewcommand\\footnotetextcopyrightpermission` branch — the permission block " +
117
+ "on page 1 can be blanked silently",
118
+ " re: /\\\\renewcommand\\s*\\*?\\s*\\{?\\s*\\\\footnotetextcopyrightpermission/g,",
119
+ " re: /\\\\renewcommand\\s*\\*?\\s*\\{?\\s*\\\\NEVERfootnotetext/g, /* MUT */",
120
+ ],
121
+ [
122
+ "frontmatter/pagestyle empty",
123
+ "narrow the page-style branch to `plain` — `\\pagestyle{empty}` strips the same furniture and " +
124
+ "goes unnoticed",
125
+ " re: /\\\\(?:this)?pagestyle\\s*\\{\\s*(?:plain|empty)\\s*\\}/g,",
126
+ " re: /\\\\(?:this)?pagestyle\\s*\\{\\s*(?:plain)\\s*\\}/g, /* MUT */",
127
+ ],
128
+ [
129
+ "frontmatter/thispagestyle",
130
+ "drop `(?:this)?` — `\\thispagestyle{empty}`, which targets page 1 SPECIFICALLY, stops being caught",
131
+ " re: /\\\\(?:this)?pagestyle\\s*\\{\\s*(?:plain|empty)\\s*\\}/g,",
132
+ " re: /\\\\pagestyle\\s*\\{\\s*(?:plain|empty)\\s*\\}/g, /* MUT */",
133
+ ],
134
+ [
135
+ "frontmatter/comment blanking",
136
+ "stop cutting LaTeX comments — the accepted sibling paper's COMMENTED record of its old " +
137
+ "preamble becomes three findings, and the arming `\\documentclass` may come from a comment too",
138
+ ' .map((l) => l.replace(/(^|[^\\\\])%.*$/, "$1"));\n const code = lines.join("\\n");',
139
+ ' .map((l) => l);\n const code = lines.join("\\n"); /* MUT */',
140
+ ],
141
+ // ⚠️ `frontmatter/lastIndex reset` STOOD HERE AND WAS REMOVED, not silenced. It mutated away
142
+ // an explicit `re.lastIndex = 0` and the harness stayed GREEN — which by this file's own
143
+ // ranking is a finding about the TEST or about the CODE, never a pass. It was the code: a
144
+ // `/g` `exec` that returns `null` resets `lastIndex` itself, and the loop always drains the
145
+ // regex, so the line was a guard nothing could kill. The line is gone; the property is held
146
+ // by the harness assertion that lints one input twice in a single process.
147
+ [
148
+ "frontmatter/line",
149
+ "report every finding on line 1 — the address of the offending preamble line is lost",
150
+ " start: { line: i + 1, column },",
151
+ " start: { line: 1, column: 1 }, /* MUT */",
152
+ ],
153
+ [
154
+ "frontmatter/all matches per line",
155
+ "take only the FIRST match of each pattern per line — a compacted preamble is under-counted",
156
+ " while ((m = re.exec(line)) !== null) {",
157
+ " if ((m = re.exec(line)) !== null) { /* MUT */",
158
+ ],
159
+ [
160
+ "frontmatter/column",
161
+ "report the start of the line instead of the start of the match (second rule's copy)",
162
+ " const column = m.index + 1;",
163
+ " const column = 1; /* MUT */",
164
+ ],
165
+ [
166
+ "disk read/quiet instead of throwing",
167
+ "remove the `try/catch` around the read — the rule THROWS on a stdin run where there is no file",
168
+ ' try {\n text = readFileSync(context.filename, "utf8");\n } catch {\n return;\n }\n if (REVIEW_MODE_RE.test(text)) return;',
169
+ ' text = readFileSync(context.filename, "utf8"); /* MUT */\n if (REVIEW_MODE_RE.test(text)) return;',
170
+ ],
171
+ ];
172
+
173
+ const runHarness = () => {
174
+ try {
175
+ execFileSync("npx", ["vigiles", "test", HARNESS], {
176
+ encoding: "utf8",
177
+ stdio: ["ignore", "pipe", "pipe"],
178
+ });
179
+ return { failed: false, out: "" };
180
+ } catch (e) {
181
+ return { failed: true, out: (e.stdout ?? "") + (e.stderr ?? "") };
182
+ }
183
+ };
184
+
185
+ // 🔴 A BASELINE RUN BEFORE ANY MUTATION. A red harness makes EVERY mutation look "killed", and
186
+ // the battery would print a confident green verdict about a defence that is not there.
187
+ {
188
+ const base = runHarness();
189
+ if (base.failed) {
190
+ console.log(
191
+ "❌ THE HARNESS IS RED BEFORE ANY MUTATION — the battery cannot tell a killed mutation " +
192
+ "from that:\n" +
193
+ base.out.slice(-1500),
194
+ );
195
+ process.exit(1);
196
+ }
197
+ }
198
+
199
+ let ok = 0;
200
+ let bad = 0;
201
+ const rows = [];
202
+ for (const [label, what, from, to] of M) {
203
+ const hits = PRISTINE.split(from).length - 1;
204
+ if (hits !== 1) {
205
+ console.log(
206
+ `❌ ${label}: TARGET ${hits === 0 ? "NOT FOUND" : `NOT UNIQUE (${hits})`} — ${from.slice(0, 70)}`,
207
+ );
208
+ bad++;
209
+ continue;
210
+ }
211
+ writeFileSync(RULES, PRISTINE.replace(from, to));
212
+ const on = readFileSync(RULES, "utf8");
213
+ if (!on.includes(to) || on.includes(from)) {
214
+ console.log(
215
+ `❌ ${label}: THE MUTATION DID NOT LAND (checked by re-reading the file)`,
216
+ );
217
+ bad++;
218
+ writeFileSync(RULES, PRISTINE);
219
+ continue;
220
+ }
221
+ const { failed, out } = runHarness();
222
+ writeFileSync(RULES, PRISTINE);
223
+ if (readFileSync(RULES, "utf8") !== PRISTINE) {
224
+ console.log(`❌ ${label}: THE ROLLBACK FAILED`);
225
+ bad++;
226
+ continue;
227
+ }
228
+ // The verdict carries the HARNESS LINE and the assertion text: "killed" without saying by
229
+ // what is half an answer. The battery must show that each mutation died on ITS OWN assertion.
230
+ const at = (out.match(/tex-build\.harness\.mjs:(\d+)/) ?? [])[1];
231
+ const msg =
232
+ (out.match(/AssertionError[^:]*: ([^\n]+)/) ?? [])[1] ??
233
+ // Not every mutation dies on an assertion: removing the `try/catch` crashes the linter
234
+ // itself, and then "what it died on" is a thrown error. Without this branch the column
235
+ // would come out EMPTY — "killed by something unknown", which is the half-answer this
236
+ // battery forbids.
237
+ (out.match(/((?:Error|ENOENT)[^\n]*)/) ?? [])[1] ??
238
+ (out.match(
239
+ /([^\n]*(?:must|the wrong|drifted|was lost|did not fire)[^\n]*)/,
240
+ ) ?? [])[1] ??
241
+ "";
242
+ const where = (at ? `harness:${at} · ` : "") + msg;
243
+ if (failed) {
244
+ ok++;
245
+ rows.push([label, what, where.trim().slice(0, 130)]);
246
+ } else {
247
+ console.log(
248
+ `🔴 ${label}: THE HARNESS IS GREEN UNDER THE MUTATION — a finding about the TEST, not a conclusion about the defence`,
249
+ );
250
+ bad++;
251
+ }
252
+ }
253
+ console.log("\n| property | mutation | what the harness died on |");
254
+ console.log("|---|---|---|");
255
+ for (const [a, b, c] of rows)
256
+ console.log(`| \`${a}\` | ${b} | ${c.replace(/\|/g, "\\|")} |`);
257
+ console.log(`\n${ok} mutation(s) killed, ${bad} problem(s)`);
258
+ process.exit(bad ? 1 : 0);