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,701 @@
1
+ /**
2
+ * skill-checks.mjs — everything asserted about ONE skill, as a function you call with its name.
3
+ * `checkSkill("verify-citations")`.
4
+ *
5
+ * WHY A FUNCTION AND NOT A HARNESS. Until 2026-08-11 these assertions lived in
6
+ * `skills.harness.mjs`, a 614-line file named after no surface, and the 21 colocated per-skill
7
+ * harnesses reached it by setting `PIPELINE_SKILL_ONLY` and importing it for its side effects.
8
+ * That is a script pretending to be an API: the caller could not pass an argument, could not get
9
+ * a result, and the coupling was an environment variable. The owner asked three times why the
10
+ * file survived; the answer was that I had wrapped it instead of splitting it.
11
+ *
12
+ * WHAT IS CHECKED HERE vs NEXT DOOR. Anything decidable from one SKILL.md: strict-YAML
13
+ * frontmatter, the declared tool contract, every script path it instructs, whether it is
14
+ * PERMITTED to run what it instructs, whether it files under its OWN identity, and its verdict
15
+ * vocabulary. Anything about the SET — name uniqueness, the gate table, the exclusion list — is
16
+ * `pipeline-corpus.harness.mjs`, because a per-skill run structurally cannot see it.
17
+ *
18
+ * 🔴 KNOWN-RED ASSERTIONS ARE DEFERRED, NOT WEAKENED, and the deferral list is now PER CALL.
19
+ * A throw would abort the rest, making one open finding silently skip every check after it —
20
+ * the failure this whole directory exists to end. Per call matters too: with one shared list,
21
+ * a full sweep could attribute one skill's failures to another's line in the report.
22
+ */
23
+ import assert from "node:assert/strict";
24
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
25
+ import { join } from "node:path";
26
+ import { recordCheck } from "vigiles";
27
+
28
+ // Markup is parsed with a parser (`CLAUDE.md`, 2026-08-11). `requireMarkdown()` is called by
29
+ // `skill-corpus.mjs` on the import below, so by the time anything is parsed here the parser
30
+ // either exists or the process does not.
31
+ import { headings as mdHeadings } from "./markdown.mjs";
32
+
33
+ // 🔴 2026-08-28: nine names were removed from this list, and that is NOT a style cleanup — they
34
+ // had drifted apart from what the file does. `EXCLUDED PIPELINE_MARKER GATE_SKILLS EXPECTED_GATES
35
+ // KINDS parseGate` are about the SET of skills (name uniqueness, the gate table, the exclusion
36
+ // list); the file's own header says that is `pipeline-corpus.harness.mjs`'s job, and it was
37
+ // verified by grep — all six really are used there, no logic was lost. `frontmatter` and
38
+ // `toolList` are called INSIDE `load()` in `skill-corpus.mjs`, and the direct import was a
39
+ // second path to the same data. `allBold` is the raw material for `namedKinds`/`namedRetired`,
40
+ // which are imported right beside it.
41
+ import {
42
+ SKILLS,
43
+ SKILLS_DIR,
44
+ RETIRED,
45
+ ABSTENTIONS,
46
+ record,
47
+ ROOT,
48
+ LEDGER_ONLY_BASH,
49
+ recordBlock,
50
+ namedKinds,
51
+ namedRetired,
52
+ load,
53
+ commands,
54
+ permits,
55
+ definedKinds,
56
+ namedReasons,
57
+ ADMISSION,
58
+ tmp,
59
+ } from "./skill-corpus.mjs";
60
+
61
+ // ═════════════════════════════════════════════════════════════════════════════════════════════
62
+ // WHERE THE PIPELINE'S SCRIPTS ARE — ASKED, NOT SPELLED (2026-09-12)
63
+ //
64
+ // Until today this file carried the string `.claude/skills/paper-pipeline/scripts/` in SEVEN
65
+ // places: two announce/record templates, two `indexOf` ordering probes, one `startsWith` filter
66
+ // and two regular expressions. Every one of them is this consumer's own layout, and a mechanism
67
+ // that spells one consumer's layout has exactly one possible user — the thing the extraction
68
+ // exists to undo. Nine colocated harnesses of wave ① import THIS module and nothing else from
69
+ // `lib/`, so until the seven literals left, none of them could be built anywhere but here.
70
+ //
71
+ // The value now comes from the package, as the fourth carrier beside `papersRoot()` (where the
72
+ // papers are), `ledgerPath()` (where the journal is) and `citeChecks` (where the consumer's own
73
+ // citation checkers are). One declaration in `package.json`, default
74
+ // `.claude/skills/paper-pipeline/scripts`, and the resolver REFUSES a root that is not on disk
75
+ // or that resolves inside `node_modules`.
76
+ //
77
+ // 🔴 WHY THE REFUSAL IS THE POINT AND NOT DEFENSIVENESS. Six of the seven uses are FILTERS —
78
+ // `startsWith`, `indexOf`, `RegExp.test`. A wrong prefix does not make them wrong; it makes them
79
+ // match nothing, so every loop body below goes unentered and `checkSkill` reports a clean skill
80
+ // it never examined. That failure is byte-identical to success, and a throw while this module is
81
+ // still LOADING is the only place it can be made visible.
82
+ //
83
+ // `{ env: {}, cwd: ROOT }` rather than the defaults: `ROOT` above already resolved
84
+ // `CLAUDE_PROJECT_DIR`, and letting the resolver consult it again would make the two disagree on
85
+ // exactly one input (an empty-string variable — `??` keeps it, `||` does not).
86
+ import {
87
+ installedSkills,
88
+ pipelineScripts,
89
+ scriptsRoot,
90
+ } from "../skills/paper-pipeline/scripts/consumer.mjs";
91
+
92
+ const PIPELINE = pipelineScripts(scriptsRoot({ env: {}, cwd: ROOT }));
93
+
94
+ /**
95
+ * A literal path, embedded in a RegExp. Not a parser dodge — the subject here IS a string (a
96
+ * path, which has no grammar to ask), and this is the standard way to put one inside a pattern.
97
+ */
98
+ const reEscape = (t) => t.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
99
+
100
+ /** `Bash(node <pipeline scripts>/<name>.mjs:*)` — the narrowed entry the ledger-only skills get. */
101
+ const LEDGER_BASH_RE = new RegExp(
102
+ `^Bash\\(node ${reEscape(PIPELINE.prefix)}[a-z-]+\\.mjs:\\*\\)$`,
103
+ );
104
+
105
+ /**
106
+ * A command that is one of the two ledger calls.
107
+ *
108
+ * ⚠️ DELIBERATELY NOT ANCHORED AT THE START, because the literal it replaced was not either. The
109
+ * anchored form would be tighter and is probably what anyone would write today, but tightening it
110
+ * in the same change that parameterises it would mean a `before = after` comparison could no
111
+ * longer tell a successful parameterisation from a changed rule.
112
+ */
113
+ const LEDGER_SCRIPT_RE = new RegExp(
114
+ `${reEscape(PIPELINE.prefix)}(announce|ledger)\\.mjs$`,
115
+ );
116
+
117
+ /**
118
+ * Assert everything single-skill about `name`. Throws with EVERY finding, not the first.
119
+ * Returns the number of skills checked (1) so a caller can report what it actually did.
120
+ */
121
+ export async function checkSkill(name) {
122
+ if (!SKILLS.includes(name))
123
+ throw new Error(
124
+ `"${name}" is not a wired pipeline skill. Known: ${SKILLS.join(", ")}`,
125
+ );
126
+ const deferred = [];
127
+ const soft = (fn) => {
128
+ try {
129
+ fn();
130
+ } catch (e) {
131
+ deferred.push(e);
132
+ }
133
+ };
134
+ const skills = [load(name)];
135
+ const skillDirs = new Set(installedSkills(SKILLS_DIR));
136
+
137
+ // ═══════════════════════════════════════════════════════════════════════════════════════════════
138
+ // 1. GONE, AND THAT IS THE RESULT (2026-08-19). This slot held a strict-YAML assertion whose whole
139
+ // job was to police the gap between how the corpus was READ here (a hand-rolled lenient reader)
140
+ // and how every other consumer reads it (a real YAML parser). The corpus owner, on being shown the mutation
141
+ // suite: "sounds like a hack… let's think about how to do it properly". The right fix was not a better
142
+ // check — it was to stop being lenient: the two remaining unparseable descriptions were quoted and
143
+ // `parseFm` in skill-corpus.mjs now throws on anything a real parser rejects.
144
+ //
145
+ // So the defect class is not detected here any more; it is UNREACHABLE. A skill whose frontmatter
146
+ // is not valid YAML does not reach these assertions with a plausible-looking object and a missing
147
+ // `allowed-tools` — it does not load at all, and says why. What the corpus does about the root
148
+ // cause (`vigiles compile` interpolating the description into YAML raw) is written where the
149
+ // parser lives.
150
+
151
+ // ═══════════════════════════════════════════════════════════════════════════════════════════════
152
+ // 2-4. Identity and contract fields, read from the strict parse — there is no other kind now, and
153
+ // nothing above can fail while leaving these reachable. `name` is what the ledger and `skillHash()`
154
+ // key on: a name that disagrees
155
+ // with its directory means every verdict this skill records hashes against a directory that is not
156
+ // its own, and staleness detection quietly stops working for it.
157
+ // ═══════════════════════════════════════════════════════════════════════════════════════════════
158
+ for (const s of skills) {
159
+ soft(() =>
160
+ assert.equal(
161
+ s.fm.name,
162
+ s.name,
163
+ `${s.name}: frontmatter name is ${JSON.stringify(s.fm.name)} but the directory is "${s.name}". ` +
164
+ `skillHash() resolves by DIRECTORY and the ledger stores whatever the SKILL.md text passes — ` +
165
+ `disagreement here splits one skill's history across two keys and neither is ever fresh.`,
166
+ ),
167
+ );
168
+
169
+ soft(() =>
170
+ assert.ok(
171
+ (s.fm.description || "").length > 40,
172
+ `${s.name}: description is missing or trivially short (${(s.fm.description || "").length} chars). ` +
173
+ `The description is the ONLY thing the model matches on when choosing a skill — an empty one is ` +
174
+ `a skill that never fires, and a skill that never fires is indistinguishable from one that works.`,
175
+ ),
176
+ );
177
+
178
+ soft(() =>
179
+ assert.ok(
180
+ s.tools.length > 0,
181
+ `${s.name}: no allowed-tools. A skill declaring nothing INHERITS EVERYTHING — Bash, WebFetch and ` +
182
+ `every MCP server (Calendar, GitHub-write, Vercel) — over a repo holding health data, DNA and ` +
183
+ `bank balances. This is the finding the 2026-08-03 audit closed for all 35 skills.`,
184
+ ),
185
+ );
186
+ }
187
+
188
+ // ═══════════════════════════════════════════════════════════════════════════════════════════════
189
+ // 5-6. The two wired blocks: `## Run me` (announce, first) and `Record the verdict` (ledger, last).
190
+ // The README calls these the whole wiring contract. Each is checked three ways — the block exists,
191
+ // the script it names exists on disk, and the SKILL NAME IT PASSES IS ITS OWN. The third is the
192
+ // copy-paste defect: these blocks differ from their siblings' by one token, and `announce.mjs`
193
+ // validates nothing, so a stale token records a real verdict under another skill's name.
194
+ // ═══════════════════════════════════════════════════════════════════════════════════════════════
195
+ for (const s of skills) {
196
+ const cmds = commands(s.src);
197
+
198
+ // 2026-08-11: "is there a `## Run me` block" is a question about a HEADING, therefore about
199
+ // level and text, not about the number of hashes. The former `/^#{1,3} Run me\s*$/m` also
200
+ // counted a `## Run me` line inside a ```-block as a heading: a skill SHOWING someone else's
201
+ // block in an example passed the check without having one of its own. Silence is a soft
202
+ // assertion's success state, so there was nothing to notice this with.
203
+ soft(() =>
204
+ assert.ok(
205
+ mdHeadings(s.src).some((h) => h.depth <= 3 && h.text === "Run me"),
206
+ `${s.name}: no "## Run me" block. Without it the skill leaves no trace of having started, and ` +
207
+ `an advisory pass that leaves no trace cannot be observed failing — silence is both its error ` +
208
+ `state and its normal state. Three hooks in this repo died exactly that way.`,
209
+ ),
210
+ );
211
+
212
+ for (const [label, script, want] of [
213
+ ["announce", PIPELINE.announce, "Run me"],
214
+ ["record", PIPELINE.ledger, "Record the verdict"],
215
+ ]) {
216
+ const hits = cmds.filter((c) => c.script === script);
217
+ soft(() =>
218
+ assert.ok(
219
+ hits.length > 0,
220
+ `${s.name}: the "${want}" block instructs no \`node ${script}\` command. The block may be present ` +
221
+ `and empty — the README's contract is the CALL, not the heading.`,
222
+ ),
223
+ );
224
+ for (const h of hits) {
225
+ soft(() =>
226
+ assert.ok(
227
+ existsSync(join(ROOT, h.script)),
228
+ `${s.name}: instructs \`${h.cmd}\` and ${h.script} DOES NOT EXIST. A skill naming a path that ` +
229
+ `is not there is a dead instruction: the model reads it, does something adjacent, and reports ` +
230
+ `success. Prose cannot notice this; nothing else in this repo was looking.`,
231
+ ),
232
+ );
233
+ // arg[0] for announce is the skill; for ledger.mjs it is the literal `record`, then the skill.
234
+ const passed =
235
+ label === "announce"
236
+ ? h.args[0]
237
+ : h.args[0] === "record"
238
+ ? h.args[1]
239
+ : h.args[0];
240
+ soft(() =>
241
+ assert.equal(
242
+ passed,
243
+ s.name,
244
+ `${s.name}: its ${label} command files under ${JSON.stringify(passed)}, not "${s.name}" — ` +
245
+ `\`${h.cmd}\`. These blocks are copy-paste templates differing in one token and announce.mjs ` +
246
+ `validates nothing, so this records a real run under a SIBLING'S identity: the wrong gate ` +
247
+ `reads FRESH in status.mjs and the right one reads NEVER-RUN.`,
248
+ ),
249
+ );
250
+ }
251
+ }
252
+
253
+ soft(() => {
254
+ const a = s.src.indexOf(PIPELINE.announce);
255
+ const r = s.src.indexOf(`${PIPELINE.ledger} record`);
256
+ if (a >= 0 && r >= 0)
257
+ assert.ok(
258
+ a < r,
259
+ `${s.name}: the ledger record call appears BEFORE the announce call. The contract is announce ` +
260
+ `first / record last; a verdict written before the run started is a verdict about nothing.`,
261
+ );
262
+ });
263
+
264
+ soft(() =>
265
+ assert.ok(
266
+ recordBlock(s.src),
267
+ `${s.name}: no "Record the verdict" heading. A run with an announce row and no verdict row is ` +
268
+ `indistinguishable from a skill that started and died.`,
269
+ ),
270
+ );
271
+ }
272
+
273
+ // ═══════════════════════════════════════════════════════════════════════════════════════════════
274
+ // 7. EVERY script path the skill instructs resolves — not only the two ledger calls.
275
+ //
276
+ // Scoped to paths that are unambiguously resolvable: `.claude/…` is repo-root-relative and
277
+ // `scripts/…` is skill-relative (both conventions are used consistently across the corpus). Paths
278
+ // like `repro/build-submission.sh` are relative to a PAPER directory that does not exist at test
279
+ // time, so they are excluded BY NAME rather than by a guess that would fire on correct text.
280
+ // ═══════════════════════════════════════════════════════════════════════════════════════════════
281
+ for (const s of skills) {
282
+ for (const c of commands(s.src)) {
283
+ const abs = c.script.startsWith(".claude/")
284
+ ? join(ROOT, c.script)
285
+ : c.script.startsWith("scripts/")
286
+ ? join(SKILLS_DIR, s.name, c.script)
287
+ : null;
288
+ if (!abs) continue;
289
+ soft(() =>
290
+ assert.ok(
291
+ existsSync(abs),
292
+ `${s.name}: instructs \`${c.cmd}\` but ${c.script} does not exist (looked at ${abs}). Dead ` +
293
+ `instruction — the model will improvise something adjacent and report it as done.`,
294
+ ),
295
+ );
296
+ }
297
+ }
298
+
299
+ // ═══════════════════════════════════════════════════════════════════════════════════════════════
300
+ // 8. A skill is PERMITTED TO RUN what it instructs. The inverse-direction risk of narrowing: over-
301
+ // narrow an allowed-tools entry and the skill's own first instruction is denied at runtime, which
302
+ // looks like the skill silently declining to announce — the same invisible failure as a dead hook,
303
+ // arrived at from the opposite direction. Nothing else checks the two halves against each other.
304
+ // ═══════════════════════════════════════════════════════════════════════════════════════════════
305
+ for (const s of skills) {
306
+ for (const c of commands(s.src)) {
307
+ // The calls the pipeline contract owns. Was `.claude/pipeline/` until 2026-08-15, when that
308
+ // directory was dissolved and `ledger.mjs`/`announce.mjs` went home to the skill they serve.
309
+ if (!c.script.startsWith(PIPELINE.prefix)) continue;
310
+ soft(() =>
311
+ assert.ok(
312
+ s.tools.some((t) => permits(t, c.cmd)),
313
+ `${s.name}: instructs \`${c.cmd}\` but its allowed-tools [${s.tools.join(", ")}] permit no such ` +
314
+ `Bash command. The instruction is denied at runtime and the skill simply does not announce or ` +
315
+ `record — invisible, because a gate that never wrote a row looks exactly like one nobody ran.`,
316
+ ),
317
+ );
318
+ }
319
+ }
320
+
321
+ // ═══════════════════════════════════════════════════════════════════════════════════════════════
322
+ // 9. The nine ledger-only skills keep their Bash NARROWED.
323
+ //
324
+ // A plain `Bash` on one of these is the regression that took this repo's Safety score from 70 to 0
325
+ // in the audit's own arithmetic: Read + Bash + WebFetch is the lethal trifecta, and these nine are
326
+ // analysis skills that read the knowledge base. `argument-arc` grading a paper does not need a
327
+ // shell; it needs to append one line to a JSONL. Construction beats prose — with the narrow entry
328
+ // the capability is ABSENT, and absence cannot be talked around by injected text in a fetched CFP.
329
+ // ═══════════════════════════════════════════════════════════════════════════════════════════════
330
+ for (const s of skills.filter((s) => LEDGER_ONLY_BASH.has(s.name))) {
331
+ const bash = s.tools.filter((t) => t === "Bash" || t.startsWith("Bash("));
332
+ soft(() =>
333
+ assert.ok(
334
+ bash.length > 0,
335
+ `${s.name}: listed as ledger-only-Bash but declares no Bash entry at all — it cannot run its own ` +
336
+ `announce/record calls. Either add the narrow entries or remove it from LEDGER_ONLY_BASH here.`,
337
+ ),
338
+ );
339
+ for (const b of bash) {
340
+ soft(() =>
341
+ assert.match(
342
+ b,
343
+ LEDGER_BASH_RE,
344
+ `${s.name}: Bash entry ${JSON.stringify(b)} is not narrowed to the ledger. This skill gained Bash ` +
345
+ `ONLY to append a run row; a bare \`Bash\` hands a knowledge-base reader a shell and completes ` +
346
+ `the lethal trifecta. See ALLOWED-TOOLS-AUDIT-2026-08-03.md §2 for why this one was narrowed.`,
347
+ ),
348
+ );
349
+ }
350
+ }
351
+
352
+ // ═══════════════════════════════════════════════════════════════════════════════════════════════
353
+ // 10. THE VOCABULARY, TURNED AROUND ON 2026-08-10.
354
+ //
355
+ // WHAT THIS USED TO ASSERT, and why it had to change rather than be deleted. The old text was:
356
+ // "the vocabulary contains a REACHABLE NEGATIVE — or the text admits it does not", because a gate
357
+ // documented as PASS-only cannot say no and no amount of running would reveal it. With `PASS`
358
+ // deleted from the ledger, the literal form of that assertion is VACUOUS: `FINDING` and
359
+ // `ABSTAINED` are the only constructors, so every conforming block names a negative by
360
+ // construction and the check would pass on every file forever while reading as though it still
361
+ // guarded something. A vacuous assertion is worse than a missing one — it is a green light with
362
+ // nothing behind it, which is this directory's whole subject.
363
+ //
364
+ // So the property is carried over rather than the wording. It was never really "names a negative";
365
+ // it was **"this check is capable of reporting that something is wrong."** Under the new
366
+ // vocabulary that is: the block names FINDING, the evidence-carrying constructor. A block that
367
+ // documents only how to abstain describes a check that cannot fail — the same defect, wearing the
368
+ // new words.
369
+ //
370
+ // 10b is genuinely new and is the migration guard: a block still instructing a RETIRED constructor
371
+ // tells the model to run a command that now exits 2. Prose survives refactors that code does not,
372
+ // and 22 files were rewritten by hand today.
373
+ //
374
+ // 10c requires a named abstention REASON, because `ABSTAINED` with no reason is the deleted `PASS`
375
+ // with a longer name — the closed reason vocabulary is the entire mechanism separating "ran and
376
+ // found nothing" from "could not run".
377
+ //
378
+ // The escape hatch on 10a is deliberate and is the honest case, not a loophole: `draft-paper`
379
+ // GENERATES, will abstain nearly always, and its text says so in status.mjs's own terms. That
380
+ // admission is load-bearing documentation — delete it and an explained limitation becomes an
381
+ // unexplained never-finding check — so it is asserted directly, not merely permitted.
382
+ // ═══════════════════════════════════════════════════════════════════════════════════════════════
383
+ for (const s of skills) {
384
+ const blk = recordBlock(s.src);
385
+ if (!blk) continue;
386
+
387
+ soft(() =>
388
+ assert.ok(
389
+ namedKinds(blk).includes("FINDING") || ADMISSION.test(blk),
390
+ `${s.name}: its record block names [${namedKinds(blk).join(", ") || "nothing"}] and not FINDING, ` +
391
+ `and does not admit that it cannot produce one. FINDING is the only constructor that carries ` +
392
+ `evidence; a block documenting only how to ABSTAIN describes a check that can never report that ` +
393
+ `anything is wrong. Either name the finding this skill can return, or state plainly that it ` +
394
+ `generates rather than judges — as draft-paper does.`,
395
+ ),
396
+ );
397
+
398
+ soft(() =>
399
+ assert.deepEqual(
400
+ namedRetired(blk),
401
+ [],
402
+ `${s.name}: its record block still instructs the RETIRED constructor(s) ` +
403
+ `[${namedRetired(blk).join(", ")}]. ${[...RETIRED.keys()]
404
+ .filter((k) => namedRetired(blk).includes(k))
405
+ .map((k) => `${k} ${RETIRED.get(k)}`)
406
+ .join(" ")} ` +
407
+ `A model following this block runs a command that exits 2 at the last step of finished work.`,
408
+ ),
409
+ );
410
+
411
+ soft(() =>
412
+ assert.ok(
413
+ namedReasons(blk).length > 0,
414
+ `${s.name}: its record block names no abstention reason from [${[...ABSTENTIONS.keys()].join(", ")}]. ` +
415
+ `An ABSTAINED with an unnamed reason is the deleted PASS with more characters: the closed reason ` +
416
+ `set is the only thing separating "ran and found nothing" from "could not run", and a block that ` +
417
+ `does not say which one applies leaves the model to invent a string record() will refuse.`,
418
+ ),
419
+ );
420
+ }
421
+ // Guarded for one-skill mode: this assertion is ABOUT draft-paper, so it only runs
422
+ // when draft-paper is in scope. Silently skipping it in a full sweep would be the
423
+ // vacuity this file hunts, so the guard is the membership test, not a try/catch.
424
+ const draftPaper = skills.find((s) => s.name === "draft-paper");
425
+ if (draftPaper)
426
+ soft(() =>
427
+ assert.match(
428
+ recordBlock(draftPaper.src),
429
+ ADMISSION,
430
+ `draft-paper: the honest admission is gone from its record block. It is the one skill in the ` +
431
+ `pipeline that will almost always abstain, and the paragraph saying so — in status.mjs's own ` +
432
+ `terms, "a generator is not a gate" — is what keeps that flag readable as a known limitation ` +
433
+ `rather than an unexplained never-finding check someone later "fixes" by faking one.`,
434
+ ),
435
+ );
436
+
437
+ // ═══════════════════════════════════════════════════════════════════════════════════════════════
438
+ // 11. No skill instructs something `record()` will throw on — proven by CALLING IT, not by reading
439
+ // the vocabulary list. `record()` validates its argument and throws; a skill telling the model to
440
+ // record `GREEN`, `OK` or `PASS` produces a crash at the last step of a completed pass, after the
441
+ // work is done.
442
+ //
443
+ // Each constructor is called in its VALID shape, because the shape is part of the contract: a
444
+ // FINDING needs a count and an existing report, an ABSTENTION needs a reason from the closed set.
445
+ // Calling `record` with a bare kind would prove only that the kind string is spelled right.
446
+ // ═══════════════════════════════════════════════════════════════════════════════════════════════
447
+ {
448
+ writeFileSync(
449
+ join(tmp, "fixture-report.md"),
450
+ "---\nfindings: 1\n---\n\nharness fixture\n",
451
+ );
452
+ for (const s of skills) {
453
+ const blk = recordBlock(s.src);
454
+ if (!blk) continue;
455
+ for (const v of definedKinds(blk)) {
456
+ soft(() =>
457
+ record(
458
+ v === "ABSTAINED"
459
+ ? {
460
+ skill: s.name,
461
+ paper: tmp,
462
+ kind: v,
463
+ reason: namedReasons(blk)[0] ?? null,
464
+ note: "harness fixture",
465
+ }
466
+ : {
467
+ skill: s.name,
468
+ paper: tmp,
469
+ kind: v,
470
+ findings: 1,
471
+ report: "fixture-report.md",
472
+ note: "harness fixture",
473
+ },
474
+ ),
475
+ );
476
+ }
477
+ }
478
+ }
479
+ // Every skill name any of these files hands to announce.mjs or ledger.mjs must resolve to a real
480
+ // skill, including names inside worked examples. announce.mjs does NOT validate: an unknown name
481
+ // hashes to `????????` and the row is written anyway.
482
+ for (const s of skills) {
483
+ for (const c of commands(s.src)) {
484
+ if (!LEDGER_SCRIPT_RE.test(c.script)) continue;
485
+ const passed = c.script.endsWith("announce.mjs")
486
+ ? c.args[0]
487
+ : c.args[0] === "record"
488
+ ? c.args[1]
489
+ : null;
490
+ if (!passed || passed.startsWith("<")) continue;
491
+ soft(() =>
492
+ assert.ok(
493
+ skillDirs.has(passed),
494
+ `${s.name}: instructs \`${c.cmd}\`, naming skill "${passed}", which has no directory under ` +
495
+ `.claude/skills/. announce.mjs validates nothing — it hashes the unknown name to "????????" ` +
496
+ `and writes the row, so the typo is permanent and silent in the ledger.`,
497
+ ),
498
+ );
499
+ }
500
+ }
501
+ if (deferred.length) {
502
+ const e = new Error(
503
+ `${deferred.length} assertion(s) failed for pipeline skill "${name}":\n\n` +
504
+ deferred.map((d, i) => ` [${i + 1}] ${d.message}`).join("\n\n"),
505
+ );
506
+ e.stack = e.message;
507
+ throw e;
508
+ }
509
+ // Reported so `vigiles test` can tell a run that verified this skill from a file that merely
510
+ // sits beside it — the distinction colocation cannot make (see vigiles check-count.ts).
511
+ recordCheck();
512
+ console.log(
513
+ `✓ ${name}: frontmatter, wiring, script paths, tool contract, verdict vocabulary`,
514
+ );
515
+ return 1;
516
+ }
517
+
518
+ // ═════════════════════════════════════════════════════════════════════════════════════════════════
519
+ // checkProseSkill — for skills WITHOUT the pipeline (2026-08-27)
520
+ //
521
+ // WHY A SEPARATE FUNCTION AND NOT A FLAG ON checkSkill. `checkSkill` requires `## Run me`, a call
522
+ // to `announce.mjs` and a "Record the verdict" block — those are paper-pipeline conventions, and a
523
+ // skill like `avito-posting` neither has them nor should. Running it through checkSkill means
524
+ // getting three red assertions about the absence of what it never promised. A check that is red by
525
+ // construction lives one day, after which it gets silenced — that has already happened in this repo.
526
+ //
527
+ // 🔴 BOTH HALVES ARE BUILT IN, NOT WRITTEN BY HAND. The `CLAUDE.md` rule requires two halves of
528
+ // every check: it fires on a planted defect AND it stays silent on clean input. The "fires" half is
529
+ // written less often — it is not needed to make a run go green. So here it is not entrusted to the
530
+ // author: EVERY predicate below is run twice — over the real text and over the mutated one, and the
531
+ // disagreement of the verdicts IS the assertion. The mutation cannot be forgotten, there is nowhere
532
+ // to forget it.
533
+ //
534
+ // WHAT THIS DOES NOT PROVE: that the skill fires on the right request and that its advice gives a
535
+ // good result. The first is the trigger eval, the second is the model. Only what is decidable from
536
+ // the text is here.
537
+ // ═════════════════════════════════════════════════════════════════════════════════════════════════
538
+
539
+ /** Relative markdown links that do not resolve from the skill's directory. */
540
+ function danglingLinks(src, dir) {
541
+ const out = [];
542
+ for (const m of src.matchAll(/\[[^\]]*\]\(([^)\s]+)\)/g)) {
543
+ const t = m[1].split("#")[0];
544
+ if (!t || /^(https?:|mailto:)/.test(t)) continue;
545
+ if (!existsSync(join(dir, t))) out.push(t);
546
+ }
547
+ return out;
548
+ }
549
+
550
+ /**
551
+ * Check the prose skill `name`.
552
+ *
553
+ * @param {string} name — the directory name in `.claude/skills/`
554
+ * @param {{mustSay?: [RegExp,string][], mustNotSay?: [RegExp,string][],
555
+ * toolsNeeded?: Record<string,string>}} opts
556
+ * `mustSay` — that whose DISAPPEARANCE is a regression (a rule obtained by measurement).
557
+ * `mustNotSay` — that whose APPEARANCE is a regression (a refuted claim).
558
+ * `toolsNeeded` — {tool: REASON}. Lifts the ban on Bash/Agent/Task for a skill that
559
+ * needs the tool IN SUBSTANCE. The reason is mandatory and non-empty.
560
+ *
561
+ * 🔴 Why `toolsNeeded` appeared (2026-09-01) and why it carries a REASON rather than a flag.
562
+ * The ban on Bash/Agent/Task was written for two text-only skills and was hard-wired as
563
+ * universal: "a skill that only writes text". For `session-retro` that is a lie — its own
564
+ * instruction prescribes SPAWNING auditors ("Spawn parallel audit subagents") and running
565
+ * measurements, that is, without Agent and Bash it does not do what it is.
566
+ *
567
+ * The temptation was to switch the check off for it entirely. That is wrong: the ban protects
568
+ * a repository with health data and balances from the third leg of the lethal trifecta, and it
569
+ * is still right for the rest. So it is not a flag but a NAMED exemption with a reason — the
570
+ * same form as `vigiles:local-by-design` in this same knowledge base: switching off is
571
+ * possible, but it stays written down, and the next reader sees WHAT exactly was decided.
572
+ */
573
+ export async function checkProseSkill(name, opts = {}) {
574
+ const { skillContract } = await import("vigiles");
575
+ const dir = join(SKILLS_DIR, name);
576
+ const md = join(dir, "SKILL.md");
577
+ const deferred = [];
578
+ const soft = (fn) => {
579
+ try {
580
+ fn();
581
+ } catch (e) {
582
+ deferred.push(e);
583
+ }
584
+ };
585
+
586
+ assert.ok(existsSync(md), `${name}: no ${md} — nothing to check`);
587
+ const src = readFileSync(md, "utf8");
588
+
589
+ // 1. The tool contract. Both skills deliberately DROPPED Bash — it was the third leg of the
590
+ // lethal trifecta over a repo with health data and balances. A regression here is silent: a
591
+ // skill with no parseable `allowed-tools` INHERITS EVERYTHING, and that looks exactly like a
592
+ // declared contract.
593
+ const c = await skillContract(dir);
594
+ soft(() =>
595
+ assert.equal(
596
+ c.malformed,
597
+ false,
598
+ `${name}: allowed-tools does not parse. The file LOOKS like it declares a contract, while a ` +
599
+ `strict parser sees nothing — the skill silently inherits every tool. Known cause: ` +
600
+ `\`vigiles compile\` strips the quotes off a scalar containing ": " and breaks the YAML ` +
601
+ `(see CLAUDE.md, measured 2026-08-19).`,
602
+ ),
603
+ );
604
+ soft(() =>
605
+ assert.ok(
606
+ c.declared.length > 0,
607
+ `${name}: empty allowed-tools — inherits everything`,
608
+ ),
609
+ );
610
+ const needed = opts.toolsNeeded ?? {};
611
+ // An exemption without a reason is not an exemption: an empty string would pass as "decided"
612
+ // while saying nothing. The same miss as "a counter that counts what it ignores".
613
+ for (const [tool, why] of Object.entries(needed))
614
+ assert.ok(
615
+ typeof why === "string" && why.trim().length >= 10,
616
+ `${name}: toolsNeeded.${tool} is declared without an intelligible reason. An exemption must ` +
617
+ `say WHY the tool is needed in substance — otherwise it is a silent disabling of the check.`,
618
+ );
619
+ for (const banned of ["Bash", "Agent", "Task"]) {
620
+ if (banned in needed) continue;
621
+ soft(() =>
622
+ assert.ok(
623
+ !c.declared.includes(banned),
624
+ `${name}: ${banned} appeared in the contract. It was dropped deliberately; bringing it back ` +
625
+ `restores a leg of the lethal trifecta (data + network + execution) on a skill that only ` +
626
+ `writes text. If the tool IS needed IN SUBSTANCE — declare it in toolsNeeded WITH A REASON, ` +
627
+ `not silently.`,
628
+ ),
629
+ );
630
+ }
631
+ // The other side: an exemption that exempts nothing is garbage, and garbage accumulates.
632
+ // A year later such lines read as "there was a risk here", though there is no risk.
633
+ for (const tool of Object.keys(needed))
634
+ soft(() =>
635
+ assert.ok(
636
+ c.declared.includes(tool),
637
+ `${name}: toolsNeeded.${tool} is declared, but ${tool} itself is NOT in the contract. ` +
638
+ `The exemption is stale — remove it, otherwise it describes a different skill.`,
639
+ ),
640
+ );
641
+
642
+ // 2. Links. The class we got burned on 2026-08-27: a file moves one level deeper, the link
643
+ // stays as it was and silently stops resolving. A grep by name does not see that.
644
+ const dangling = danglingLinks(src, dir);
645
+ soft(() =>
646
+ assert.deepEqual(
647
+ dangling,
648
+ [],
649
+ `${name}: dangling links → ${dangling.join(", ")}. A skill naming a path that does not exist ` +
650
+ `is a dead instruction: the model reads it, does something adjacent and reports success.`,
651
+ ),
652
+ );
653
+ // mutation: a planted broken link must be caught
654
+ assert.equal(
655
+ danglingLinks(src + "\n[x](./net-takogo-fayla.md)\n", dir).length,
656
+ dangling.length + 1,
657
+ `${name}: the link check did not fire on a planted broken link — it is dead`,
658
+ );
659
+
660
+ // 3. Whether it is compiled. Editing the markdown by hand desynchronises the sha and gets
661
+ // quietly rolled back by the next `vigiles compile`.
662
+ soft(() =>
663
+ assert.ok(
664
+ /vigiles:sha256:/.test(src),
665
+ `${name}: no compilation mark. That means SKILL.md was edited by hand rather than through ` +
666
+ `.spec.ts — the next \`vigiles compile\` will overwrite the edit without warning.`,
667
+ ),
668
+ );
669
+
670
+ // 4-5. Substantive anchors, each with BOTH halves.
671
+ for (const [re, why] of opts.mustSay || []) {
672
+ soft(() => assert.ok(re.test(src), `${name}: "${why}" is gone (${re})`));
673
+ // Mutation: without the matched fragment the predicate must fail. The replacement is GLOBAL —
674
+ // `replace` with a non-global regex removes only the first occurrence, and an anchor occurring
675
+ // twice (for example both in the description and in the body) would "pass" on the mutated text
676
+ // too. Then this assertion would fail rather than the anchor itself — that is, the message
677
+ // would point in the wrong direction.
678
+ const g = new RegExp(
679
+ re.source,
680
+ re.flags.includes("g") ? re.flags : re.flags + "g",
681
+ );
682
+ assert.equal(
683
+ re.test(src.replace(g, "")),
684
+ false,
685
+ `${name}: the anchor ${re} passes even on text WITHOUT it — the check pins nothing down`,
686
+ );
687
+ }
688
+ for (const [re, why] of opts.mustNotSay || []) {
689
+ soft(() => assert.ok(!re.test(src), `${name}: "${why}" came back (${re})`));
690
+ assert.equal(
691
+ re.test(src + "\n" + why + "\n"),
692
+ true,
693
+ `${name}: the ban ${re} does not fire on the planted line "${why}" — the check is dead`,
694
+ );
695
+ }
696
+
697
+ if (deferred.length)
698
+ throw new AggregateError(deferred, `${name}: ${deferred.length} findings`);
699
+ recordCheck();
700
+ return 1;
701
+ }