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,156 @@
1
+ /**
2
+ * paper-status-gates — a PostToolUse react: editing a paper's source surfaces that paper's
3
+ * readiness verdict and its UNRUN GATES, unprompted.
4
+ *
5
+ * A ☐ against study-accepted / pc-panel / harden / verify-citations is a HOLE, not an
6
+ * "anti-churn" skip: a session that skipped three of them had the panel come back with real
7
+ * must-fixes. Advisory, never blocks, always exits 0 — a `react`'s type has no `deny`.
8
+ *
9
+ * ── WHY IT WAS WORTH SURFACING AT ALL ───────────────────────────────────────
10
+ * The checker PARSED the verdict line (to test it was not a bare percentage) and never PRINTED
11
+ * it, so the one fact its author kept asking for by hand was the one thing the tooling reliably
12
+ * withheld. A status file nobody is shown is a status file nobody reads.
13
+ *
14
+ * ── THE PURE / IMPURE LINE, AND WHERE IT REALLY FALLS ───────────────────────
15
+ * An earlier header claimed this half "READS A FILE and interpolates its contents, and a vigiles
16
+ * `react` is a pure function — it cannot", and called that the honest boundary of the closed
17
+ * vocabulary. That reading was one step too pessimistic. The boundary is real, but it falls
18
+ * between DECIDING and DOING, not between "hook" and "shell": deciding whether this edit
19
+ * concerns a paper, and WHICH one, is pure and belongs here; reading that paper's status file
20
+ * and running the checker is a tool, and `run()` is how a react invokes one.
21
+ *
22
+ * ── WHY THE DIRECTORY NAME IS VALIDATED BEFORE IT REACHES A COMMAND ─────────
23
+ * `run()` takes a string AND IT GOES THROUGH A SHELL — measured 2026-09-12: a `$(…)` inside a
24
+ * `run()` command substitutes. This one embeds a path taken from the event, so the name is
25
+ * extracted by an anchored pattern and accepted only if it is `[A-Za-z0-9._-]+` — no slash, no
26
+ * space, no shell metacharacter can survive. The shell predecessor got this incidentally, via a
27
+ * `grep -oE`; here it is explicit, because "incidentally safe" is how it stops being safe.
28
+ *
29
+ * 🔴 THE `(?:^|\/)` IS NOT DECORATION, AND LEAVING IT OUT KILLED THIS HOOK ONCE. The first
30
+ * version anchored the papers root at `^`, which passed every test in the harness — because the
31
+ * harness's own edit helper builds a RELATIVE path. The live harness sends an ABSOLUTE one.
32
+ * Measured against the real runtime:
33
+ *
34
+ * file_path "<root>/<paper>/paper.md" → fires
35
+ * file_path "/abs/path/to/repo/<root>/<paper>/…" → SILENT
36
+ *
37
+ * So the hook would have been dead in production and green in the tests: precisely the
38
+ * false-confidence class this whole exercise is about, reintroduced while converting a hook away
39
+ * from it. The shell version was accidentally immune — it grepped the raw payload with no anchor
40
+ * at all. The harness now sends BOTH spellings for every case.
41
+ *
42
+ * The boundary is still required (`^` or `/`), so a directory merely ENDING in the root's last
43
+ * segment cannot smuggle a match.
44
+ *
45
+ * ── WHAT THIS HOOK'S OUTPUT DOES *NOT* DO, measured and named ───────────────
46
+ * ⚠️ A `run()` reaction's output is passed through RAW — 0 bytes of `hookSpecificOutput` on
47
+ * stdout, the tool's own text on stderr (measured 2026-09-12: 1 184 bytes of stderr, 0 of
48
+ * stdout). Only a `notice` on an injectable event becomes `additionalContext`. So this hook's
49
+ * findings reach a HUMAN reading the transcript's debug output, not the model's context. That is
50
+ * a property of the reaction KIND, not a bug here, and it is the reason the static half lives in
51
+ * `paper-skills-nudge` as a `notice` instead: that one does land.
52
+ *
53
+ * ── WHY `.mjs` AND NOT A `.hook.ts` SPEC ────────────────────────────────────
54
+ * See `paper-edit-guard.hook.mjs`: a `.ts` hook reached through `node_modules` does not load,
55
+ * and a consumer-side thin spec importing this logic is refused by `vigiles compile` and can
56
+ * never be re-stamped. Both halves measured there.
57
+ */
58
+ import {
59
+ experimental_defineReact,
60
+ tools,
61
+ provide,
62
+ run,
63
+ nothing,
64
+ } from "vigiles/hook";
65
+
66
+ /** The key every carrier of this package reads its consumer-specific settings from. */
67
+ export const CONFIG_KEY = "paperlint";
68
+ /** The key's name before 2.0.0 — still read, a copy of `lib/paper-config.mjs`. */
69
+ export const LEGACY_CONFIG_KEY = "research-paper-pipeline";
70
+ /** The default. A consumer that declares nothing is assumed to keep papers in `papers/`. */
71
+ export const DEFAULT_PAPERS_ROOT = "papers";
72
+ /**
73
+ * The field under CONFIG_KEY that names the papers directory, and its old name. A copy of the
74
+ * constants in `lib/paper-config.mjs` (a hook may import nothing but `vigiles/hook`);
75
+ * `lib/paper-config.harness.mjs` checks that the copies match.
76
+ */
77
+ export const PAPERS_DIR_FIELD = "papersDir";
78
+ export const OLD_PAPERS_DIR_FIELD = "papers";
79
+
80
+ /**
81
+ * The declared papers root, or `null` when it is unusable.
82
+ *
83
+ * ⚠️ `null` means SILENCE, not refusal — same asymmetry as `paper-skills-nudge` and for the same
84
+ * reason: this is advisory, and a `react` cannot deny anyway. The gate (`paper-edit-guard`) is
85
+ * the one that must refuse on an unreadable declaration, because there a silent default would
86
+ * pass the very writes it exists to stop.
87
+ *
88
+ * 🔴 `declared === undefined`, NOT `declared ?? DEFAULT` — `"papersDir": null` is a keystroke, not
89
+ * an absence.
90
+ */
91
+ const papersRoot = (rawPkg) => {
92
+ let declared;
93
+ try {
94
+ const pkg = JSON.parse(rawPkg);
95
+ const settings = pkg?.[CONFIG_KEY] ?? pkg?.[LEGACY_CONFIG_KEY];
96
+ // Old field name: stay silent rather than fall back to the default directory.
97
+ if (settings && Object.hasOwn(settings, OLD_PAPERS_DIR_FIELD)) return null;
98
+ declared = settings?.[PAPERS_DIR_FIELD];
99
+ } catch {
100
+ return null;
101
+ }
102
+ const root = declared === undefined ? DEFAULT_PAPERS_ROOT : declared;
103
+ if (typeof root !== "string" || root.length === 0) return null;
104
+ return root.replace(/\/+$/, "");
105
+ };
106
+
107
+ /**
108
+ * A paper SOURCE under `<root>/<dir>/`, with `<dir>` captured.
109
+ *
110
+ * 🔴 THE ROOT IS REGEX-ESCAPED. It is a path from a config file, and `.` is both a legal
111
+ * directory character and a regex wildcard: an unescaped root like `docs.v2/papers` would match
112
+ * `docsXv2/papers` too. Cheap to get right, and the failure it prevents is a hook firing about
113
+ * the wrong tree.
114
+ */
115
+ const paperSourceRe = (root) =>
116
+ new RegExp(
117
+ `(?:^|/)${root.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}/([A-Za-z0-9._-]+)/(?:[^/]*\\.tex|(?:.*/)?(?:paper|draft)\\.md)$`,
118
+ );
119
+
120
+ /**
121
+ * The tool invocation, with the script located rather than assumed.
122
+ *
123
+ * 🔴 `require.resolve` INSTEAD OF A HARD-CODED `node_modules/<pkg>/…` PATH. This package must
124
+ * not spell its own install location: a consumer may install it under an alias, and a
125
+ * pnpm/yarn-berry layout is not `node_modules/<name>` at all. `require.resolve` asks the
126
+ * resolver the same question Node itself answers, and it works for a non-JS extension —
127
+ * measured 2026-09-12 against a `.md` and a `.sh` in an installed copy. It runs in a `$(…)`
128
+ * because `run()` goes through a shell (also measured).
129
+ */
130
+ const surface = (dir) =>
131
+ `bash "$(node -p "require.resolve('${CONFIG_KEY}/hooks/paper-status-gates.sh')")" --surface ${dir}`;
132
+
133
+ export default experimental_defineReact({
134
+ on: "PostToolUse",
135
+ match: tools("Edit", "Write", "MultiEdit"),
136
+ // 🔴 THE PATH IS ANCHORED TO THE PROJECT ROOT, and that is not decoration. vigiles runs the
137
+ // provider "via execSync in the hook's cwd", and the hook process's cwd is the consumer's
138
+ // own wiring — a string this hook cannot see. A bare `cat package.json` therefore reads from
139
+ // whatever directory the Bash tool last moved to, and a `cd` into a subdirectory with no
140
+ // manifest breaks the read.
141
+ //
142
+ // ⚠️ THE FAILURE HERE IS SILENT, which makes it more dangerous than its neighbor's.
143
+ // `paper-edit-guard` on PreToolUse DENIES loudly and visibly when the declaration is
144
+ // unreadable. This hook on PostToolUse just returns `nothing()`, i.e. simply stops firing:
145
+ // the `cat` chain fails → empty string → `JSON.parse("")` throws → `papersRoot` returns null
146
+ // → silence. And silence is exactly what a nudge's success state looks like, so a dead hook
147
+ // is indistinguishable from a working one.
148
+ needs: [provide("pkg", 'cat "${CLAUDE_PROJECT_DIR:-.}/package.json"')],
149
+ react: (e) => {
150
+ const root = papersRoot(e.ctx.pkg);
151
+ if (root === null) return nothing();
152
+ const m = paperSourceRe(root).exec(e.path.raw.replace(/^\.\//, ""));
153
+ if (m === null) return nothing();
154
+ return run(surface(m[1]));
155
+ },
156
+ });
@@ -0,0 +1,91 @@
1
+ #!/usr/bin/env bash
2
+ # paper-status-gates.sh — NOT A HOOK. This is a TOOL invoked by `paper-status-gates.hook.mjs`,
3
+ # which owns the event routing and decides WHICH paper (if any) an edit concerned.
4
+ #
5
+ # --surface <paper-dir> print the readiness verdict + run the pipeline checker
6
+ #
7
+ # 🔴 WHAT LEFT THIS FILE AND WHY IT MATTERS. It used to read its payload with
8
+ # `INPUT="${CLAUDE_TOOL_INPUT:-}"` plus a `timeout 1 cat` fallback and then grep that blob for a
9
+ # paper path. `$CLAUDE_TOOL_INPUT` is NOT set by the harness — the payload arrives on stdin — and
10
+ # that single idiom left THREE hooks silently dead for twelve days. None of it survives here:
11
+ # this file receives an already-validated directory NAME as an argument, and it is registered as
12
+ # a hook nowhere, so it cannot contract that disease again.
13
+ #
14
+ # Advisory (stderr), never blocks, always exits 0.
15
+ set -euo pipefail
16
+
17
+ [ "${1:-}" = "--surface" ] || {
18
+ echo "paper-status-gates.sh: a tool, not a hook. Use --surface <paper-dir>." >&2
19
+ exit 0
20
+ }
21
+
22
+ pdir_name="${2:-}"
23
+ [ -n "$pdir_name" ] || exit 0
24
+ # Defence in depth: the hook already anchored this to [A-Za-z0-9._-]+, and it is re-checked here
25
+ # so the file is safe to call directly too.
26
+ case "$pdir_name" in
27
+ *[!A-Za-z0-9._-]* | "" | . | ..) exit 0 ;;
28
+ esac
29
+
30
+ # ── WHERE THE CONSUMER'S THINGS ARE — TWO CARRIERS, READ, NOT ASSUMED ────────
31
+ # This file used to hard-code both of these paths, which made it a tool with exactly one possible
32
+ # user. Both now come from the one declaration every other carrier of this package reads:
33
+ #
34
+ # "paperlint": { "papersDir": "docs/papers", "scripts": "tools/pipeline" }
35
+ #
36
+ # (The key's name before 2.0.0, "research-paper-pipeline", is still read.)
37
+ #
38
+ # ⚠️ `node -p` RATHER THAN grep/sed ON package.json. A JSON value is not a line of text: it can
39
+ # be quoted, escaped, or spread across lines, and a pattern that gets it right today gets it
40
+ # wrong on the first reformat — silently, by producing a root that matches nothing. Node is
41
+ # already a hard dependency of the checker this script runs, so there is no new requirement.
42
+ ROOT_DIR="${CLAUDE_PROJECT_DIR:-.}"
43
+ read_key() { # $1 = key, $2 = default
44
+ node -p "((p) => p['paperlint'] ?? p['research-paper-pipeline'] ?? {})(require('$ROOT_DIR/package.json'))['$1'] ?? '$2'" \
45
+ 2>/dev/null || echo "$2"
46
+ }
47
+ # The field used to be called "papers". Every other reader refuses the old name; this script only
48
+ # gives advice, so it stays silent instead of falling back to the default directory.
49
+ [ -z "$(read_key papers '')" ] || exit 0
50
+ papers_root="$(read_key papersDir papers)"
51
+ scripts_root="$(read_key scripts .claude/skills/paper-pipeline/scripts)"
52
+
53
+ # 🔴 AN UNUSABLE ROOT EXITS, IT DOES NOT BUILD A PATH FROM IT. `node -p` prints `undefined` for a
54
+ # key whose value is literally `null` (`?? ` only catches null/undefined AFTER the object lookup,
55
+ # and a `null` value reaches the default — but a malformed package.json makes the whole command
56
+ # fail and the `|| echo` hands back the default). Either way, an empty or nonsense value would
57
+ # produce `/<paper>/PIPELINE-STATUS.md` rooted at the filesystem, which does not exist, and this
58
+ # script would exit 0 looking like a paper with nothing to report. Named rather than risked.
59
+ case "$papers_root" in
60
+ "" | undefined | null) exit 0 ;;
61
+ esac
62
+
63
+ pdir="$papers_root/$pdir_name"
64
+ STATUS="$ROOT_DIR/$pdir/PIPELINE-STATUS.md"
65
+ [ -f "$STATUS" ] || exit 0
66
+
67
+ # 🔴 SURFACE THE VERDICT, UNPROMPTED — see the header: the checker parsed this line and never
68
+ # printed it, so the single fact anyone actually wanted was the one the tooling withheld.
69
+ VERDICT=$(grep -m1 -E '^\*\*Readiness verdict:\*\*' "$STATUS" 2>/dev/null || true)
70
+ if [ -n "$VERDICT" ]; then
71
+ echo "📊 $(echo "$VERDICT" | sed -E 's/\*\*//g' | cut -c1-200)" >&2
72
+ else
73
+ echo "📊 PIPELINE-STATUS.md has NO '**Readiness verdict:**' line — the scorecard cannot say" >&2
74
+ echo " whether this paper is submittable, which is the one question it exists to answer." >&2
75
+ fi
76
+
77
+ CHECK="$ROOT_DIR/$scripts_root/pipeline-check.mjs"
78
+ if [ -f "$CHECK" ]; then
79
+ # The real checker: stale continuous passes, gates missing their declared inputs, a study that
80
+ # ran before the claim was stated, submit-access inside the moderation window, a verdict that is
81
+ # only a percentage, unrun gates. Advisory, always exit 0.
82
+ node "$CHECK" "$ROOT_DIR/$pdir" >&2 || true
83
+ else
84
+ # Fallback for a checkout without the skill tree — the old crude surface.
85
+ gates=$(grep -E '☐' "$STATUS" 2>/dev/null | grep -iE 'study-accepted|pc-panel|harden|verify.?citations' || true)
86
+ if [ -n "$gates" ]; then
87
+ echo "⚠️ UNRUN GATES in $pdir/PIPELINE-STATUS.md (☐ — run them, don't 'anti-churn'-skip):" >&2
88
+ echo "$gates" | sed -E 's/\|.*//; s/^\| */ /' >&2
89
+ fi
90
+ fi
91
+ exit 0
@@ -0,0 +1,165 @@
1
+ /**
2
+ * `observeAgentCli` — both halves, every case staged through an INJECTED spawner.
3
+ *
4
+ * The case that matters most is 4: a probe that discards its output. It cannot be staged at
5
+ * all against a real `spawnSync`, because there is no way to ask the installed binary to print
6
+ * nothing — which is precisely why the observation had to leave the harness that spawns it
7
+ * before it could be tested (issue #7).
8
+ */
9
+ import assert from "node:assert/strict";
10
+ import { recordCheck } from "vigiles";
11
+ import { observeAgentCli, CHARACTERIZED_CLI } from "./agent-cli-version.mjs";
12
+
13
+ /** A spawner that records how it was called, so the CALL SHAPE is assertable, not just the result. */
14
+ const spawner = (result) => {
15
+ const seen = [];
16
+ const fn = (program, args, options) => {
17
+ seen.push({ program, args, options });
18
+ return result;
19
+ };
20
+ fn.seen = seen;
21
+ return fn;
22
+ };
23
+
24
+ const cases = [];
25
+
26
+ // ── 1. BINARY ABSENT: no throw, and `present` says so — the skip path of the caller.
27
+ {
28
+ const spawnSync = spawner({ status: 127, stdout: "", stderr: "not found" });
29
+ const o = observeAgentCli({ spawnSync });
30
+ assert.equal(
31
+ o.present,
32
+ false,
33
+ "a non-zero probe means the binary is not usable",
34
+ );
35
+ assert.equal(o.version, "", "and there is no version to report");
36
+ assert.match(
37
+ o.note,
38
+ /not installed/,
39
+ "the note must say WHY there is nothing to observe",
40
+ );
41
+ cases.push("CLI absent → present:false, no throw, note explains");
42
+ }
43
+
44
+ // ── 2. 🔴 DRIFT IS NAMED, NOT INFERRED. The measured case from issue #7: the machine carries
45
+ // 2.1.273 while the assertions were written against 2.1.227.
46
+ {
47
+ const spawnSync = spawner({
48
+ status: 0,
49
+ stdout: "2.1.273 (Claude Code)\n",
50
+ stderr: "",
51
+ });
52
+ const o = observeAgentCli({ spawnSync });
53
+ assert.equal(o.present, true);
54
+ assert.equal(
55
+ o.version,
56
+ "2.1.273",
57
+ "the version must be PARSED out of the line, not left raw",
58
+ );
59
+ assert.equal(
60
+ o.raw,
61
+ "2.1.273 (Claude Code)",
62
+ "and the full line kept for the report",
63
+ );
64
+ assert.equal(
65
+ o.characterized,
66
+ CHARACTERIZED_CLI,
67
+ "the characterized number rides along as a value",
68
+ );
69
+ assert.equal(
70
+ o.drifted,
71
+ true,
72
+ "2.1.273 ≠ 2.1.227 is drift and must be computed, not eyeballed",
73
+ );
74
+ assert.ok(
75
+ o.note.includes("2.1.273") && o.note.includes(CHARACTERIZED_CLI),
76
+ `the note must name BOTH numbers so a reader sees them diverge; got: ${o.note}`,
77
+ );
78
+ cases.push(
79
+ "drifted CLI → version parsed, drifted:true, note names BOTH numbers",
80
+ );
81
+ }
82
+
83
+ // ── 3. NO DRIFT: the same machinery must stay quiet when the numbers agree, or `drifted`
84
+ // would be a constant rather than a comparison.
85
+ {
86
+ const spawnSync = spawner({
87
+ status: 0,
88
+ stdout: `${CHARACTERIZED_CLI} (Claude Code)\n`,
89
+ stderr: "",
90
+ });
91
+ const o = observeAgentCli({ spawnSync });
92
+ assert.equal(o.drifted, false, "matching versions are not drift");
93
+ assert.equal(o.version, CHARACTERIZED_CLI);
94
+ cases.push("CLI at the characterized version → drifted:false");
95
+ }
96
+
97
+ // ── 4. 🔴 THE DEFECT ITSELF: exit 0, nothing on stdout — what `stdio: "ignore"` produced.
98
+ // Reporting `characterized against ""` would be issue #7 wearing new clothes, so this
99
+ // must THROW rather than return an empty observation.
100
+ {
101
+ const spawnSync = spawner({ status: 0, stdout: null, stderr: "" });
102
+ assert.throws(
103
+ () => observeAgentCli({ spawnSync }),
104
+ /is not an observation/,
105
+ "exit 0 with no version on stdout must fail loudly, not be reported as a version",
106
+ );
107
+ cases.push(
108
+ 'exit 0 + empty stdout (the `stdio: "ignore"` shape) → throws, does not report a blank',
109
+ );
110
+ }
111
+
112
+ // ── 5. THE CALL SHAPE, asserted directly. Case 4 catches a discarding probe only because the
113
+ // double returns nothing; a real `stdio: "ignore"` would ALSO have to be caught if some
114
+ // other path happened to fill stdout. So the options handed to the spawner are checked.
115
+ {
116
+ const spawnSync = spawner({
117
+ status: 0,
118
+ stdout: "2.1.273 (Claude Code)\n",
119
+ stderr: "",
120
+ });
121
+ observeAgentCli({ spawnSync });
122
+ const { args, options } = spawnSync.seen[0];
123
+ assert.deepEqual(
124
+ args,
125
+ ["--version"],
126
+ "the observation must ask for the version",
127
+ );
128
+ assert.equal(
129
+ options.encoding,
130
+ "utf8",
131
+ "without an encoding the output is a Buffer nobody reads",
132
+ );
133
+ assert.notEqual(
134
+ options.stdio,
135
+ "ignore",
136
+ '`stdio: "ignore"` is the discarded-output defect itself',
137
+ );
138
+ cases.push("the probe is spawned with encoding:utf8 and never stdio:ignore");
139
+ }
140
+
141
+ // ── 6. AND THE REAL BINARY, when this machine has one. Cases 1-5 are about the module; this
142
+ // one is about reality — an observer that only ever meets a double proves nothing about
143
+ // the output format of the tool it exists to read.
144
+ {
145
+ const { spawnSync } = await import("node:child_process");
146
+ const o = observeAgentCli({ spawnSync });
147
+ if (o.present) {
148
+ assert.match(
149
+ o.version,
150
+ /^\d+\.\d+\.\d+$/,
151
+ `the real CLI's version must parse; raw was ${JSON.stringify(o.raw)}`,
152
+ );
153
+ cases.push(
154
+ `real \`claude\` binary present → observed ${o.version} (characterized ${o.characterized}${o.drifted ? ", DRIFTED" : ""})`,
155
+ );
156
+ } else {
157
+ cases.push(
158
+ "real `claude` binary absent on this machine → absent path exercised for real",
159
+ );
160
+ }
161
+ }
162
+
163
+ recordCheck(cases.length);
164
+ console.log(`observeAgentCli: ${cases.length} cases:`);
165
+ for (const c of cases) console.log(` ok ${c}`);
@@ -0,0 +1,106 @@
1
+ /**
2
+ * OBSERVE the `claude` CLI version instead of merely probing for the binary.
3
+ *
4
+ * ── THE DEFECT THIS EXISTS FOR (issue #7, measured) ─────────────────────────────
5
+ * `plan-paper-timeline.effects.harness.mjs` is a CHARACTERIZATION test: its job is to go red
6
+ * when the upstream CLI changes under it. It named the version it characterized — 2.1.227 —
7
+ * in prose, and then threw away the version it actually saw:
8
+ *
9
+ * spawnSync("claude", ["--version"], { stdio: "ignore" }) // ← only `status` was read
10
+ *
11
+ * `stdio: "ignore"` discards the output, so `claude --version` ran as a PRESENCE probe. The
12
+ * string it printed was never read, never reported and never compared with the prose. Measured
13
+ * on this machine: the binary is **2.1.273**, the docblock says **2.1.227** — forty-six patch
14
+ * releases of drift, invisible to a reader of a green log.
15
+ *
16
+ * 🔴 WHY NOT PIN THE VERSION. A pin defeats the test: it is SUPPOSED to see upstream move. The
17
+ * repair is not to stop the drift but to stop the SILENCE about it — observe what is already
18
+ * spawned, carry the characterized number as a VALUE beside it, and let a reader see the two
19
+ * diverge instead of inferring it. CI installs `@anthropic-ai/claude-code` unpinned on purpose.
20
+ *
21
+ * ── WHY A MODULE AND NOT THREE LINES IN THE HARNESS ─────────────────────────────
22
+ * The three lines the issue suggests are correct and untestable where they were proposed. The
23
+ * harness that would exercise them spawns the real CLI, so its assertions about the observation
24
+ * depend on which version the machine happens to carry — and the one case that matters most,
25
+ * "the probe discards its output", cannot be staged at all when the spawn is hard-wired. With
26
+ * the spawner INJECTED, every case becomes deterministic: absent binary, drifted version,
27
+ * matching version, and the discarding probe itself.
28
+ */
29
+
30
+ /**
31
+ * The version the effects harness's characterization was written against.
32
+ *
33
+ * 🔴 A VALUE, NOT A SENTENCE, and that is the whole point of this constant. The number lived in
34
+ * a docblock, where nothing could compare it with anything. Here it is compared on every run.
35
+ * Moving it means deliberately re-characterizing — which is the edit that should be conscious.
36
+ */
37
+ export const CHARACTERIZED_CLI = "2.1.227";
38
+
39
+ /** What `claude --version` prints: `2.1.273 (Claude Code)`. */
40
+ const VERSION_IN = /\b(\d+\.\d+\.\d+)\b/;
41
+
42
+ /**
43
+ * @typedef {object} CliObservation
44
+ * @property {boolean} present the binary answered `--version` with status 0
45
+ * @property {string} raw the full line it printed, trimmed
46
+ * @property {string} version the dotted version parsed out of it
47
+ * @property {string} characterized the version the assertions were written against
48
+ * @property {boolean} drifted observed and characterized differ
49
+ * @property {string} note one line naming BOTH numbers, for the report and for failures
50
+ */
51
+
52
+ /**
53
+ * @param {object} deps
54
+ * @param {Function} deps.spawnSync injected, so every case below is stageable
55
+ * @param {string} [deps.program]
56
+ * @param {string} [deps.characterized]
57
+ * @returns {CliObservation}
58
+ */
59
+ export function observeAgentCli({
60
+ spawnSync,
61
+ program = "claude",
62
+ characterized = CHARACTERIZED_CLI,
63
+ }) {
64
+ // 🔴 `encoding: "utf8"` IS THE FIX. With `stdio: "ignore"` — the shape this replaces — the
65
+ // child's output goes nowhere and `probe.stdout` comes back null, so everything downstream
66
+ // would be describing a string that was never read.
67
+ const probe = spawnSync(program, ["--version"], { encoding: "utf8" });
68
+
69
+ if (probe.status !== 0)
70
+ return {
71
+ present: false,
72
+ raw: "",
73
+ version: "",
74
+ characterized,
75
+ drifted: false,
76
+ note: `the \`${program}\` CLI is not installed, so no run can be observed`,
77
+ };
78
+
79
+ const raw = (probe.stdout ?? "").trim();
80
+ const version = VERSION_IN.exec(raw)?.[1] ?? "";
81
+
82
+ // ⚠️ EXIT 0 AND NOTHING ON STDOUT IS NOT A CLEAN OBSERVATION — it is exactly what the discarded
83
+ // probe looked like, and reporting `characterized against ""` would be the same lie in a new
84
+ // place. This is rule 4 of CLAUDE.md applied to the check that enforces it.
85
+ if (version === "")
86
+ throw new Error(
87
+ `\`${program} --version\` exited 0 but printed no version on stdout (got ${JSON.stringify(raw)}` +
88
+ `${probe.stderr ? `, stderr ${JSON.stringify(String(probe.stderr).trim().slice(0, 200))}` : ""}). ` +
89
+ `A presence probe is not an observation: this is the \`stdio: "ignore"\` shape that issue #7 ` +
90
+ `is about, and a version nobody read must not be reported as one.`,
91
+ );
92
+
93
+ const drifted = version !== characterized;
94
+ return {
95
+ present: true,
96
+ raw,
97
+ version,
98
+ characterized,
99
+ drifted,
100
+ note: drifted
101
+ ? `observed \`${program}\` ${version} — the assertions here were characterized against ` +
102
+ `${characterized}. They still hold, which is itself the result: a characterization test that ` +
103
+ `silently still holds is indistinguishable from one nobody re-checked.`
104
+ : `observed \`${program}\` ${version} — the version these assertions were characterized against.`,
105
+ };
106
+ }
@@ -0,0 +1,109 @@
1
+ /**
2
+ * Battery for `observeAgentCli`. Five mutations, five different assertions.
3
+ *
4
+ * 🔴 The first one is the issue #7 defect itself, restored verbatim: bring back
5
+ * `stdio: "ignore"`. It is the whole point of this battery. Before the observation was pulled
6
+ * out into its own module, there was NOWHERE to place this mutation: the spawn was baked into
7
+ * the harness, which brings up the real CLI, and "ask the binary to print nothing" is not
8
+ * something you can do. A green battery against a baked-in spawn would have been a finding
9
+ * about the test, not about the defense.
10
+ */
11
+ import { fileURLToPath } from "node:url";
12
+ import { dirname, join, resolve } from "node:path";
13
+ import { runMutations } from "./mutation-driver.mjs";
14
+
15
+ const HERE = dirname(fileURLToPath(import.meta.url));
16
+ const ROOT = resolve(HERE, "..");
17
+ const MOD = join(HERE, "agent-cli-version.mjs");
18
+ const HARNESS = join(HERE, "agent-cli-version.harness.mjs");
19
+
20
+ process.exit(
21
+ runMutations({
22
+ root: ROOT,
23
+ runner: "node",
24
+ cases: [
25
+ {
26
+ name: '🔴 revert to `stdio: "ignore"` — the issue #7 defect verbatim',
27
+ harness: HARNESS,
28
+ // ⚠️ The assertion this trips is NOT the one I was aiming for, and this was corrected
29
+ // BY RUNNING IT, not from memory. The double hands back stdout regardless of the
30
+ // options, so the observation succeeds, and the first thing to go red is the
31
+ // `encoding` assertion. The `stdio` assertion itself is hit by the next case.
32
+ expect: "without an encoding the output is a Buffer nobody reads",
33
+ disables:
34
+ "the observation itself — the version is printed into the void again, and the tier characterizes nothing",
35
+ edits: [
36
+ [
37
+ MOD,
38
+ 'spawnSync(program, ["--version"], { encoding: "utf8" })',
39
+ 'spawnSync(program, ["--version"], { stdio: "ignore" })',
40
+ ],
41
+ ],
42
+ },
43
+ {
44
+ name: '`stdio: "ignore"` added ALONGSIDE encoding — output still goes nowhere',
45
+ harness: HARNESS,
46
+ expect: "is the discarded-output defect itself",
47
+ disables:
48
+ "the second call-shape assertion: `encoding` is present, but the stream is still killed",
49
+ edits: [
50
+ [
51
+ MOD,
52
+ 'spawnSync(program, ["--version"], { encoding: "utf8" })',
53
+ 'spawnSync(program, ["--version"], { encoding: "utf8", stdio: "ignore" })',
54
+ ],
55
+ ],
56
+ },
57
+ {
58
+ name: "empty stdout at exit code 0 stops being a failure",
59
+ harness: HARNESS,
60
+ expect: "must fail loudly, not be reported as a version",
61
+ disables:
62
+ "rule 4 — `exit 0` with empty output reads as a clean measurement again",
63
+ edits: [[MOD, 'if (version === "")', "if (false)"]],
64
+ },
65
+ {
66
+ name: "drift is computed as a constant, not a comparison",
67
+ harness: HARNESS,
68
+ expect: "must be computed, not eyeballed",
69
+ disables:
70
+ "the comparison of observed against characterized — the divergence becomes invisible again",
71
+ edits: [
72
+ [
73
+ MOD,
74
+ "const drifted = version !== characterized;",
75
+ "const drifted = false;",
76
+ ],
77
+ ],
78
+ },
79
+ {
80
+ name: "only one of the two numbers survives into the message",
81
+ harness: HARNESS,
82
+ expect: "must name BOTH numbers",
83
+ disables:
84
+ "showing the divergence — the reader is back to having to remember the second number",
85
+ edits: [
86
+ [
87
+ MOD,
88
+ "`observed \\`${program}\\` ${version} — the assertions here were characterized against ` +\n `${characterized}.",
89
+ "`observed \\`${program}\\` ${version} — characterized elsewhere.",
90
+ ],
91
+ ],
92
+ },
93
+ {
94
+ name: "the version is not parsed out of the string, taken whole instead",
95
+ harness: HARNESS,
96
+ expect: "must be PARSED out of the line, not left raw",
97
+ disables:
98
+ "parsing the version string — `2.1.273 (Claude Code)` stops being comparable to `2.1.227`",
99
+ edits: [
100
+ [
101
+ MOD,
102
+ 'const version = VERSION_IN.exec(raw)?.[1] ?? "";',
103
+ "const version = raw;",
104
+ ],
105
+ ],
106
+ },
107
+ ],
108
+ }),
109
+ );