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,753 @@
1
+ /**
2
+ * Colocated test for `eslint-rules/tex-build.mjs` (`tex/future-promise`).
3
+ * Run: `npx vigiles test eslint-rules/tex-build.harness.mjs`
4
+ *
5
+ * FIVE PARTS, AND THE ORDER IS LOAD-BEARING:
6
+ * I. FIXTURES ON DISK — `fixtures/tex-build/clean.tex` must be silent, `defect.tex` must
7
+ * fire, and both are checked through the real `eslint.config.mjs`, not through a
8
+ * config invented here. A frozen finding set, positions and captured text included.
9
+ * II. QUIET ON A CORRECT INPUT, and EACH reason for the silence is checked separately.
10
+ * It comes before "fires on a planted defect" deliberately: a rule that fails a correct
11
+ * input is more dangerous than no rule at all — it gets switched off the same day.
12
+ * III. FIRES ON A PLANTED DEFECT — on the very one the check was added for on 2026-08-24,
13
+ * plus one input per branch of the promise vocabulary.
14
+ * IV. PROPERTIES NOBODY "SEES", WITHOUT WHICH THE RULE LIES.
15
+ * V. CONFIG: the rule is declared on `.tex`, its severity is the measured one, and the
16
+ * glob it is declared on is NOT EMPTY.
17
+ * VI. THE SECOND RULE OF THIS MODULE — `tex/acm-frontmatter-override` — with the same five
18
+ * parts in the same order, kept in one self-contained block rather than interleaved.
19
+ * The two rules share a file and share nothing else: one reads the PROSE of a shipped
20
+ * build, the other reads the PREAMBLE of any acmart build, and a test that mixed them
21
+ * would make each rule's silence depend on the other's input.
22
+ *
23
+ * 🔴 ZERO ON A CLEAN FIXTURE IS NOT "CHECKED". Part I on its own cannot tell a clean input
24
+ * from a dead rule: for an advisory check, silence IS its success state. Part III is what
25
+ * distinguishes them — the planted defect. Both halves are mandatory, and separately neither
26
+ * proves anything.
27
+ *
28
+ * ── PARITY WITH THE PREVIOUS IMPLEMENTATION (measured 2026-09-07, BEFORE its removal) ──
29
+ * There is deliberately NO copy of the old script's `checkFuturePromises` here: vendoring it
30
+ * for the sake of a test would re-create the second source of truth the move was meant to
31
+ * remove. What lives here instead is the RESULT of the comparison, taken by a differential run
32
+ * over 15 inputs (10 real `.tex` files of the source corpus + 5 synthetic mutations on top of
33
+ * a real paper):
34
+ * the finding sets agreed on ALL FIFTEEN, positions and captured text included
35
+ * (`284:will be released`, `284:Upon acceptance` + `285:will be made publicly available`).
36
+ *
37
+ * 🔴 AND ONE DISAGREEMENT, WHICH IS NOT IN THE LOGIC BUT IN THE INPUT — and it is the whole
38
+ * gain. The old implementation picked its file by looking for `paper.md` / `draft.md` and
39
+ * otherwise taking the FIRST `.tex` in `readdirSync` order. Measured 2026-09-07:
40
+ * paper A → a 536-BYTE file of nine `\def`s with venue numbers. The check, added
41
+ * specifically BECAUSE of paper A, was not reading paper A;
42
+ * paper B → a four-month-old draft, one of seven `.tex` files in that directory;
43
+ * paper C → `paper.tex`, the only `.tex` there; it matched.
44
+ * The finding sets agreed anyway (all empty), but they agreed BY ACCIDENT, not by
45
+ * construction. A config glob removes the dependency of the address on directory order.
46
+ *
47
+ * ⚠️ WHAT WAS LOST WHEN THIS TEST LEFT THE PRIVATE CORPUS BEHIND, stated rather than glossed:
48
+ * part I used to freeze the finding set of three REAL papers, and one assertion used a real
49
+ * paper as a LIVE SUBJECT for the commented-out-`review` case. Fixtures cannot reproduce "this
50
+ * is what the corpus actually looks like today"; they reproduce the mechanism only. The
51
+ * measurements from those runs are kept in the comments, the papers are not.
52
+ *
53
+ * 🔴 Assertions live at MODULE TOP LEVEL: `vigiles test` imports the file and treats "it did
54
+ * not throw" as success; an exported `tests` object is run by nothing.
55
+ */
56
+ import { ESLint } from "eslint";
57
+ import assert from "node:assert/strict";
58
+ import {
59
+ existsSync,
60
+ mkdirSync,
61
+ mkdtempSync,
62
+ readFileSync,
63
+ rmSync,
64
+ writeFileSync,
65
+ realpathSync,
66
+ } from "node:fs";
67
+ import { tmpdir } from "node:os";
68
+ import { dirname, join, relative } from "node:path";
69
+ import { fileURLToPath } from "node:url";
70
+
71
+ const HERE = dirname(fileURLToPath(import.meta.url));
72
+ const ROOT = join(HERE, ".."); // eslint-rules → repository root
73
+ const RULES = join(HERE, "tex-build.mjs");
74
+ assert.ok(existsSync(RULES), `no entry point for the test: ${RULES}`);
75
+
76
+ const TMP = realpathSync(mkdtempSync(join(tmpdir(), "tex-build-")));
77
+ // Cleanup is registered IMMEDIATELY, not at the end of the file: assertions throw, and an
78
+ // `rmSync` at the bottom never runs in exactly the runs that are red.
79
+ process.on("exit", () => rmSync(TMP, { recursive: true, force: true }));
80
+
81
+ const { texLanguage } = await import(join(HERE, "latex-language.mjs"));
82
+ const texBuild = (await import(RULES)).default;
83
+
84
+ // A new rule with no declared severity must not slip through unnoticed.
85
+ assert.deepEqual(
86
+ Object.keys(texBuild).sort(),
87
+ ["acm-frontmatter-override", "future-promise"],
88
+ "the module's rule set changed — update the config and this harness",
89
+ );
90
+
91
+ const makeEslint = (cwd) =>
92
+ new ESLint({
93
+ cwd,
94
+ overrideConfigFile: true,
95
+ overrideConfig: [
96
+ {
97
+ files: ["**/*.tex"],
98
+ plugins: {
99
+ tex: { languages: { latex: texLanguage }, rules: texBuild },
100
+ },
101
+ language: "tex/latex",
102
+ rules: { "tex/future-promise": "warn" },
103
+ },
104
+ ],
105
+ });
106
+
107
+ /** Findings for one file as `{line, column, text}` — and not one of them fatal. */
108
+ async function lintTex(eslint, file) {
109
+ const [res] = await eslint.lintFiles([file]);
110
+ assert.deepEqual(
111
+ res.messages.filter((m) => m.fatal || !m.ruleId),
112
+ [],
113
+ `${file}: the language failed to parse or the rule threw — that is a linter crash, not a finding`,
114
+ );
115
+ return res.messages.map((m) => ({
116
+ rule: m.ruleId,
117
+ line: m.line,
118
+ column: m.column,
119
+ // The captured promise, taken out of the message itself. What matters is WHAT was caught,
120
+ // not only how much: a rule catching the wrong substring yields the same count.
121
+ text: /«([^»]+)»/.exec(m.message)?.[1] ?? null,
122
+ }));
123
+ }
124
+
125
+ let fixtureN = 0;
126
+ /** Writes a `.tex` into a temp directory and returns the findings. */
127
+ async function findings(name, text) {
128
+ const dir = join(TMP, `f${fixtureN++}-${name.replace(/[^a-z0-9-]/gi, "_")}`);
129
+ mkdirSync(dir, { recursive: true });
130
+ const file = join(dir, "paper.tex");
131
+ writeFileSync(file, text);
132
+ return lintTex(makeEslint(TMP), file);
133
+ }
134
+
135
+ /** A minimal build: NOT review mode, with somewhere to put prose. */
136
+ const build = (
137
+ body,
138
+ { cls = "\\documentclass[sigconf,screen]{acmart}" } = {},
139
+ ) =>
140
+ [
141
+ cls,
142
+ "\\begin{document}",
143
+ "\\begin{abstract}",
144
+ body,
145
+ "\\end{abstract}",
146
+ "\\end{document}",
147
+ "",
148
+ ].join("\n");
149
+
150
+ // ═════════════════════════════════════════════════════════════════════════════
151
+ // I. FIXTURES ON DISK, THROUGH THE REPOSITORY'S OWN CONFIG
152
+ // ═════════════════════════════════════════════════════════════════════════════
153
+ // 🔴 Deliberately NOT `makeEslint` here. Everywhere else in this file the config is built
154
+ // in-process, which tests the rule and says nothing about whether the rule is WIRED UP. These
155
+ // two runs go through `eslint.config.mjs` as it sits on disk, so a rule that is correct and
156
+ // unreferenced fails here.
157
+ const CLEAN_FIXTURE = join(ROOT, "fixtures/tex-build/clean.tex");
158
+ const DEFECT_FIXTURE = join(ROOT, "fixtures/tex-build/defect.tex");
159
+ const real = new ESLint({ cwd: ROOT });
160
+
161
+ for (const f of [CLEAN_FIXTURE, DEFECT_FIXTURE])
162
+ assert.ok(existsSync(f), `missing fixture: ${f}`);
163
+
164
+ assert.deepEqual(
165
+ await lintTex(real, CLEAN_FIXTURE),
166
+ [],
167
+ "fixtures/tex-build/clean.tex produced findings — a check that fails a correct input gets " +
168
+ "switched off the same day",
169
+ );
170
+
171
+ // 🔴 AND THE REASON FOR THAT ZERO IS CHECKED SEPARATELY. "Zero because the build is clean" and
172
+ // "zero because the build is exempt as review mode" are indistinguishable from the outside; the
173
+ // only thing telling them apart is that the exemption sign is genuinely absent from the file.
174
+ // Without this assertion, someone editing the fixture into a review build would turn the quiet
175
+ // half into a tautology and nothing would notice.
176
+ assert.doesNotMatch(
177
+ readFileSync(CLEAN_FIXTURE, "utf8"),
178
+ /^[^%\n]*(?:\\documentclass\[[^\]]*\breview\b|printacmref=false)/m,
179
+ "fixtures/tex-build/clean.tex became a review build — its zero stopped meaning `the rule ran " +
180
+ "and found nothing` and started meaning `the rule was exempt`",
181
+ );
182
+
183
+ // The frozen finding set of the defect fixture: not "some findings", but exactly these two,
184
+ // at these addresses, with these captured substrings.
185
+ assert.deepEqual(
186
+ await lintTex(real, DEFECT_FIXTURE),
187
+ [
188
+ {
189
+ rule: "tex/future-promise",
190
+ line: 15,
191
+ column: 59,
192
+ text: "at camera-ready",
193
+ },
194
+ {
195
+ rule: "tex/future-promise",
196
+ line: 22,
197
+ column: 52,
198
+ text: "will be made publicly available",
199
+ },
200
+ ],
201
+ "fixtures/tex-build/defect.tex: the finding set drifted from the frozen measurement",
202
+ );
203
+
204
+ // ═════════════════════════════════════════════════════════════════════════════
205
+ // II. QUIET ON A CORRECT INPUT — each reason for the silence on its own
206
+ // ═════════════════════════════════════════════════════════════════════════════
207
+ assert.deepEqual(
208
+ await findings("clean-build", build("We measured how often a rule fails.")),
209
+ [],
210
+ "a build with no promises must be silent",
211
+ );
212
+
213
+ // A promise made DURING PEER REVIEW is true and ordinary. This is not leniency, it is the
214
+ // subject: the defect is only a promise that outlived its own delivery.
215
+ for (const [label, cls] of [
216
+ ["review option", "\\documentclass[sigconf,review]{acmart}"],
217
+ [
218
+ "printacmref=false",
219
+ "\\documentclass[sigconf]{acmart}\n\\settopmatter{printacmref=false}",
220
+ ],
221
+ ])
222
+ assert.deepEqual(
223
+ await findings(
224
+ `review-${label}`,
225
+ build("The harness ships at camera-ready.", { cls }),
226
+ ),
227
+ [],
228
+ `${label}: a review build must be exempt`,
229
+ );
230
+
231
+ // 🔴 A LaTeX COMMENT IS A LEGITIMATE PLACE FOR THESE WORDS. Author notes of the form
232
+ // `% camera-ready blocker` live in real sources, and a finding on them would be exactly the
233
+ // false positive that kills a check.
234
+ assert.deepEqual(
235
+ await findings(
236
+ "promise-in-comment",
237
+ build("% blocker: the harness ships at camera-ready."),
238
+ ),
239
+ [],
240
+ "a promise inside a LaTeX comment is not a finding",
241
+ );
242
+
243
+ // ═════════════════════════════════════════════════════════════════════════════
244
+ // III. FIRES ON A PLANTED DEFECT
245
+ // ═════════════════════════════════════════════════════════════════════════════
246
+ {
247
+ // THE defect: the line printed in an accepted camera-ready on 2026-08-24 in both of the most
248
+ // read positions, while the Availability paragraph three lines below said the opposite.
249
+ const ms = await findings(
250
+ "the-2026-08-24-defect",
251
+ build("We report the finding (full harness at camera-ready)."),
252
+ );
253
+ assert.equal(ms.length, 1, "the historical defect must be caught");
254
+ assert.equal(ms[0].rule, "tex/future-promise");
255
+ assert.equal(ms[0].text, "at camera-ready", "the wrong substring was caught");
256
+ // The address is what the move to a lint rule bought, so it is checked rather than declared:
257
+ // line 4 of the fixture, and a column at the start of the match, not at the start of the line.
258
+ assert.equal(
259
+ ms[0].line,
260
+ 4,
261
+ "the finding must point at the line carrying the promise",
262
+ );
263
+ assert.equal(
264
+ ms[0].column,
265
+ "We report the finding (full harness ".length + 1,
266
+ "the column must point at the start of the match",
267
+ );
268
+ }
269
+
270
+ // Every branch of the promise vocabulary gets its own input. Without that, "a vocabulary of six
271
+ // forms" is a claim verified by one of the six.
272
+ for (const [sentence, want] of [
273
+ ["The harness ships at camera-ready.", "at camera-ready"],
274
+ ["The harness ships at the camera ready stage.", "at the camera ready"],
275
+ ["Upon acceptance we open the repository.", "Upon acceptance"],
276
+ ["On acceptance we open the repository.", "On acceptance"],
277
+ ["The harness will be released later.", "will be released"],
278
+ ["The harness will be published later.", "will be published"],
279
+ [
280
+ "The data will be made publicly available.",
281
+ "will be made publicly available",
282
+ ],
283
+ ["The data will be made available.", "will be made available"],
284
+ ["The harness will be public.", "will be public"],
285
+ ["The harness will be open-sourced.", "will be open-sourced"],
286
+ ]) {
287
+ const ms = await findings(`vocab-${want}`, build(sentence));
288
+ assert.equal(
289
+ ms.length,
290
+ 1,
291
+ `vocabulary branch «${want}» did not fire on «${sentence}»`,
292
+ );
293
+ assert.equal(
294
+ ms[0].text,
295
+ want,
296
+ `vocabulary branch «${want}» caught the wrong substring`,
297
+ );
298
+ }
299
+
300
+ // Two promises on different lines do not collapse into one (the rule reports EVERY place).
301
+ {
302
+ const ms = await findings(
303
+ "two-lines",
304
+ build(
305
+ "Upon acceptance we ship.\nThe data will be made publicly available.",
306
+ ),
307
+ );
308
+ assert.deepEqual(
309
+ ms.map((m) => `${m.line}:${m.text}`),
310
+ ["4:Upon acceptance", "5:will be made publicly available"],
311
+ "two promises on different lines must give two findings with their own addresses",
312
+ );
313
+ }
314
+
315
+ // ═════════════════════════════════════════════════════════════════════════════
316
+ // IV. PROPERTIES NOBODY "SEES", WITHOUT WHICH THE RULE LIES
317
+ // ═════════════════════════════════════════════════════════════════════════════
318
+ // 🔴 A COMMENTED-OUT `\documentclass[…review…]` DOES NOT MAKE A BUILD A REVIEW BUILD. In the
319
+ // source corpus this was a live property, not an invented case: one paper carried
320
+ // `% was: \documentclass[sigconf,review]{acmart}` a few lines above its real
321
+ // `\documentclass[sigconf,screen]{acmart}`. Without the `^[^%\n]*` anchor in `REVIEW_MODE_RE`
322
+ // that whole paper would have been silently exempted by the history of its own edits.
323
+ // ⚠️ The live half of this assertion (reading the real paper) did not come along with the
324
+ // extraction — the paper is private. What is left is the synthetic half, which pins the
325
+ // mechanism but no longer proves the case still occurs in the wild.
326
+ {
327
+ const ms = await findings(
328
+ "commented-out-review-class",
329
+ build("The harness ships at camera-ready.", {
330
+ cls: "% was: \\documentclass[sigconf,review]{acmart}\n\\documentclass[sigconf,screen]{acmart}",
331
+ }),
332
+ );
333
+ assert.equal(
334
+ ms.length,
335
+ 1,
336
+ "a commented-out review class exempted the build — the `^[^%\\n]*` anchor was lost",
337
+ );
338
+ }
339
+
340
+ // An escaped `\%` is NOT a comment: a line where a percent sign precedes a promise must still
341
+ // be checked in full.
342
+ {
343
+ const ms = await findings(
344
+ "escaped-percent",
345
+ build("We cover 95\\% of cases and the harness will be published later."),
346
+ );
347
+ assert.equal(
348
+ ms.length,
349
+ 1,
350
+ "`\\%` ate the rest of the line — the promise behind it went invisible",
351
+ );
352
+ assert.equal(ms[0].text, "will be published");
353
+ }
354
+
355
+ // No file on disk (a run through stdin): the rule stays quiet rather than judging content it
356
+ // never saw.
357
+ {
358
+ const [res] = await makeEslint(TMP).lintText(
359
+ build("The harness ships at camera-ready."),
360
+ { filePath: join(TMP, "no-such-dir", "paper.tex") },
361
+ );
362
+ assert.deepEqual(
363
+ res.messages.filter((m) => m.fatal),
364
+ [],
365
+ "the rule threw on a non-existent path",
366
+ );
367
+ assert.deepEqual(
368
+ res.messages.map((m) => m.ruleId),
369
+ [],
370
+ "the rule reads the file FROM DISK (the preamble is blanked in the projection), and on a " +
371
+ "path with no file it must stay quiet rather than judge",
372
+ );
373
+ }
374
+
375
+ // ═════════════════════════════════════════════════════════════════════════════
376
+ // V. CONFIG — the rule is on, at the measured severity, over a NON-EMPTY glob
377
+ // ═════════════════════════════════════════════════════════════════════════════
378
+ {
379
+ const config = (await import(join(ROOT, "eslint.config.mjs"))).default;
380
+ const tex = config.find((b) => b.rules?.["tex/future-promise"]);
381
+ assert.ok(
382
+ tex,
383
+ "eslint.config.mjs declares no block carrying `tex/future-promise`",
384
+ );
385
+ assert.equal(
386
+ tex.rules["tex/future-promise"],
387
+ "warn",
388
+ "the severity of `tex/future-promise` changed. `warn` is an analysis, not caution: the " +
389
+ "finding is not binary (a promise in a camera-ready is sometimes honest, and the verdict " +
390
+ "«does it contradict Availability» is a human one), plus there is a demonstrable class of " +
391
+ "false positives — a sentence about SOMEONE ELSE's work («their replication will be " +
392
+ "published in 2027»)",
393
+ );
394
+ assert.ok(
395
+ tex.plugins?.tex?.rules === texBuild,
396
+ "the `.tex` block no longer wires up `tex-build.mjs` — the rule stopped executing",
397
+ );
398
+
399
+ // 🔴 AND THE HALF WITHOUT WHICH ALL OF THE ABOVE IS DECORATION: the glob must match files
400
+ // that exist. A rule whose glob matches nothing is never invoked, and the run is green with
401
+ // zero findings — byte-identical to a rule that passed. See `scripts/rules-see-files.mjs`,
402
+ // which checks this for every declared rule; the assertion here is the cheap local copy so
403
+ // that this rule's own wiring fails in this rule's own test.
404
+ const seen = (await real.lintFiles(["."])).map((r) =>
405
+ relative(ROOT, r.filePath),
406
+ );
407
+ const texSeen = seen.filter((f) => f.endsWith(".tex"));
408
+ assert.ok(
409
+ texSeen.length > 0,
410
+ "the `.tex` glob of eslint.config.mjs matched NO files on disk — the rule was never " +
411
+ "invoked, and this run's zero findings mean nothing. Files linted: " +
412
+ seen.join(", "),
413
+ );
414
+ }
415
+
416
+ // ═════════════════════════════════════════════════════════════════════════════
417
+ // VI. tex/acm-frontmatter-override — the module's second rule, tested end to end
418
+ // ═════════════════════════════════════════════════════════════════════════════
419
+ // 🔴 ITS OWN ESLint FACTORY, with ONLY this rule enabled. Sharing `makeEslint` would make the
420
+ // two rules' findings land in one array, and then "quiet" for one of them would depend on the
421
+ // other's input — the class of tautology part I already guards against on the fixtures.
422
+ const makeEslintFm = (cwd) =>
423
+ new ESLint({
424
+ cwd,
425
+ overrideConfigFile: true,
426
+ overrideConfig: [
427
+ {
428
+ files: ["**/*.tex"],
429
+ plugins: {
430
+ tex: { languages: { latex: texLanguage }, rules: texBuild },
431
+ },
432
+ language: "tex/latex",
433
+ rules: { "tex/acm-frontmatter-override": "warn" },
434
+ },
435
+ ],
436
+ });
437
+
438
+ let fmN = 0;
439
+ async function fmFindings(name, text) {
440
+ const dir = join(TMP, `fm${fmN++}-${name.replace(/[^a-z0-9-]/gi, "_")}`);
441
+ mkdirSync(dir, { recursive: true });
442
+ const file = join(dir, "paper.tex");
443
+ writeFileSync(file, text);
444
+ return lintTex(makeEslintFm(TMP), file);
445
+ }
446
+
447
+ /** A minimal acmart preamble plus a body, so the language has something to parse. */
448
+ const acm = (
449
+ preambleTail,
450
+ { cls = "\\documentclass[sigconf,screen]{acmart}" } = {},
451
+ ) =>
452
+ [
453
+ cls,
454
+ preambleTail,
455
+ "\\begin{document}",
456
+ "\\begin{abstract}",
457
+ "We measured one thing and report it plainly.",
458
+ "\\end{abstract}",
459
+ "\\end{document}",
460
+ "",
461
+ ].join("\n");
462
+
463
+ // ── VI.I FIXTURES ON DISK, THROUGH THE REPOSITORY'S OWN CONFIG ───────────────
464
+ const FM_CLEAN = join(ROOT, "fixtures/tex-build/frontmatter-clean.tex");
465
+ const FM_DEFECT = join(ROOT, "fixtures/tex-build/frontmatter-defect.tex");
466
+ for (const f of [FM_CLEAN, FM_DEFECT])
467
+ assert.ok(existsSync(f), `missing fixture: ${f}`);
468
+
469
+ assert.deepEqual(
470
+ await lintTex(real, FM_CLEAN),
471
+ [],
472
+ "fixtures/tex-build/frontmatter-clean.tex produced findings — a check that fails a correct " +
473
+ "input gets switched off the same day",
474
+ );
475
+
476
+ // 🔴 AND THE REASONS FOR THAT ZERO, EACH CHECKED SEPARATELY. Three different edits to the
477
+ // fixture would turn its silence into a tautology, and from the outside all three look the
478
+ // same as "the rule ran and the build is clean":
479
+ // (a) it stops being an acmart build → the rule returns before looking at anything;
480
+ // (b) it acquires `nonacm` → the rule is exempt;
481
+ // (c) the commented override block disappears → the comment-blanking half of the rule stops
482
+ // being exercised at all, and that half is the one that protects the accepted sibling
483
+ // paper of the source corpus, which keeps exactly such a block as a record.
484
+ {
485
+ const src = readFileSync(FM_CLEAN, "utf8");
486
+ assert.match(
487
+ src,
488
+ /^\\documentclass(?:\s*\[[^\]]*\])?\s*\{acmart\}/m,
489
+ "(a) fixtures/tex-build/frontmatter-clean.tex stopped being an acmart build — its zero now " +
490
+ "means «the rule returned early», not «the rule ran and found nothing»",
491
+ );
492
+ assert.doesNotMatch(
493
+ src,
494
+ /^\\documentclass\s*\[[^\]]*\bnonacm\b/m,
495
+ "(b) fixtures/tex-build/frontmatter-clean.tex became a `nonacm` build — its zero now means " +
496
+ "«exempt», not «clean»",
497
+ );
498
+ assert.match(
499
+ src,
500
+ /^%.*\\setcopyright\{none\}/m,
501
+ "(c) fixtures/tex-build/frontmatter-clean.tex lost its COMMENTED override block. That block " +
502
+ "is the fixture's point: it proves a finding is not raised on an author's record of what " +
503
+ "the preamble used to do. Without it the quiet half no longer exercises comment blanking",
504
+ );
505
+ }
506
+
507
+ // The frozen finding set of the defect fixture: not "some findings", but exactly these three,
508
+ // at these addresses, with these captured macros.
509
+ assert.deepEqual(
510
+ await lintTex(real, FM_DEFECT),
511
+ [
512
+ {
513
+ rule: "tex/acm-frontmatter-override",
514
+ line: 10,
515
+ column: 1,
516
+ text: "\\setcopyright{none}",
517
+ },
518
+ {
519
+ rule: "tex/acm-frontmatter-override",
520
+ line: 11,
521
+ column: 1,
522
+ text: "\\renewcommand\\footnotetextcopyrightpermission",
523
+ },
524
+ {
525
+ rule: "tex/acm-frontmatter-override",
526
+ line: 12,
527
+ column: 1,
528
+ text: "\\pagestyle{plain}",
529
+ },
530
+ ],
531
+ "fixtures/tex-build/frontmatter-defect.tex: the finding set drifted from the frozen measurement",
532
+ );
533
+
534
+ // ── VI.II QUIET ON A CORRECT INPUT — each reason for the silence on its own ──
535
+ assert.deepEqual(
536
+ await fmFindings("plain-acmart", acm("\\setcopyright{cc}")),
537
+ [],
538
+ "an acmart build that leaves the front matter alone must be silent",
539
+ );
540
+
541
+ // OUTSIDE acmart every one of these macros is an ordinary, correct line. `\pagestyle{plain}`
542
+ // in an `article` is not a defect in any sense, and a rule that said so would be switched off
543
+ // by the first person who writes a non-ACM document.
544
+ for (const cls of [
545
+ "\\documentclass{article}",
546
+ "\\documentclass[11pt]{report}",
547
+ "\\documentclass[conference]{IEEEtran}",
548
+ ])
549
+ assert.deepEqual(
550
+ await fmFindings(
551
+ `non-acmart-${cls.slice(-12)}`,
552
+ acm("\\pagestyle{plain}", { cls }),
553
+ ),
554
+ [],
555
+ `${cls}: the macros only mean something inside acmart — outside it the rule must not fire`,
556
+ );
557
+
558
+ // `nonacm` is acmart's OWN way of saying "this is not going to an ACM venue", and in that mode
559
+ // the class disables the ACM Reference Format itself (acmart.cls:110-121). A build that says
560
+ // so the supported way is exempt — otherwise the rule would punish the very escape hatch its
561
+ // own message recommends.
562
+ for (const cls of [
563
+ "\\documentclass[sigconf,nonacm]{acmart}",
564
+ "\\documentclass[nonacm,screen]{acmart}",
565
+ ])
566
+ assert.deepEqual(
567
+ await fmFindings(
568
+ `nonacm-${cls.length}`,
569
+ acm("\\setcopyright{none}\n\\pagestyle{plain}", { cls }),
570
+ ),
571
+ [],
572
+ `${cls}: a declared non-ACM build must be exempt`,
573
+ );
574
+
575
+ // A LaTeX COMMENT IS A LEGITIMATE PLACE FOR THESE MACROS — and not a hypothetical one: the
576
+ // accepted paper of the source corpus keeps the whole block commented out as a record of what
577
+ // it used to do. Measured 2026-09-11 on the real files: the rejected paper yields three
578
+ // findings (181,182,183), the accepted one yields ZERO with the same three macros present as
579
+ // comments.
580
+ assert.deepEqual(
581
+ await fmFindings(
582
+ "override-in-comment",
583
+ acm("% was: \\setcopyright{none} + \\pagestyle{plain}\n\\setcopyright{cc}"),
584
+ ),
585
+ [],
586
+ "a commented-out override must not fire — an author's record of a removed line is not the line",
587
+ );
588
+
589
+ // 🔴 AND THE SAME FOR THE ARMING SIDE, which is the half a simpler implementation gets wrong:
590
+ // a `\documentclass` quoted inside a comment must not turn a non-acmart file into an acmart
591
+ // one. Here the REAL class is `article`, and only the comment says `acmart`.
592
+ assert.deepEqual(
593
+ await fmFindings(
594
+ "acmart-only-in-comment",
595
+ acm("% was: \\documentclass[sigconf]{acmart}\n\\pagestyle{plain}", {
596
+ cls: "\\documentclass{article}",
597
+ }),
598
+ ),
599
+ [],
600
+ "a `\\documentclass{acmart}` quoted in a comment must not arm the rule on a non-acmart build",
601
+ );
602
+
603
+ // ── VI.III FIRES ON A PLANTED DEFECT — one input per member of the set ───────
604
+ for (const [line, want] of [
605
+ ["\\setcopyright{none}", "\\setcopyright{none}"],
606
+ ["\\setcopyright{ none }", "\\setcopyright{ none }"],
607
+ [
608
+ "\\renewcommand\\footnotetextcopyrightpermission[1]{}",
609
+ "\\renewcommand\\footnotetextcopyrightpermission",
610
+ ],
611
+ [
612
+ "\\renewcommand{\\footnotetextcopyrightpermission}[1]{}",
613
+ "\\renewcommand{\\footnotetextcopyrightpermission",
614
+ ],
615
+ ["\\pagestyle{plain}", "\\pagestyle{plain}"],
616
+ ["\\pagestyle{empty}", "\\pagestyle{empty}"],
617
+ ["\\thispagestyle{empty}", "\\thispagestyle{empty}"],
618
+ ]) {
619
+ const got = await fmFindings(`fire-${want.slice(1, 20)}`, acm(line));
620
+ assert.deepEqual(
621
+ got.map((f) => f.text),
622
+ [want],
623
+ `«${line}» must produce exactly one finding capturing «${want}» — got ${JSON.stringify(got)}`,
624
+ );
625
+ }
626
+
627
+ // The stage does NOT exempt, and this is the assertion that pins the measurement the rule's
628
+ // comment rests on. The obvious design — "only flag these in review mode, the class handles
629
+ // it" — is wrong in BOTH directions, so both directions are frozen here: a review build fires,
630
+ // and so does a camera-ready one. `acmart.cls:93-99`: the `review` option turns on line
631
+ // numbers and `\@ACM@printfoliostrue`, and touches neither the copyright statement nor the
632
+ // permission footnote.
633
+ for (const cls of [
634
+ "\\documentclass[sigconf,review,anonymous]{acmart}",
635
+ "\\documentclass[sigconf,screen]{acmart}",
636
+ "\\documentclass[manuscript]{acmart}",
637
+ ])
638
+ assert.deepEqual(
639
+ (
640
+ await fmFindings(
641
+ `stage-${cls.length}`,
642
+ acm("\\setcopyright{none}", { cls }),
643
+ )
644
+ ).map((f) => f.text),
645
+ ["\\setcopyright{none}"],
646
+ `${cls}: the finding must not depend on the build stage — the overrides strip the same ` +
647
+ "furniture in every mode, and in a camera-ready that is worse, because that is the " +
648
+ "version the digital library keeps",
649
+ );
650
+
651
+ // ── VI.IV PROPERTIES NOBODY "SEES", WITHOUT WHICH THE RULE LIES ─────────────
652
+ // 🔴 THE MODULE-LEVEL REGEXES ARE `/g`, SO THEY CARRY `lastIndex`. Without the reset in the
653
+ // rule, the SECOND file linted in the same process starts searching from wherever the previous
654
+ // file's match ended — i.e. findings would depend on the ORDER files were linted in, which is
655
+ // the exact class of defect that made the previous implementation read the wrong `.tex`. Two
656
+ // runs of the same input through the same process must agree.
657
+ {
658
+ const once = (
659
+ await fmFindings("lastindex-a", acm("\\setcopyright{none}"))
660
+ ).map((f) => f.text);
661
+ const twice = (
662
+ await fmFindings("lastindex-b", acm("\\setcopyright{none}"))
663
+ ).map((f) => f.text);
664
+ assert.deepEqual(
665
+ [once, twice],
666
+ [["\\setcopyright{none}"], ["\\setcopyright{none}"]],
667
+ "the second lint in the same process disagreed with the first — a `/g` regex kept its " +
668
+ "`lastIndex` across files, and findings became order-dependent",
669
+ );
670
+ }
671
+
672
+ // TWO OVERRIDES ON ONE LINE are two findings, at two columns. A rule reporting once per line
673
+ // would silently under-count a compacted preamble.
674
+ {
675
+ const got = await fmFindings(
676
+ "two-on-one-line",
677
+ acm("\\setcopyright{none}\\pagestyle{plain}"),
678
+ );
679
+ assert.deepEqual(
680
+ got.map((f) => [f.line, f.column, f.text]),
681
+ [
682
+ [2, 1, "\\setcopyright{none}"],
683
+ [2, 20, "\\pagestyle{plain}"],
684
+ ],
685
+ "two overrides on one line must produce two findings at two columns",
686
+ );
687
+ }
688
+
689
+ // 🔴 AND TWO MATCHES OF THE **SAME** PATTERN ON ONE LINE, which the case above cannot reach:
690
+ // there each finding comes from a different entry of the table, so a rule that took only the
691
+ // FIRST match per pattern would still produce two. `\pagestyle` and `\thispagestyle` are one
692
+ // regex, so this is the only input that distinguishes `while (re.exec(...))` from `if`.
693
+ {
694
+ const got = await fmFindings(
695
+ "same-pattern-twice",
696
+ acm("\\pagestyle{plain}\\thispagestyle{empty}"),
697
+ );
698
+ assert.deepEqual(
699
+ got.map((f) => [f.column, f.text]),
700
+ [
701
+ [1, "\\pagestyle{plain}"],
702
+ [18, "\\thispagestyle{empty}"],
703
+ ],
704
+ "two matches of the SAME pattern on one line must both be reported — the rule stopped at " +
705
+ "the first match per pattern",
706
+ );
707
+ }
708
+
709
+ // A FILE THE RULE CANNOT READ MUST BE SILENT, NOT THROWN. The rule reads from disk because the
710
+ // language's projection blanks the preamble; the price is that a run with no real file behind
711
+ // it (stdin, a deleted path) has nothing to judge, and judging nothing must not crash the lint.
712
+ {
713
+ const dir = join(TMP, "unreadable");
714
+ mkdirSync(dir, { recursive: true });
715
+ const file = join(dir, "paper.tex");
716
+ writeFileSync(file, acm("\\setcopyright{none}"));
717
+ const eslint = makeEslintFm(TMP);
718
+ const before = (await lintTex(eslint, file)).length;
719
+ assert.equal(
720
+ before,
721
+ 1,
722
+ "setup failed: the file should fire before it is removed",
723
+ );
724
+ rmSync(file);
725
+ const [res] = await eslint.lintText(acm("\\setcopyright{none}"), {
726
+ filePath: file,
727
+ });
728
+ assert.deepEqual(
729
+ res.messages.filter((m) => m.fatal),
730
+ [],
731
+ "a file that cannot be read made the rule throw — an unreadable path must be silent, not fatal",
732
+ );
733
+ }
734
+
735
+ // ── VI.V CONFIG — the rule is on, at the measured severity ───────────────────
736
+ {
737
+ const config = (await import(join(ROOT, "eslint.config.mjs"))).default;
738
+ const tex = config.find((b) => b.rules?.["tex/acm-frontmatter-override"]);
739
+ assert.ok(
740
+ tex,
741
+ "eslint.config.mjs declares no block carrying `tex/acm-frontmatter-override` — the rule is " +
742
+ "correct and unreferenced, which lints exactly like a rule that passed",
743
+ );
744
+ assert.equal(
745
+ tex.rules["tex/acm-frontmatter-override"],
746
+ "warn",
747
+ "the severity of `tex/acm-frontmatter-override` changed IN THIS REPOSITORY. `warn` here is " +
748
+ "not a statement about confidence — the finding is binary and has a named exemption. It " +
749
+ "is a statement about the corpus: this repository lints fixtures that are broken by " +
750
+ "construction, so `error` would only mean `npx eslint .` exits non-zero on a healthy " +
751
+ "checkout. A consumer linting a real paper sets `error`",
752
+ );
753
+ }