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,113 @@
1
+ /**
2
+ * Colocated test for `scripts/rules-are-content-only.mjs` โ€” a rule may not ask git.
3
+ * Run: `npx vigiles test scripts/rules-are-content-only.harness.mjs`
4
+ *
5
+ * ๐Ÿ”ด BOTH HALVES. Like its sibling, this guard's success state is silence, so silence alone is
6
+ * no evidence it works:
7
+ * I. QUIET on this repository's real `eslint-rules/`, and the count is asserted non-zero โ€”
8
+ * a guard that scanned nothing is also quiet, and that is the reading to rule out first.
9
+ * II. FIRES on each form the defect takes: static `import`, `require()`, dynamic `import()`,
10
+ * and the `node:` prefix โ€” the prefix matters because a set membership test that lists
11
+ * only the bare specifier passes a file that writes the modern spelling.
12
+ * III. QUIET on sources that merely TALK about the banned module โ€” a comment and a string
13
+ * literal. This is the half a grep cannot have, and the reason the check parses.
14
+ */
15
+ import assert from "node:assert/strict";
16
+ import { dirname, join } from "node:path";
17
+ import { fileURLToPath } from "node:url";
18
+
19
+ const HERE = dirname(fileURLToPath(import.meta.url));
20
+ const { rulesAreContentOnly, processImports } = await import(
21
+ join(HERE, "rules-are-content-only.mjs")
22
+ );
23
+
24
+ let n = 0;
25
+ const check = (label, ok) => {
26
+ n++;
27
+ assert.equal(ok, true, label);
28
+ };
29
+
30
+ // โ”€โ”€ I. quiet on the real corpus, and it actually looked โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
31
+ {
32
+ const { checked, findings } = rulesAreContentOnly({ cwd: join(HERE, "..") });
33
+ check(
34
+ `scanned something (${checked.length} rule sources)`,
35
+ checked.length > 0,
36
+ );
37
+ check(
38
+ `quiet on the real corpus (${findings.length} findings)`,
39
+ findings.length === 0,
40
+ );
41
+ check(
42
+ "mutation files are not scanned as rules",
43
+ !checked.some((f) => f.endsWith(".mutations.mjs")),
44
+ );
45
+ }
46
+
47
+ // โ”€โ”€ II. fires on every form the defect has taken โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
48
+ for (const [label, src] of [
49
+ [
50
+ "execFileSync with a git argv",
51
+ `import { execFileSync } from "node:child_process";
52
+ execFileSync("git", ["cat-file", "-e", sha]);`,
53
+ ],
54
+ [
55
+ "a namespaced call",
56
+ `import cp from "node:child_process";
57
+ cp.spawnSync("git", ["rev-parse", ref]);`,
58
+ ],
59
+ [
60
+ "execSync with a git shell line",
61
+ `import { execSync } from "child_process";
62
+ execSync("git log --oneline -1");`,
63
+ ],
64
+ ["exec, async form", `exec("git rev-list --all", cb);`],
65
+ ]) {
66
+ check(`FIRES on ${label}`, processImports(src).length === 1);
67
+ }
68
+
69
+ // โ”€โ”€ III. the FALSE POSITIVE that forced the narrowing, asserted so it cannot return โ”€โ”€
70
+ // ๐Ÿ”ด `paper-texcount.mjs` in the consumer shells out to `texcount` to count words in a .tex.
71
+ // The first version of this check keyed on importing `child_process` at all, so it claimed that
72
+ // rule on its first run โ€” a false positive on an `error` gate, which is worse than a miss
73
+ // because the gate that cannot be cleared gets switched off rather than fixed.
74
+ for (const [label, src] of [
75
+ [
76
+ "a rule spawning texcount",
77
+ `import { execFileSync } from "node:child_process";
78
+ execFileSync("texcount", ["-brief", file]);`,
79
+ ],
80
+ [
81
+ "a rule spawning pdflatex",
82
+ `import { spawnSync } from "node:child_process";
83
+ spawnSync("pdflatex", [tex]);`,
84
+ ],
85
+ [
86
+ "importing child_process without running git",
87
+ `import { execFileSync } from "node:child_process";
88
+ export default {};`,
89
+ ],
90
+ ]) {
91
+ check(`quiet on ${label}`, processImports(src).length === 0);
92
+ }
93
+
94
+ // โ”€โ”€ IV. quiet on sources that only MENTION it โ€” what a grep cannot do โ”€โ”€โ”€โ”€โ”€โ”€โ”€
95
+ for (const [label, src] of [
96
+ [
97
+ "a comment naming git",
98
+ `// never reach for git cat-file here\nexport default {};`,
99
+ ],
100
+ ["a string literal", `export const why = "git cat-file is banned in rules";`],
101
+ [
102
+ "a reported message",
103
+ `export default { meta: { messages: { m: "run git cat-file? no" } } };`,
104
+ ],
105
+ [
106
+ "a program merely NAMED git-something",
107
+ `execFileSync("git-lfs-helper", []);`,
108
+ ],
109
+ ]) {
110
+ check(`quiet on ${label}`, processImports(src).length === 0);
111
+ }
112
+
113
+ console.log(`rules-are-content-only.harness: ${n} assertions passed`);
@@ -0,0 +1,138 @@
1
+ /**
2
+ * A RULE MAY NOT ASK GIT.
3
+ *
4
+ * ๐Ÿ”ด The defect it exists for, measured 2026-09-16. `paper/source` bound a paper's stage to a
5
+ * commit sha and verified it with `git cat-file -e`. Of four shas recorded in the consumer's
6
+ * front matter, ONE still resolved ninety minutes later: a squash-merge had orphaned the branch
7
+ * commits and `gc` collected them. A recorded sha is a pointer to a pointer, and both hops are
8
+ * removed by ROUTINE maintenance โ€” squash, rebase, gc, force-push, filter-repo. Nobody is at
9
+ * fault, so nobody is on the hook to fix it; the rule simply turns red and gets switched off.
10
+ *
11
+ * โ”€โ”€ THE STRONGER LEG, AND THE REASON THIS IS A FLOOR CHECK โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
12
+ * `actions/checkout` defaults to `fetch-depth: 1` โ€” exactly one commit. Measured in the
13
+ * consumer the same day: six checkouts, not one override. So a git-reading rule does not
14
+ * resolve even a LIVE sha under CI, which is the one place it has to hold. Locally it stays
15
+ * green, so the failure is one-directional and in the flattering direction.
16
+ *
17
+ * โ”€โ”€ WHAT IT CHECKS, AND WHY NOT A GREP โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
18
+ * A `child_process` spawner called with `git` as the program, read from the AST. Parsed, not
19
+ * grepped, so the words `git cat-file` in this very comment are not a finding โ€” a text guard
20
+ * would have to exempt itself, which is the failure mode that makes text guards unmaintainable.
21
+ *
22
+ * โš ๏ธ THE BOUND, stated so the check is not mistaken for more than it is. Two things pass: a
23
+ * rule that opens `.git/` with `fs` (indistinguishable at this level from reading a data file),
24
+ * and a rule that assembles the program name at runtime. Both accepted deliberately โ€” see the
25
+ * note on `SPAWNERS` for the false positive that forced the predicate this narrow.
26
+ *
27
+ * Run: `node scripts/rules-are-content-only.mjs` (exit 1 and a named rule on a finding)
28
+ * Tested by: `scripts/rules-are-content-only.harness.mjs` โ€” quiet on the real corpus, firing on
29
+ * a planted rule.
30
+ */
31
+ import { readdirSync, readFileSync } from "node:fs";
32
+ import { join } from "node:path";
33
+ import { parse } from "espree";
34
+ import { isMain } from "../skills/paper-pipeline/scripts/consumer.mjs";
35
+
36
+ /**
37
+ * ๐Ÿ”ด THE PREDICATE IS "RUNS GIT", NOT "SPAWNS A PROCESS", and the narrowing was forced by a
38
+ * FALSE POSITIVE on the consumer's corpus the hour this check shipped: `paper-texcount.mjs`
39
+ * shells out to `texcount` to count words in a .tex, which is a measuring tool, not history.
40
+ * A false positive on an `error` gate is worse than a miss โ€” the gate that cannot be cleared
41
+ * gets switched off, and this one would have been switched off on its first run.
42
+ *
43
+ * โš ๏ธ THE COST OF NARROWING, stated rather than hidden: a rule that builds the program name at
44
+ * runtime (`const g = "gi" + "t"`) passes. That is accepted. The defect has taken exactly one
45
+ * form โ€” a literal `git` handed to a `child_process` function โ€” and a check that also caught
46
+ * the contrived form would have to claim every spawn, which is the false positive above.
47
+ */
48
+ const SPAWNERS = new Set([
49
+ "exec",
50
+ "execSync",
51
+ "execFile",
52
+ "execFileSync",
53
+ "spawn",
54
+ "spawnSync",
55
+ "fork",
56
+ ]);
57
+
58
+ /** `"git"` as the program, or `"git โ€ฆ"` as the head of a shell line handed to `exec`. */
59
+ const isGit = (v) =>
60
+ typeof v === "string" && (v === "git" || v.startsWith("git "));
61
+
62
+ /** Rule sources only: a mutation file plants defects on purpose, a harness asserts about them. */
63
+ const isRuleSource = (f) =>
64
+ f.endsWith(".mjs") &&
65
+ !f.endsWith(".mutations.mjs") &&
66
+ !f.endsWith(".harness.mjs");
67
+
68
+ /** @param {string} src @returns {string[]} the git invocations this source makes */
69
+ export function processImports(src) {
70
+ const ast = parse(src, {
71
+ ecmaVersion: "latest",
72
+ sourceType: "module",
73
+ range: false,
74
+ });
75
+ const found = [];
76
+ const walk = (n) => {
77
+ if (n === null || typeof n !== "object") return;
78
+ if (Array.isArray(n)) return n.forEach(walk);
79
+ // Both spellings of the callee: `execFileSync(โ€ฆ)` after a named import, and
80
+ // `cp.execFileSync(โ€ฆ)` after a namespace import. Listed, not described โ€” the node types a
81
+ // check walks are the only honest statement of its coverage.
82
+ if (n.type === "CallExpression") {
83
+ const name =
84
+ n.callee?.type === "MemberExpression"
85
+ ? n.callee.property?.name
86
+ : n.callee?.name;
87
+ if (SPAWNERS.has(String(name)) && isGit(n.arguments?.[0]?.value))
88
+ found.push(String(n.arguments[0].value));
89
+ }
90
+ for (const k of Object.keys(n)) if (k !== "parent") walk(n[k]);
91
+ };
92
+ walk(ast);
93
+ return found;
94
+ }
95
+
96
+ /** @param {{cwd: string}} options */
97
+ export function rulesAreContentOnly({ cwd }) {
98
+ const dir = join(cwd, "eslint-rules");
99
+ const checked = [];
100
+ const findings = [];
101
+ for (const f of readdirSync(dir).filter(isRuleSource)) {
102
+ const hits = processImports(readFileSync(join(dir, f), "utf8"));
103
+ // ๐Ÿ”ด The counter increments WITH the verdict, not before it: a `checked` that keeps
104
+ // counting while the verdict stops being reached is the failure recorded in the consumer's
105
+ // rules as "a counter that counts what it ignores".
106
+ checked.push(f);
107
+ for (const h of hits) findings.push({ file: f, specifier: h });
108
+ }
109
+ return { checked, findings };
110
+ }
111
+
112
+ if (isMain(import.meta.url)) {
113
+ // ๐Ÿ”ด `process.cwd()`, NOT the package root. A consumer installs this package and runs the
114
+ // script from its own repository, where the rules that matter are ITS `eslint-rules/` โ€” the
115
+ // package's own are already checked by the package's own gate. Anchored to the package root
116
+ // the consumer's gate would re-check the same eight files and report a confident zero about
117
+ // rules it never opened: a counter that counts what it ignores.
118
+ const cwd = process.cwd();
119
+ const { checked, findings } = rulesAreContentOnly({ cwd });
120
+ if (checked.length === 0) {
121
+ console.error(
122
+ `${join(cwd, "eslint-rules")}: no rule sources. Either this is not a repository with ` +
123
+ `ESLint rules, or the directory moved โ€” a silent zero here would read as a clean run.`,
124
+ );
125
+ process.exit(1);
126
+ }
127
+ for (const { file, specifier } of findings) {
128
+ console.error(
129
+ `${file}: imports \`${specifier}\`. A rule may not ask git: history is rewritten by ` +
130
+ `routine maintenance and CI clones one commit deep, so the fact does not survive where ` +
131
+ `the rule has to hold. Put the fact on disk and check it by bytes.`,
132
+ );
133
+ }
134
+ console.log(
135
+ `rules-are-content-only: ${checked.length} rule sources under ${cwd}, ${findings.length} findings`,
136
+ );
137
+ process.exit(findings.length === 0 ? 0 : 1);
138
+ }
@@ -0,0 +1,81 @@
1
+ /**
2
+ * Battery for the "rule does not touch git" gate, `scripts/rules-are-content-only.mjs`.
3
+ *
4
+ * ๐Ÿ”ด This battery needs to be sharper than the rule. Its success state is an EMPTY LIST, which is
5
+ * exactly the state it teaches not to trust. A check seen only silent is indistinguishable from
6
+ * one that never looks.
7
+ *
8
+ * BOTH DIRECTIONS, and they must die on DIFFERENT assertions:
9
+ * - `undercatch` โ€” stops finding anything. The "quiet on clean" half stays green;
10
+ * the "fires" half must fail.
11
+ * - `overcatch` โ€” calls any spawner a finding. The "fires" half stays green
12
+ * (it still sees `git`), the "quiet on clean" half must fail. MEASURED: it breaks on
13
+ * LIVE CORPUS (6 findings in the package's own rules) before a planted fixture with
14
+ * `texcount` โ€” and that is stronger, not weaker: real data.
15
+ * A battery where both mutations die on one assertion proves ONE half exists.
16
+ *
17
+ * โš ๏ธ The second mutation reproduces a REAL false positive: the first version of the check
18
+ * was keyed on the `child_process` import and in the first hour called `paper-texcount.mjs`
19
+ * a finding in the consumer โ€” it calls `texcount` to count words. On an `error` gate this is
20
+ * worse than a miss: a gate you cannot pass gets turned off. The mutation holds this line.
21
+ */
22
+ import { fileURLToPath } from "node:url";
23
+ import { dirname, join, resolve } from "node:path";
24
+ import { runMutations } from "../lib/mutation-driver.mjs";
25
+
26
+ const HERE = dirname(fileURLToPath(import.meta.url));
27
+ const ROOT = resolve(HERE, "..");
28
+ const GUARD = join(HERE, "rules-are-content-only.mjs");
29
+ const HARNESS = join(HERE, "rules-are-content-only.harness.mjs");
30
+
31
+ process.exit(
32
+ runMutations({
33
+ root: ROOT,
34
+ runner: "node",
35
+ cases: [
36
+ {
37
+ name: "undercatch โ€” check stops finding",
38
+ harness: HARNESS,
39
+ expect: "FIRES on execFileSync with a git argv",
40
+ disables:
41
+ "the verdict itself: the check becomes that very confident green zero it was written " +
42
+ "to spot โ€” it scans, counts files and renders no judgment",
43
+ edits: [
44
+ [
45
+ GUARD,
46
+ "if (SPAWNERS.has(String(name)) && isGit(n.arguments?.[0]?.value))",
47
+ "if (false && SPAWNERS.has(String(name)) && isGit(n.arguments?.[0]?.value))",
48
+ ],
49
+ ],
50
+ },
51
+ {
52
+ name: "overcatch โ€” any spawner call becomes a finding",
53
+ harness: HARNESS,
54
+ // Fails on LIVE CORPUS, not on the texcount fixture, and this is measured: under this mutation
55
+ // the package's own rules yield 6 findings, meaning the "quiet on clean" half breaks
56
+ // before we reach the planted case. Live signal is stronger than fixture โ€” it is real data โ€” so
57
+ // it is named, not the one I expected to see.
58
+ expect: "quiet on the real corpus",
59
+ disables:
60
+ "the boundary of the predicate 'calls git' vs 'spawns a process' โ€” the one that was " +
61
+ "set after the texcount false positive in the consumer",
62
+ edits: [
63
+ [
64
+ GUARD,
65
+ "if (SPAWNERS.has(String(name)) && isGit(n.arguments?.[0]?.value))",
66
+ "if (SPAWNERS.has(String(name)))",
67
+ ],
68
+ ],
69
+ },
70
+ {
71
+ name: "guard for empty scan stops failing",
72
+ harness: HARNESS,
73
+ expect: "scanned something",
74
+ disables:
75
+ "the answer to 'did you even look': zero found sources become silent success again, " +
76
+ "indistinguishable from a clean run",
77
+ edits: [[GUARD, 'f.endsWith(".mjs") &&', "false &&"]],
78
+ },
79
+ ],
80
+ }),
81
+ );
@@ -0,0 +1,115 @@
1
+ /**
2
+ * Colocated test for `scripts/rules-see-files.mjs` โ€” the guard against a green zero.
3
+ * Run: `npx vigiles test scripts/rules-see-files.harness.mjs`
4
+ *
5
+ * ๐Ÿ”ด BOTH HALVES, and for this guard the order matters more than usual. Its success state is
6
+ * silence, and silence is exactly the state it was built to distrust: a guard that has only
7
+ * ever been seen quiet is indistinguishable from a guard that is dead. So:
8
+ * I. QUIET on this repository โ€” every rule declared in `eslint.config.mjs` is enabled for at
9
+ * least one file that exists.
10
+ * II. FIRES on a planted empty glob โ€” a throwaway project whose config declares a rule over a
11
+ * glob nothing matches. The rule is syntactically fine, the config is valid, ESLint exits
12
+ * 0 with zero findings, and the guard is the only thing that says anything.
13
+ * III. The fixture in II has a CONTROL rule that does see a file, so "fires" means "named the
14
+ * blind one", not "complained about everything".
15
+ */
16
+ import assert from "node:assert/strict";
17
+ import { ESLint } from "eslint";
18
+ import {
19
+ mkdirSync,
20
+ mkdtempSync,
21
+ rmSync,
22
+ writeFileSync,
23
+ realpathSync,
24
+ } from "node:fs";
25
+ import { tmpdir } from "node:os";
26
+ import { dirname, join } from "node:path";
27
+ import { fileURLToPath } from "node:url";
28
+
29
+ const HERE = dirname(fileURLToPath(import.meta.url));
30
+ const ROOT = join(HERE, "..");
31
+ const { rulesSeeFiles } = await import(join(HERE, "rules-see-files.mjs"));
32
+
33
+ const TMP = realpathSync(mkdtempSync(join(tmpdir(), "rules-see-files-")));
34
+ process.on("exit", () => rmSync(TMP, { recursive: true, force: true }));
35
+
36
+ // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
37
+ // I. QUIET ON THIS REPOSITORY
38
+ // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
39
+ {
40
+ const { rules, blind, linted } = await rulesSeeFiles({ cwd: ROOT });
41
+ assert.deepEqual(
42
+ blind,
43
+ [],
44
+ "a rule declared in eslint.config.mjs was enabled for zero files on disk โ€” it is never " +
45
+ "invoked, and its zero findings mean nothing",
46
+ );
47
+ assert.ok(
48
+ rules.length > 0,
49
+ "no rules were found in eslint.config.mjs โ€” the guard has no subject",
50
+ );
51
+ assert.ok(
52
+ linted > 0,
53
+ "ESLint linted no files at all โ€” the guard cannot distinguish anything",
54
+ );
55
+ // Not just "not blind": the count has to be the fixtures it should be seeing. A rule that saw
56
+ // one file when four exist is a narrower defect of the same family.
57
+ const tex = rules.find((r) => r.rule === "tex/future-promise");
58
+ assert.ok(tex, "`tex/future-promise` is no longer declared");
59
+ assert.ok(
60
+ tex.files >= 4,
61
+ `\`tex/future-promise\` was enabled for ${tex.files} file(s); the four fixtures alone should ` +
62
+ "exceed that, so its glob is narrower than the test data it is supposed to cover",
63
+ );
64
+ }
65
+
66
+ // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
67
+ // II + III. FIRES ON A PLANTED EMPTY GLOB, AND NAMES THE RIGHT RULE
68
+ // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
69
+ {
70
+ const project = join(TMP, "planted");
71
+ mkdirSync(project, { recursive: true });
72
+ writeFileSync(join(project, "probe.js"), "export const x = 1;\n");
73
+ writeFileSync(
74
+ join(project, "eslint.config.mjs"),
75
+ [
76
+ "const noop = { meta: { schema: [] }, create: () => ({}) };",
77
+ "export default [",
78
+ " {",
79
+ ' files: ["**/*.js"],',
80
+ " plugins: { probe: { rules: { alive: noop, dead: noop } } },",
81
+ ' rules: { "probe/alive": "warn" },',
82
+ " },",
83
+ " {",
84
+ // Nothing in this project has that extension. The block is valid, the rule is valid,
85
+ // and it will never be called.
86
+ ' files: ["**/*.nosuchextension"],',
87
+ " plugins: { probe: { rules: { alive: noop, dead: noop } } },",
88
+ ' rules: { "probe/dead": "warn" },',
89
+ " },",
90
+ "];",
91
+ "",
92
+ ].join("\n"),
93
+ );
94
+
95
+ // First: confirm the thing the guard exists to distrust. ESLint itself is perfectly happy.
96
+ const results = await new ESLint({ cwd: project }).lintFiles(["."]);
97
+ assert.deepEqual(
98
+ results.flatMap((r) => r.messages),
99
+ [],
100
+ "the planted project produced findings โ€” it is supposed to be the silent, green, and wrong case",
101
+ );
102
+
103
+ const { blind, rules } = await rulesSeeFiles({ cwd: project });
104
+ assert.deepEqual(
105
+ blind,
106
+ ["probe/dead"],
107
+ "the guard did not name the rule declared over an empty glob",
108
+ );
109
+ assert.equal(
110
+ rules.find((r) => r.rule === "probe/alive").files,
111
+ 1,
112
+ "the control rule was not credited with the file it does see โ€” the guard reports blindness " +
113
+ "for everything rather than for the blind one",
114
+ );
115
+ }
@@ -0,0 +1,99 @@
1
+ /**
2
+ * THE GUARD AGAINST A GREEN ZERO.
3
+ *
4
+ * ๐Ÿ”ด The defect it exists for, measured 2026-09-10 on the source corpus: a lint rule whose glob
5
+ * matches NO file is never invoked. The run exits 0 with no findings โ€” byte-identical to the
6
+ * run of a rule that examined everything and passed. In that corpus a fresh clone produced
7
+ * RC=0 and 652 findings, and NINETEEN rules had seen no file at all. Nothing in the output said
8
+ * so, because there is nothing for a rule to say when it is not called.
9
+ *
10
+ * โ”€โ”€ WHY THIS CANNOT LIVE INSIDE A RULE โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
11
+ * A rule cannot report that it was not invoked; not being invoked is precisely the state in
12
+ * which it has no opportunity to report anything. The check therefore has to stand OUTSIDE the
13
+ * rules, look at the config and at the files on disk, and answer a question no individual rule
14
+ * can ask: "was anything actually handed to you?"
15
+ *
16
+ * โ”€โ”€ WHAT IT CHECKS, AND WHY THAT AND NOT "THE GLOB IS NON-EMPTY" โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
17
+ * Per RULE, not per glob. The unit that goes silent is a rule, and a rule can be enabled in one
18
+ * block whose glob is empty while a completely different block is busy โ€” a non-empty glob
19
+ * somewhere in the config is not evidence about the rule you care about. So: collect every rule
20
+ * declared at a severity other than `off`, lint the repository, and for each linted file ask
21
+ * ESLint what the effective config is. A rule enabled for zero linted files is BLIND.
22
+ *
23
+ * Run: `node scripts/rules-see-files.mjs` (exit 1 and a named rule when something is blind)
24
+ * Tested by: `scripts/rules-see-files.harness.mjs` โ€” both halves, quiet here and firing on a
25
+ * planted empty glob.
26
+ */
27
+ import { ESLint } from "eslint";
28
+ import { isMain } from "../skills/paper-pipeline/scripts/consumer.mjs";
29
+
30
+ /** ESLint accepts severities as strings and as numbers; `off` and `0` are the same thing. */
31
+ const isOff = (v) => {
32
+ const s = Array.isArray(v) ? v[0] : v;
33
+ return s === "off" || s === 0;
34
+ };
35
+
36
+ /**
37
+ * @param {{cwd: string}} options
38
+ * @returns {Promise<{rules: {rule: string, files: number}[], linted: number, blind: string[]}>}
39
+ */
40
+ export async function rulesSeeFiles({ cwd }) {
41
+ const configPath = new URL(
42
+ "eslint.config.mjs",
43
+ `file://${cwd.endsWith("/") ? cwd : cwd + "/"}`,
44
+ );
45
+ const config = (await import(configPath.href)).default;
46
+
47
+ const declared = new Set();
48
+ for (const block of config)
49
+ for (const [id, severity] of Object.entries(block.rules ?? {}))
50
+ if (!isOff(severity)) declared.add(id);
51
+
52
+ const eslint = new ESLint({ cwd });
53
+ // `errorOnUnmatchedPattern: false` is NOT set on purpose. A repository where `.` matches
54
+ // nothing at all is a different failure, and it should be loud in its own words rather than
55
+ // arrive here disguised as "every rule is blind".
56
+ const results = await eslint.lintFiles(["."]);
57
+
58
+ const seen = new Map([...declared].map((id) => [id, 0]));
59
+ for (const result of results) {
60
+ const effective = await eslint.calculateConfigForFile(result.filePath);
61
+ for (const id of declared)
62
+ if (effective.rules?.[id] && !isOff(effective.rules[id]))
63
+ seen.set(id, seen.get(id) + 1);
64
+ }
65
+
66
+ const rules = [...seen]
67
+ .map(([rule, files]) => ({ rule, files }))
68
+ .sort((a, b) => a.rule.localeCompare(b.rule));
69
+ return {
70
+ rules,
71
+ linted: results.length,
72
+ blind: rules.filter((r) => r.files === 0).map((r) => r.rule),
73
+ };
74
+ }
75
+
76
+ // CLI entry. Guarded so the harness can import the function without running the process exit.
77
+ // ๐Ÿ”ด `isMain`, NOT `import.meta.url === `file://${process.argv[1]}``. Node resolves the entry
78
+ // point to its REAL path for `import.meta.url` but leaves `process.argv[1]` as typed, so through
79
+ // a symlink the two are not equal and the CLI silently does not run โ€” the process exits 0 having
80
+ // done nothing. The consumer reaches these scripts through exactly such a symlink. Observed 14.09
81
+ // on run 34784079821: `extract-pdf-facts.mjs --strict` returned RC=0 and created no facts file.
82
+ if (isMain(import.meta.url)) {
83
+ const { rules, linted, blind } = await rulesSeeFiles({ cwd: process.cwd() });
84
+ for (const { rule, files } of rules)
85
+ console.log(`${String(files).padStart(4)} ${rule}`);
86
+ console.log(`\n${rules.length} rule(s) declared, ${linted} file(s) linted.`);
87
+ if (blind.length) {
88
+ console.log(
89
+ `\nโœ— ${blind.length} rule(s) saw NO file and were never invoked:\n` +
90
+ blind.map((r) => ` ${r}`).join("\n") +
91
+ "\n\n A rule that is never invoked reports exactly what a rule that passed reports.\n" +
92
+ " Fix the glob it is declared on, or delete the declaration โ€” do not leave it green.",
93
+ );
94
+ process.exit(1);
95
+ }
96
+ console.log(
97
+ "โœ“ every declared rule was enabled for at least one file on disk.",
98
+ );
99
+ }
@@ -0,0 +1,131 @@
1
+ /**
2
+ * Mutation battery for the guard against a green zero, `scripts/rules-see-files.mjs`.
3
+ * Run: `node scripts/rules-see-files.mutations.mjs` (not `vigiles test` โ€” not a harness).
4
+ *
5
+ * ๐Ÿ”ด WHY THIS GUARD NEEDS A MUTATION BATTERY MORE THAN ANYTHING ELSE HERE. Its success state
6
+ * is an empty list, which is the exact state it was built to distrust. A guard that has only
7
+ * been observed quiet is indistinguishable from a guard that does nothing โ€” that is the whole
8
+ * premise of the thing being guarded, applied to the guard itself.
9
+ *
10
+ * BOTH DIRECTIONS, one mutation each, and they must die on DIFFERENT assertions:
11
+ * - `under-report` makes the guard incapable of ever calling a rule blind. The quiet half of
12
+ * its harness stays green; the FIRING half must go red.
13
+ * - `over-report` makes it call everything blind. The firing half stays green (it still names
14
+ * `probe/dead`, among others); the QUIET half must go red.
15
+ * A battery in which both mutations died on the same assertion would prove only that one of the
16
+ * two halves exists.
17
+ */
18
+ import { readFileSync, writeFileSync } from "node:fs";
19
+ import { execFileSync } from "node:child_process";
20
+
21
+ const GUARD = "scripts/rules-see-files.mjs";
22
+ const HARNESS = "scripts/rules-see-files.harness.mjs";
23
+
24
+ // Coverage mode โ€” see `scripts/run-mutations.mjs` guard 2.
25
+ if (process.env.MUTATIONS_REPORT_COVERAGE) {
26
+ console.log(`MUTATION-COVERS\t${HARNESS}`);
27
+ process.exit(0);
28
+ }
29
+
30
+ const PRISTINE = readFileSync(GUARD, "utf8");
31
+
32
+ const M = [
33
+ [
34
+ "under-report",
35
+ "never call a rule blind โ€” the guard becomes the silent green it exists to catch",
36
+ "blind: rules.filter((r) => r.files === 0).map((r) => r.rule)",
37
+ "blind: [] /* MUT */",
38
+ ],
39
+ [
40
+ "over-report",
41
+ "call every rule blind โ€” the guard becomes noise, and noise gets switched off within a day",
42
+ " if (effective.rules?.[id] && !isOff(effective.rules[id]))\n seen.set(id, seen.get(id) + 1);",
43
+ " if (false)\n seen.set(id, seen.get(id) + 1); /* MUT */",
44
+ ],
45
+ ];
46
+
47
+ const runHarness = () => {
48
+ try {
49
+ execFileSync("npx", ["vigiles", "test", HARNESS], {
50
+ encoding: "utf8",
51
+ stdio: ["ignore", "pipe", "pipe"],
52
+ });
53
+ return { failed: false, out: "" };
54
+ } catch (e) {
55
+ return { failed: true, out: (e.stdout ?? "") + (e.stderr ?? "") };
56
+ }
57
+ };
58
+
59
+ {
60
+ const base = runHarness();
61
+ if (base.failed) {
62
+ console.log(
63
+ "โŒ THE HARNESS IS RED BEFORE ANY MUTATION โ€” the battery cannot tell a killed mutation " +
64
+ "from that:\n" +
65
+ base.out.slice(-1500),
66
+ );
67
+ process.exit(1);
68
+ }
69
+ }
70
+
71
+ let ok = 0;
72
+ let bad = 0;
73
+ const rows = [];
74
+ for (const [label, what, from, to] of M) {
75
+ const hits = PRISTINE.split(from).length - 1;
76
+ if (hits !== 1) {
77
+ console.log(
78
+ `โŒ ${label}: TARGET ${hits === 0 ? "NOT FOUND" : `NOT UNIQUE (${hits})`} โ€” ${from.slice(0, 70)}`,
79
+ );
80
+ bad++;
81
+ continue;
82
+ }
83
+ writeFileSync(GUARD, PRISTINE.replace(from, to));
84
+ const on = readFileSync(GUARD, "utf8");
85
+ if (!on.includes(to) || on.includes(from)) {
86
+ console.log(
87
+ `โŒ ${label}: THE MUTATION DID NOT LAND (checked by re-reading the file)`,
88
+ );
89
+ bad++;
90
+ writeFileSync(GUARD, PRISTINE);
91
+ continue;
92
+ }
93
+ const { failed, out } = runHarness();
94
+ writeFileSync(GUARD, PRISTINE);
95
+ if (readFileSync(GUARD, "utf8") !== PRISTINE) {
96
+ console.log(`โŒ ${label}: THE ROLLBACK FAILED`);
97
+ bad++;
98
+ continue;
99
+ }
100
+ const at = (out.match(/rules-see-files\.harness\.mjs:(\d+)/) ?? [])[1];
101
+ const msg = (out.match(/AssertionError[^:]*: ([^\n]+)/) ?? [])[1] ?? "";
102
+ if (failed) {
103
+ ok++;
104
+ rows.push([
105
+ label,
106
+ what,
107
+ ((at ? `harness:${at} ยท ` : "") + msg).trim().slice(0, 130),
108
+ ]);
109
+ } else {
110
+ console.log(
111
+ `๐Ÿ”ด ${label}: THE HARNESS IS GREEN UNDER THE MUTATION โ€” a finding about the TEST, not a conclusion about the guard`,
112
+ );
113
+ bad++;
114
+ }
115
+ }
116
+ console.log("\n| direction | mutation | what the harness died on |");
117
+ console.log("|---|---|---|");
118
+ for (const [a, b, c] of rows)
119
+ console.log(`| \`${a}\` | ${b} | ${c.replace(/\|/g, "\\|")} |`);
120
+ // The two must die on DIFFERENT lines, or only one half of the guard is actually tested.
121
+ const lines = rows.map(
122
+ ([, , where]) => (where.match(/harness:(\d+)/) ?? [])[1],
123
+ );
124
+ if (rows.length === 2 && lines[0] && lines[0] === lines[1]) {
125
+ console.log(
126
+ "\n๐Ÿ”ด BOTH mutations died on the SAME assertion โ€” only one half of the guard is tested",
127
+ );
128
+ bad++;
129
+ }
130
+ console.log(`\n${ok} mutation(s) killed, ${bad} problem(s)`);
131
+ process.exit(bad ? 1 : 0);