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,215 @@
1
+ /**
2
+ * trigger-ledger.mjs — "this description was measured, here is when and with what result".
3
+ *
4
+ * WHY. Trigger evals cost money and are therefore not in CI ("Not CI. Run it deliberately" —
5
+ * that is what every `*.eval.mjs` says). The consequence, measured 2026-08-28: recall is a
6
+ * quantity obtained ONCE, not one that is maintained. Let anyone reword a `description`
7
+ * tomorrow — nobody will ever catch the firing regression, because there is nothing and
8
+ * nobody to catch it with.
9
+ *
10
+ * 🔴 WHY THE READY-MADE `skillHash()` WAS NOT TAKEN. It hashes the WHOLE skill directory
11
+ * (`.md`, `.mjs`, `.sh`, `.py`, `.ts`). EXACTLY ONE FIELD affects the model's choice of a
12
+ * skill — `description`: the eval kit sets `stubSkillBodies: true` with the comment
13
+ * "selection is decided by frontmatter; don't run the body". Had we taken the whole
14
+ * directory, the gate would redden at a typo in the body and at every new harness, that is,
15
+ * almost always. A check that is red for no reason lives one day, gets silenced, and the hole
16
+ * comes back, this time unnoticed. So the field is what gets hashed.
17
+ *
18
+ * ⚠️ WHAT THIS LEDGER DOES NOT KNOW. It does not know that a description got BETTER or WORSE
19
+ * — only that it is DIFFERENT. The verdict still comes from the paid run; all that is
20
+ * recorded here is that the run relates to this text and not to some earlier one.
21
+ *
22
+ * vigiles:local-by-design — the generic version ("coverage must cover the eval tier, not only
23
+ * the harness") belongs to the product: `.vigiles/coverage.json` is written by the CLI from a
24
+ * completed run, while `*.eval.mjs` are started as `node <file>` past the CLI, so there are
25
+ * ZERO evals there against 50 harness records (measured 2026-08-28). What stays here is ours:
26
+ * WHICH of our descriptions were measured and with what number.
27
+ */
28
+ import { createHash } from "node:crypto";
29
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
30
+ import { join } from "node:path";
31
+ import {
32
+ consumerRoot,
33
+ installedSkills,
34
+ } from "../skills/paper-pipeline/scripts/consumer.mjs";
35
+
36
+ // The CONSUMER's root, not this file's grandparent — see the same note in `skill-corpus.mjs`.
37
+ // The ledger itself (`.claude/skill-trigger-runs.json`) is the consumer's DATA and stays there;
38
+ // only the code that reads and writes it lives here.
39
+ export const ROOT = consumerRoot();
40
+ export const LEDGER = join(ROOT, ".claude", "skill-trigger-runs.json");
41
+ const SKILLS = join(ROOT, ".claude", "skills");
42
+
43
+ // THE DEBT BASELINE WAS DELETED 2026-08-28. A ratchet with 24 names "never measured once"
44
+ // stood here — and it was a way of not doing the work. The corpus owner: "a ratchet again, fuck…
45
+ // let's just fix it properly". All 24 were run (~40 min of machine time, $0 metered —
46
+ // subscription), the debt is paid off, the check became an ordinary gate. Its meaning changed
47
+ // with that: it was "the debt does not grow", it became "a measurement must relate to the
48
+ // current description". The same transition as the markdown-regex debt, and for the same
49
+ // reason — paying it off is cheaper than servicing it.
50
+
51
+ /** The exact text of `description:` from the frontmatter. Empty string if the field is absent. */
52
+ export function descriptionOf(skill) {
53
+ const p = join(SKILLS, skill, "SKILL.md");
54
+ if (!existsSync(p)) return "";
55
+ const src = readFileSync(p, "utf8");
56
+ const fm = src.match(/^---\r?\n([\s\S]*?)\r?\n---/);
57
+ if (!fm) return "";
58
+ // description may be multi-line, up to the next top-level key
59
+ const m = fm[1].match(
60
+ /^description:[ \t]*([\s\S]*?)(?=\r?\n[a-zA-Z_-]+:|$)/m,
61
+ );
62
+ return m ? m[1].trim() : "";
63
+ }
64
+
65
+ export const hashOf = (s) =>
66
+ createHash("sha256").update(s).digest("hex").slice(0, 16);
67
+
68
+ /**
69
+ * A hash of the WHOLE set of descriptions — "against whom it was measured".
70
+ *
71
+ * 🔴 Why a second mark (the corpus owner's question, 2026-08-28). A trigger eval measures the
72
+ * model's CHOICE out of all installed descriptions — the kit says exactly that: "whole-harness by
73
+ * construction… an isolated measurement OVERSTATES recall and UNDERSTATES false positives". So my
74
+ * recall can go stale from an edit to a NEIGHBOUR'S DESCRIPTION: it starts winning my prompts
75
+ * while my own description does not change by a single character. A hash of my own field is not
76
+ * enough for that.
77
+ *
78
+ * ⚠️ But hashing the corpus STRICTLY is not allowed either: one edit to any description would
79
+ * void every record at once, and the check would again be always-red. Hence the different
80
+ * loudness:
81
+ * my own description changed → THE MEASUREMENT IS INVALID (an error);
82
+ * the set of competitors changed → the measurement WEAKENED (a note in the ledger, no shouting).
83
+ */
84
+ export function corpusHash() {
85
+ const names = installedSkills(SKILLS);
86
+ return hashOf(
87
+ names.map((n) => n + "\u0000" + descriptionOf(n)).join("\u0001"),
88
+ );
89
+ }
90
+
91
+ export const readLedger = () =>
92
+ existsSync(LEDGER)
93
+ ? JSON.parse(readFileSync(LEDGER, "utf8"))
94
+ : { v: 1, runs: {} };
95
+
96
+ /** Record a measurement that happened. Called by the kit AFTER the run, not before. */
97
+ export function recordRun(skill, { recall, precision, model, trials }) {
98
+ const l = readLedger();
99
+ l.runs[skill] = {
100
+ description_sha: hashOf(descriptionOf(skill)),
101
+ corpus_sha: corpusHash(),
102
+ measured_at: new Date().toISOString().slice(0, 10),
103
+ recall,
104
+ precision,
105
+ model,
106
+ trials,
107
+ };
108
+ writeFileSync(LEDGER, JSON.stringify(l, null, 2) + "\n");
109
+ return l.runs[skill];
110
+ }
111
+
112
+ /**
113
+ * Skills that HAVE an `*.eval.mjs` but whose measurement does not relate to the current
114
+ * description. Returns [{skill, reason}]. `reason` — 'never' or 'changed'.
115
+ */
116
+ export function staleSkills(skillsWithEval) {
117
+ const l = readLedger();
118
+ const out = [];
119
+ for (const s of skillsWithEval) {
120
+ const rec = l.runs[s];
121
+ if (!rec) {
122
+ out.push({ skill: s, reason: "never" });
123
+ continue;
124
+ }
125
+ if (rec.description_sha !== hashOf(descriptionOf(s))) {
126
+ out.push({ skill: s, reason: "changed", measured_at: rec.measured_at });
127
+ continue;
128
+ }
129
+ // The skill's own description is the same but the competitors differ — the measurement
130
+ // weakened, it did not die.
131
+ if (rec.corpus_sha && rec.corpus_sha !== corpusHash())
132
+ out.push({
133
+ skill: s,
134
+ reason: "corpus",
135
+ measured_at: rec.measured_at,
136
+ soft: true,
137
+ });
138
+ }
139
+ return out;
140
+ }
141
+
142
+ // ── CLI: `node .claude/lib/trigger-ledger.mjs --check` ────────────────────────────
143
+ // Cheap: no model, no network — only reading frontmatter and comparing hashes.
144
+ // That is exactly why this can be kept in CI, unlike the evals themselves.
145
+ if (process.argv[1] && process.argv[1].endsWith("trigger-ledger.mjs")) {
146
+ const withEval = installedSkills(SKILLS).filter((d) =>
147
+ existsSync(join(SKILLS, d, `${d}.eval.mjs`)),
148
+ );
149
+ // A RATCHET, not a gate. Measured 2026-08-28 on a clean tree: 24 findings "never measured
150
+ // once" — for the paper skills firing had been measured earlier, just not into this ledger,
151
+ // which is five minutes old. A check with 24 red lines on its first day does not get read, it
152
+ // gets silenced: that is exactly how the markdown-regex debt already died in this repo, and
153
+ // there it was turned into a ratchet.
154
+ //
155
+ // So: "the description changed after the measurement" — LOUD ALWAYS, that is the very
156
+ // regression all of this was started for. "Never measured once" — loud only for skills that
157
+ // are not in the debt baseline, that is, for NEW ones. The debt does not grow silently, but it
158
+ // does not shout on every run either.
159
+ const all = staleSkills(withEval);
160
+ const soft = all.filter((x) => x.soft);
161
+ const stale = all.filter((x) => !x.soft);
162
+ // ONE line, not one line per skill. Measured 2026-08-28: an edit to ONE description changes
163
+ // the corpus hash as a whole, and per-line output produced 27 notes at once. Twenty-seven
164
+ // informational lines per edit is noise, and noise gets silenced; and then the hard half stops
165
+ // being read along with it.
166
+ // Three names + a counter, not only a counter and not 27 lines. A bare counter cannot be
167
+ // checked and gives nothing to start a re-measurement from; one line per skill is 27 lines of
168
+ // noise per edit (measured 2026-08-28). The assertion "the note is addressed" in the harness
169
+ // requires exactly the names — and it was RED from 2026-08-28 to 2026-08-31, because the code
170
+ // printed only the counter: the harness was written, declared working, and not run.
171
+ if (soft.length) {
172
+ const names = soft
173
+ .slice(0, 3)
174
+ .map((x) => x.skill)
175
+ .join(", ");
176
+ console.log(
177
+ `ℹ️ the set of competing descriptions changed after the last measurement — ` +
178
+ `for ${soft.length} skill(s) recall may have shifted without their own description being ` +
179
+ `touched (${names}${soft.length > 3 ? ` and ${soft.length - 3} more` : ""}). ` +
180
+ `Not an error: re-measure when convenient.`,
181
+ );
182
+ }
183
+ // 🔴 THE DENOMINATOR IS MANDATORY. The line "✓ current for 27" reads as "27 skills are fine",
184
+ // whereas the true statement is "27 of 50 fall under this check at all": a skill without its
185
+ // own .eval.mjs is inexpressible for this gate, there is nothing to re-measure for it.
186
+ // Measured 2026-08-31: 50 skills on disk, 27 with an eval, 23 without. That day I changed the
187
+ // description of telegram-channel-audit (no eval), the gate printed green, and that looked
188
+ // like "the change was verified". A counter that does not name what it ignores lies — the
189
+ // CLAUDE.md rule §"A counter that counts what it IGNORES".
190
+ const total = installedSkills(SKILLS).length;
191
+ const noEval = total - withEval.length;
192
+ if (!stale.length) {
193
+ console.log(
194
+ `✓ trigger measurements are current for ${withEval.length} skill(s) of ${total}` +
195
+ (noEval
196
+ ? ` — the other ${noEval} have NO .eval.mjs of their own, they are outside this ` +
197
+ `check by construction (nothing to measure, not "fine")`
198
+ : ""),
199
+ );
200
+ process.exit(0);
201
+ }
202
+ for (const s of stale) {
203
+ console.error(
204
+ s.reason === "never"
205
+ ? `✗ ${s.skill}: ${s.skill}.eval.mjs exists, but a firing measurement NEVER happened`
206
+ : `✗ ${s.skill}: description changed after the ${s.measured_at} measurement — recall relates to the former text`,
207
+ );
208
+ }
209
+ console.error(
210
+ `\nRun it: node .claude/skills/<skill>/<skill>.eval.mjs` +
211
+ `\nThis is the paid tier (a real model), so CI does not run it — it only requires that a run ` +
212
+ `exists and relates to the current description.`,
213
+ );
214
+ process.exit(1);
215
+ }
package/package.json ADDED
@@ -0,0 +1,97 @@
1
+ {
2
+ "name": "paperlint",
3
+ "version": "2.0.0",
4
+ "description": "Machine-checkable gates for a research paper kept in git: ESLint rules over .tex/.md paper sources, plus the skills and hooks that drive each stage.",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "scripts": {
8
+ "build": "tsc",
9
+ "prepare": "tsc",
10
+ "test": "node scripts/exclusive.mjs vitest run && node scripts/exclusive.mjs vigiles test --min=1",
11
+ "lint": "node scripts/exclusive.mjs eslint .",
12
+ "check": "node scripts/check.mjs",
13
+ "test:e2e": "node scripts/exclusive.mjs node test/e2e/install.mjs && node scripts/exclusive.mjs node test/e2e/build.mjs",
14
+ "fmt": "prettier --write .",
15
+ "fmt:check": "prettier --check ."
16
+ },
17
+ "dependencies": {
18
+ "@eslint/markdown": "^8.0.3",
19
+ "@eslint/plugin-kit": "^0.7.2",
20
+ "@unified-latex/unified-latex-util-parse": "^1.8.4",
21
+ "ajv": "^6.12.6",
22
+ "eslint": "^10.9.1",
23
+ "js-yaml": "^4.1.0",
24
+ "markdown-it": "^14.3.0",
25
+ "ts-essentials": "^10.2.1",
26
+ "typescript": "^5.9.3",
27
+ "unpdf": "^1.8.1",
28
+ "vigiles": "^31.0.1",
29
+ "zod": "^4.6.5"
30
+ },
31
+ "devDependencies": {
32
+ "@retorquere/bibtex-parser": "^10.0.1",
33
+ "@semantic-release/commit-analyzer": "13.0.1",
34
+ "@semantic-release/release-notes-generator": "14.1.1",
35
+ "@types/node": "^26.6.1",
36
+ "conventional-changelog-conventionalcommits": "9.3.1",
37
+ "eslint-plugin-boundaries": "^7.2.0",
38
+ "eslint-plugin-n": "^18.3.0",
39
+ "prettier": "^3.8.1",
40
+ "semantic-release": "25.0.9",
41
+ "typescript-eslint": "^8.70.1",
42
+ "vitest": "^4.1.11"
43
+ },
44
+ "peerDependencies": {
45
+ "@retorquere/bibtex-parser": "^10.0.1"
46
+ },
47
+ "peerDependenciesMeta": {
48
+ "@retorquere/bibtex-parser": {
49
+ "optional": true
50
+ }
51
+ },
52
+ "repository": {
53
+ "type": "git",
54
+ "url": "git+https://github.com/zernie/research-paper-pipeline.git"
55
+ },
56
+ "keywords": [
57
+ "eslint",
58
+ "eslintplugin",
59
+ "latex",
60
+ "tex",
61
+ "research",
62
+ "paper",
63
+ "academic",
64
+ "linter",
65
+ "claude-code"
66
+ ],
67
+ "engines": {
68
+ "node": ">=22.13"
69
+ },
70
+ "bin": {
71
+ "paperlint": "bin/rpp.mjs"
72
+ },
73
+ "files": [
74
+ ".github",
75
+ "CLAUDE.md",
76
+ "CONTRIBUTING.md",
77
+ "action.harness.mjs",
78
+ "action.mutations.mjs",
79
+ "action.yml",
80
+ "bin",
81
+ "dist",
82
+ "docs",
83
+ "eslint-rules",
84
+ "eslint.config.mjs",
85
+ "fixtures",
86
+ "!fixtures/real-markdown-paper",
87
+ "fixtures/real-markdown-paper/baseline.*",
88
+ "hooks",
89
+ "lib",
90
+ "plugin",
91
+ "scripts",
92
+ "skills",
93
+ "src",
94
+ "templates",
95
+ "tsconfig.json"
96
+ ]
97
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "name": "research-paper-pipeline",
3
+ "version": "0.1.0",
4
+ "description": "Wiring for the research-paper-pipeline hooks. The code itself comes from the npm package installed in the consumer's project — this plugin carries no code and no dependencies on purpose.",
5
+ "author": { "name": "zernie" },
6
+ "repository": "https://github.com/zernie/research-paper-pipeline",
7
+ "keywords": ["latex", "academic-writing", "eslint", "linting", "ci"]
8
+ }
@@ -0,0 +1,30 @@
1
+ {
2
+ "hooks": {
3
+ "PreToolUse": [
4
+ {
5
+ "matcher": "Bash",
6
+ "hooks": [
7
+ {
8
+ "type": "command",
9
+ "command": "node \"${CLAUDE_PROJECT_DIR}/node_modules/paperlint/bin/rpp.mjs\" hook paper-edit-guard"
10
+ }
11
+ ]
12
+ }
13
+ ],
14
+ "PostToolUse": [
15
+ {
16
+ "matcher": "Edit|Write|MultiEdit",
17
+ "hooks": [
18
+ {
19
+ "type": "command",
20
+ "command": "node \"${CLAUDE_PROJECT_DIR}/node_modules/paperlint/bin/rpp.mjs\" hook paper-skills-nudge"
21
+ },
22
+ {
23
+ "type": "command",
24
+ "command": "node \"${CLAUDE_PROJECT_DIR}/node_modules/paperlint/bin/rpp.mjs\" hook paper-status-gates"
25
+ }
26
+ ]
27
+ }
28
+ ]
29
+ }
30
+ }
@@ -0,0 +1,177 @@
1
+ /**
2
+ * Both halves for `npm run check` — and the half that matters is not "does it run the gates",
3
+ * it is "does its list still describe the CI it claims to mirror".
4
+ *
5
+ * 🔴 A HANDWRITTEN LIST OF GATES IS A FOSSIL THE DAY AFTER IT IS WRITTEN. That is measured, not
6
+ * feared: on 2026-09-19 eleven scripts existed with no aggregate, a subset was run from memory,
7
+ * and a suite-breaking change was pushed. Writing the subset down in a doc would have produced
8
+ * the same outcome one release later — the list would simply have been wrong instead of absent.
9
+ *
10
+ * So the assertions below do not check that `check.mjs` contains the right strings. They pull
11
+ * the job names OUT OF `.github/workflows/ci.yml` and require every one to be accounted for.
12
+ * Add a job to CI and this goes red the same day, naming the job nobody covered.
13
+ *
14
+ * ⚠️ Assertions at the TOP LEVEL: `vigiles test` imports the file and counts "did not throw"
15
+ * as a pass.
16
+ */
17
+ import assert from "node:assert/strict";
18
+ import { existsSync, readdirSync, readFileSync } from "node:fs";
19
+ import { dirname, join } from "node:path";
20
+ import { fileURLToPath } from "node:url";
21
+ import { load } from "js-yaml";
22
+
23
+ const HERE = dirname(fileURLToPath(import.meta.url));
24
+ const ROOT = dirname(HERE);
25
+
26
+ const { GATES, NOT_COVERED, outcome, SKIP_EXIT, commandOf } =
27
+ await import("./check.mjs");
28
+
29
+ let n = 0;
30
+ const check = (label, cond) => {
31
+ n++;
32
+ assert.ok(cond, label);
33
+ };
34
+
35
+ // ── the workflow is the ORACLE, not a copy of it ───────────────────────────────────────────
36
+ // A declared skip is a THIRD outcome. Folding it into "pass" printed "all gates passed" on a
37
+ // machine where the e2e never ran (Codex review on #45); folding it into "fail" would make the
38
+ // command red for anyone without pnpm or TeX, and it would be ignored.
39
+ check("exit 0 is a pass", outcome(0) === "pass");
40
+ check(
41
+ `exit ${SKIP_EXIT} is a skip — neither a pass nor a failure`,
42
+ outcome(SKIP_EXIT) === "skip",
43
+ );
44
+ check("the skip code is the one vigiles' runner uses (77)", SKIP_EXIT === 77);
45
+ check(
46
+ "any other nonzero exit is a failure",
47
+ outcome(1) === "fail" && outcome(2) === "fail",
48
+ );
49
+ check(
50
+ "a process killed by a signal (status null) is a failure, not a skip",
51
+ outcome(null) === "fail",
52
+ );
53
+
54
+ // Every workflow, not just ci.yml: a harness reading one file would stop seeing a job added to
55
+ // another without a sound. (macOS lived in platform.yml from 2026-09-23 to 2026-09-24; it is now
56
+ // a cell of ci.yml's build-e2e matrix.)
57
+ const WF_DIR = join(ROOT, ".github", "workflows");
58
+ const workflows = Object.fromEntries(
59
+ readdirSync(WF_DIR)
60
+ .filter((f) => f.endsWith(".yml"))
61
+ .map((f) => [f, load(readFileSync(join(WF_DIR, f), "utf-8"))]),
62
+ );
63
+ const ciJobs = Object.values(workflows).flatMap((wf) =>
64
+ Object.keys(wf.jobs ?? {}),
65
+ );
66
+ check(
67
+ "the workflows parse and declare jobs — without this every assertion below is vacuous",
68
+ ciJobs.length > 0 && "ci.yml" in workflows,
69
+ );
70
+
71
+ // ── macOS: a cell of the TeX job, on every push ─────────────────────────────────────────
72
+ // install-tl-unx must work there, and issue #9 appears there alone. The trigger used to be a
73
+ // separate once-per-PR workflow; the repository is public, so the minutes are free.
74
+ const prTypes = (wf) => wf.on?.pull_request?.types ?? [];
75
+ const buildOs =
76
+ workflows["ci.yml"]?.jobs?.["build-e2e"]?.strategy?.matrix?.os ?? [];
77
+ check(
78
+ "build-e2e runs on Linux AND macOS — one job, two operating systems",
79
+ buildOs.includes("ubuntu-latest") && buildOs.includes("macos-latest"),
80
+ JSON.stringify(buildOs),
81
+ );
82
+ check(
83
+ "no workflow listens to `labeled` — a label trigger re-fires on every push and can pass by skipping",
84
+ Object.values(workflows).every((wf) => !prTypes(wf).includes("labeled")),
85
+ );
86
+ check(
87
+ "ci.yml listens to ready_for_review — otherwise marking a draft ready runs NOTHING",
88
+ prTypes(workflows["ci.yml"]).includes("ready_for_review"),
89
+ );
90
+
91
+ // ── HALF ONE: every CI job is accounted for ────────────────────────────────────────────────
92
+ const covered = new Set(GATES.map((g) => g.job).filter(Boolean));
93
+ for (const job of ciJobs) {
94
+ check(
95
+ `CI job «${job}» is either reproduced by a gate or named in NOT_COVERED with a reason — ` +
96
+ `an unexplained gap is indistinguishable from an oversight`,
97
+ covered.has(job) || typeof NOT_COVERED[job] === "string",
98
+ );
99
+ }
100
+
101
+ // ── HALF TWO: the list names nothing that does not exist ───────────────────────────────────
102
+ // A dead job name is worse than a missing one: it reads as coverage and delivers nothing.
103
+ for (const g of GATES) {
104
+ if (g.job === null) continue;
105
+ check(
106
+ `gate «${g.name}» names a job that really exists in a workflow (${g.job})`,
107
+ ciJobs.includes(g.job),
108
+ );
109
+ }
110
+ for (const job of Object.keys(NOT_COVERED)) {
111
+ check(
112
+ `NOT_COVERED names a job that really exists in a workflow (${job})`,
113
+ ciJobs.includes(job),
114
+ );
115
+ }
116
+
117
+ // ── HALF THREE: every gate is runnable ─────────────────────────────────────────────────────
118
+ // A gate whose script or file was renamed fails at the moment someone runs it — which is
119
+ // exactly the moment they are trusting it. Catch it here instead.
120
+ const scripts =
121
+ JSON.parse(readFileSync(join(ROOT, "package.json"), "utf-8")).scripts ?? {};
122
+ for (const g of GATES) {
123
+ check(
124
+ `gate «${g.name}» says how to run it in exactly one way (script or run)`,
125
+ Boolean(g.script) !== Array.isArray(g.run),
126
+ );
127
+ if (g.script) {
128
+ check(
129
+ `gate «${g.name}» maps to a script that exists (npm run ${g.script})`,
130
+ typeof scripts[g.script] === "string",
131
+ );
132
+ continue;
133
+ }
134
+ // A `run` gate names files and programs directly. Every file it names must be on disk, and
135
+ // every program other than `node` must be installed in node_modules/.bin.
136
+ const argv = commandOf(g);
137
+ const files = argv.filter((a) => /\.m?js$/.test(a));
138
+ const programs = argv.filter(
139
+ (a, i) => i === 0 || argv[i - 1] === "scripts/exclusive.mjs",
140
+ );
141
+ // Guards: a gate runs SOMETHING — a script of ours, or an installed program such as `tsc`.
142
+ check(
143
+ `gate «${g.name}» runs files that exist (${files.join(", ")})`,
144
+ (files.length > 0 || programs.some((p) => p !== "node")) &&
145
+ files.every((f) => existsSync(join(ROOT, f))),
146
+ );
147
+ check(
148
+ `gate «${g.name}» runs programs that are installed (${programs.join(", ")})`,
149
+ programs.every(
150
+ (p) => p === "node" || existsSync(join(ROOT, "node_modules", ".bin", p)),
151
+ ),
152
+ );
153
+ }
154
+ check(
155
+ "`check` itself is wired as a script, or nobody can run any of this",
156
+ typeof scripts.check === "string",
157
+ );
158
+
159
+ // ── HALF FOUR: a local-only gate must say WHY ──────────────────────────────────────────────
160
+ // Without this, `job: null` becomes the quiet way to drop something out of CI.
161
+ for (const g of GATES.filter((g) => g.job === null)) {
162
+ check(
163
+ `local-only gate «${g.name}» carries a reason`,
164
+ typeof g.reason === "string" && g.reason.length > 20,
165
+ );
166
+ }
167
+
168
+ // ── and the tail is not optional ───────────────────────────────────────────────────────────
169
+ check(
170
+ "NOT_COVERED is non-empty — if it ever is, either CI shrank or someone silenced the tail",
171
+ Object.keys(NOT_COVERED).length > 0,
172
+ );
173
+
174
+ console.log(
175
+ `✓ ${String(n)} assertions passed — npm run check: ${GATES.length} gates, ` +
176
+ `${ciJobs.length} CI job(s) all accounted for`,
177
+ );