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,713 @@
1
+ /**
2
+ * hooks.harness.mjs β€” the three shipped hooks, each asserted in BOTH halves.
3
+ *
4
+ * πŸ”΄ WHY "BOTH HALVES" IS THE ORGANISING RULE HERE AND NOT A STYLE. Two of these three are
5
+ * ADVISORY: silence is their success state. So "it printed nothing" and "it is dead" look
6
+ * identical from outside, and a harness that only checks the quiet case is green over a corpse.
7
+ * Three hooks in the first consumer were dead for twelve days behind exactly that. Every block
8
+ * below therefore pairs a firing case with a silent one.
9
+ *
10
+ * ── WHAT IS MEASURED: BYTES AND EXIT CODES, NOT THE RETURNED OBJECT ─────────
11
+ * πŸ”΄ The first consumer's harness asserts over the REACTION β€” the object the pure function
12
+ * returned β€” and that is why a 184-assertion suite could not see that nine of its nudges were
13
+ * being delivered nowhere. "The hook fired" and "the hook's text landed" are different claims
14
+ * and the second does not follow from the first. Here every assertion reads `exitCode`,
15
+ * `stdout` and `stderr` of a real `run-program`, through `runHook`.
16
+ *
17
+ * ── THE CAPABILITY SURFACE IS ASSERTED ON THE ARTIFACT THAT EXECUTES ────────
18
+ * These hooks ship as `.mjs`, so `vigiles compile` β€” which normally enforces "a compiled hook
19
+ * may import ONLY `vigiles/hook`" β€” never sees them. Part I runs that same check,
20
+ * `checkHookImports`, over the shipped files directly. That is STRICTER than compiling a twin
21
+ * `.ts` source would be: a twin can drift from its build, and there is no twin here to drift.
22
+ *
23
+ * Run: `npx vigiles test hooks/hooks.harness.mjs`
24
+ * Killed by: `hooks/hooks.mutations.mjs`
25
+ */
26
+ import assert from "node:assert/strict";
27
+ import { runHook } from "vigiles";
28
+ import { checkHookImports } from "vigiles/hook";
29
+ import {
30
+ mkdtempSync,
31
+ mkdirSync,
32
+ readdirSync,
33
+ readFileSync,
34
+ rmSync,
35
+ symlinkSync,
36
+ writeFileSync,
37
+ realpathSync,
38
+ } from "node:fs";
39
+ import { tmpdir } from "node:os";
40
+ import { dirname, join, resolve } from "node:path";
41
+ import { fileURLToPath } from "node:url";
42
+ import {
43
+ OLD_PAPERS_DIR_FIELD,
44
+ PAPERS_DIR_FIELD,
45
+ } from "../lib/paper-config.mjs";
46
+
47
+ const HOOKS = dirname(fileURLToPath(import.meta.url));
48
+ const ROOT = resolve(HOOKS, "..");
49
+ const CLI = join(ROOT, "node_modules", "vigiles", "dist", "cli.js");
50
+ /** `>` built rather than typed: a literal one in this file is a redirection to the guard's own scan. */
51
+ const GT = String.fromCharCode(62);
52
+
53
+ const SHIPPED = readdirSync(HOOKS)
54
+ .filter((f) => f.endsWith(".hook.mjs"))
55
+ .sort();
56
+
57
+ let n = 0;
58
+ const check = (label, cond) => {
59
+ assert.ok(cond, label);
60
+ n++;
61
+ };
62
+
63
+ /**
64
+ * A throwaway consumer repository: a `package.json` with the given block, a papers tree, and
65
+ * `node_modules` wired so both `vigiles/hook` and `require.resolve("<this package>/…")` work
66
+ * from inside it.
67
+ *
68
+ * πŸ”΄ IT IS A REAL DIRECTORY, NOT A MOCK, because what is under test is precisely the part a
69
+ * mock would stub out: whether the hook can READ a declaration out of the consumer's
70
+ * `package.json` via a provider command, and whether a path resolves once this package is
71
+ * installed rather than adjacent. The `node_modules/<pkg>` entry is a symlink to this very
72
+ * checkout, which is what `npm install` of a local package does anyway.
73
+ */
74
+ const consumer = (block, { papers = "docs/papers", paper = "alpha" } = {}) => {
75
+ const dir = realpathSync(mkdtempSync(join(tmpdir(), "rpp-hooks-")));
76
+ const nm = join(dir, "node_modules");
77
+ mkdirSync(nm, { recursive: true });
78
+ symlinkSync(join(ROOT, "node_modules", "vigiles"), join(nm, "vigiles"));
79
+ symlinkSync(ROOT, join(nm, "paperlint"));
80
+ writeFileSync(
81
+ join(dir, "package.json"),
82
+ block === null
83
+ ? '{"name":"c","type":"module"}\n'
84
+ : JSON.stringify({ name: "c", type: "module", ...block }) + "\n",
85
+ );
86
+ if (papers !== null) {
87
+ mkdirSync(join(dir, papers, paper), { recursive: true });
88
+ writeFileSync(
89
+ join(dir, papers, paper, "PIPELINE-STATUS.md"),
90
+ "**Readiness verdict:** βœ… fixture verdict line\n\n| gate | state |\n| study-accepted | ☐ |\n",
91
+ );
92
+ }
93
+ return dir;
94
+ };
95
+
96
+ /** `run-program <hook>` exactly as a consumer's settings block invokes it. */
97
+ const program = (name) =>
98
+ `node ${JSON.stringify(CLI)} hook-runtime run-program ${JSON.stringify(join(HOOKS, name + ".hook.mjs"))}`;
99
+
100
+ const onBash = (command) => ({
101
+ hook_event_name: "PreToolUse",
102
+ tool_name: "Bash",
103
+ tool_input: { command },
104
+ });
105
+ const onEdit = (file_path) => ({
106
+ hook_event_name: "PostToolUse",
107
+ tool_name: "Edit",
108
+ tool_input: { file_path },
109
+ tool_response: {},
110
+ });
111
+ const STOP = { hook_event_name: "Stop", tool_name: "", tool_input: {} };
112
+
113
+ /** Run a hook AS a consumer: cwd and `CLAUDE_PROJECT_DIR` both point at the fixture repo. */
114
+ const at = (dir, name, input) =>
115
+ runHook(program(name), input, { cwd: dir, env: { CLAUDE_PROJECT_DIR: dir } });
116
+
117
+ /**
118
+ * The same hook, run with the tool's cwd DRIFTED away from the project root.
119
+ *
120
+ * πŸ”΄ Why this helper has to exist at all: `at()` sets `cwd` and `CLAUDE_PROJECT_DIR` to the SAME
121
+ * directory, so a provider reading `package.json` relatively and one reading it anchored are
122
+ * indistinguishable under it. The defect was therefore invisible by construction β€” not missed
123
+ * by inattention, but unexpressible by the only runner the harness had.
124
+ *
125
+ * ⚠️ The drifted cwd is deliberately a directory with NO `package.json`, because that is the
126
+ * live case: a session `cd`s into a subtree, and vigiles runs every provider Β«via execSync in
127
+ * the hook's cwdΒ».
128
+ */
129
+ const adrift = (dir, name, input) =>
130
+ runHook(program(name), input, {
131
+ cwd: realpathSync(mkdtempSync(join(tmpdir(), "drift-"))),
132
+ env: { CLAUDE_PROJECT_DIR: dir },
133
+ });
134
+
135
+ /** An injected notice, as the model would receive it β€” not the returned reaction. */
136
+ const injected = (r) => {
137
+ try {
138
+ return JSON.parse(r.stdout).hookSpecificOutput.additionalContext ?? "";
139
+ } catch {
140
+ return "";
141
+ }
142
+ };
143
+
144
+ const tmps = [];
145
+ const fixture = (...args) => {
146
+ const d = consumer(...args);
147
+ tmps.push(d);
148
+ return d;
149
+ };
150
+
151
+ try {
152
+ // ═══════════════════════════════════════════════════════════════════════════
153
+ // I. CAPABILITY CLOSURE β€” on the shipped artifact, not on a source twin
154
+ // ═══════════════════════════════════════════════════════════════════════════
155
+ // ⚠️ `checkHookImports` IS A REGEX OVER THE SOURCE TEXT, so it counts an import-shaped
156
+ // sentence inside a COMMENT as an offender. Measured 2026-09-12: this block failed on the
157
+ // guard's own docblock, which quoted the rejected form verbatim while explaining why it was
158
+ // rejected. That is the product's own instrument landing on the "a string is the SHADOW of a
159
+ // thing" rule β€” and the fix is NOT to loosen the check, because it is the same check any
160
+ // future `vigiles compile`/`lint` pass would apply to the shipped artifact. It is to describe
161
+ // a forbidden import in prose WITHOUT writing one. Anyone extending these headers: do not
162
+ // quote an import statement in a comment here; name it in words.
163
+ check("there are shipped hooks to check at all", SHIPPED.length === 3);
164
+ for (const f of SHIPPED) {
165
+ const offenders = checkHookImports(readFileSync(join(HOOKS, f), "utf8"));
166
+ check(
167
+ `${f} imports ONLY vigiles/hook (offenders: ${offenders.join(", ") || "none"})`,
168
+ offenders.length === 0,
169
+ );
170
+ }
171
+
172
+ // ═══════════════════════════════════════════════════════════════════════════
173
+ // II. EVERY SHIPPED HOOK LOADS β€” FIRST among the behavioural blocks, deliberately
174
+ // ═══════════════════════════════════════════════════════════════════════════
175
+ // πŸ”΄ A hook that cannot resolve its imports prints `vigiles: cannot load hook program` and,
176
+ // for a PreToolUse gate, REFUSES EVERY COMMAND. In the first consumer that state was reached
177
+ // five separate times β€” a lockfile behind its manifest, a missing `node_modules`, a merge
178
+ // conflict on the import path, a removed export after a major bump. The behavioural blocks
179
+ // below do go red for it, but they name a hundred other things first: "blocks a redirect
180
+ // write" is a long way from "your runtime lacks the export this hook imports".
181
+ //
182
+ // πŸ”‘ IT RUNS EARLY, AND THAT PLACEMENT IS THE CHECK β€” node's assert aborts on the first
183
+ // failure, so a diagnostic written at the END only ever prints when nothing else is broken,
184
+ // which is when it diagnoses nothing.
185
+ {
186
+ const dir = fixture({
187
+ paperlint: { [PAPERS_DIR_FIELD]: "docs/papers" },
188
+ });
189
+ for (const f of SHIPPED) {
190
+ const name = f.replace(/\.hook\.mjs$/, "");
191
+ const r = at(dir, name, STOP);
192
+ check(
193
+ `${name} LOADS (the runtime can resolve its imports)`,
194
+ !/cannot load hook program/.test(r.stdout + r.stderr),
195
+ );
196
+ }
197
+ }
198
+
199
+ // ═══════════════════════════════════════════════════════════════════════════
200
+ // III. paper-edit-guard β€” the blocking half and the allowing half
201
+ // ═══════════════════════════════════════════════════════════════════════════
202
+ {
203
+ const dir = fixture({
204
+ paperlint: { [PAPERS_DIR_FIELD]: "docs/papers" },
205
+ });
206
+ const P = "docs/papers/alpha/paper.md";
207
+ const T = "docs/papers/alpha/paper.tex";
208
+ const deny = (label, cmd) => {
209
+ const r = at(dir, "paper-edit-guard", onBash(cmd));
210
+ check(
211
+ `guard BLOCKS ${label} (rc=${r.exitCode})`,
212
+ r.exitCode === 2 && r.blocked,
213
+ );
214
+ check(
215
+ `guard's refusal of ${label} says what to do instead`,
216
+ /Use Edit or Write/.test(r.stderr),
217
+ );
218
+ };
219
+ const allow = (label, cmd) => {
220
+ const r = at(dir, "paper-edit-guard", onBash(cmd));
221
+ check(
222
+ `guard ALLOWS ${label} (rc=${r.exitCode}, ${r.stderr.length}b err)`,
223
+ r.exitCode === 0 && !r.blocked,
224
+ );
225
+ };
226
+
227
+ // The two cases `touches` alone MISSES: a redirection target is not an argv token.
228
+ deny("a redirect write to paper.md", `echo x ${GT} ${P}`);
229
+ deny("an append write to a .tex", `cat /tmp/a ${GT}${GT} ${T}`);
230
+ deny("sed -i on a paper", `sed -i s/a/b/ ${P}`);
231
+ deny("cp onto a paper", `cp /tmp/a ${P}`);
232
+ // The quote bypasses: both were OPEN until 2026-08-13 and each was one quote wide.
233
+ deny("a QUOTED redirect target", `echo x ${GT} "${P}"`);
234
+ deny("a QUOTED argv path", `sed -i s/a/b/ "${T}"`);
235
+ // The wrapper bypass: `runs()` reads RAW leaves, `writesTo`/`touches` read NORMALIZED ones.
236
+ deny("a mutator behind `env`", `env sed -i s/a/b/ ${T}`);
237
+
238
+ allow("reading a paper", `cat ${P}`);
239
+ allow("an unrelated command", "echo hi");
240
+ allow("an unrelated write", `echo x ${GT} /tmp/unrelated.txt`);
241
+ // The false positives that blocked real work: a bundle copy is data, and a grep is a read.
242
+ allow(
243
+ "a copy into a paper's repro bundle",
244
+ `cp h.mjs docs/papers/alpha/repro/anon/h.mjs`,
245
+ );
246
+ // A FROZEN SNAPSHOT is archival, not the live source: `versions/` is immutable by
247
+ // construction and `paper/stages` gates it by bytes, which is stricter than anything the
248
+ // PostToolUse checks this guard protects would say about it.
249
+ allow(
250
+ "freezing a snapshot into versions/",
251
+ `cp /tmp/frozen.tex docs/papers/alpha/versions/2026-08-29-camera-ready.tex`,
252
+ );
253
+ allow(
254
+ "a redirect into versions/",
255
+ `echo x ${GT} docs/papers/alpha/versions/a.tex`,
256
+ );
257
+ // ⚠️ NOT exempt, and deliberately asserted so the limit is recorded rather than discovered:
258
+ // with the LIVE source as the copy's argument, `namesPaperSource` hits on that token, and
259
+ // argv carries no direction β€” `cp a b` and `cp b a` are the same shape. Freezing therefore
260
+ // goes through a temp file, which is what the refusal already tells the caller to do.
261
+ deny(
262
+ "a copy whose SOURCE argument is the live paper",
263
+ `cp ${T} docs/papers/alpha/versions/a.tex`,
264
+ );
265
+ // πŸ”΄ The other half: the carve-out must not reach the LIVE source, which keeps the same
266
+ // extension. Without this assert the exemption could be widened to `.tex` and stay green.
267
+ deny("sed -i on the live source beside versions/", `sed -i s/a/b/ ${T}`);
268
+ deny(
269
+ "a write to a .tex that merely MENTIONS versions",
270
+ `cp /tmp/versions/a.tex ${T}`,
271
+ );
272
+ allow(
273
+ "a grep of a status file",
274
+ `grep -c x docs/papers/alpha/PIPELINE-STATUS.md 2${GT}/dev/null`,
275
+ );
276
+ }
277
+
278
+ // ═══════════════════════════════════════════════════════════════════════════
279
+ // III-b. CWD DRIFT β€” every hook reads its manifest from the PROJECT ROOT
280
+ // ═══════════════════════════════════════════════════════════════════════════
281
+ // πŸ”΄ Reported live 2026-09-16 by a consumer session: after a `cd` into a subdirectory with no
282
+ // `package.json`, `paper-edit-guard` denied EVERY Bash command β€” `pwd` included β€” and the wedge
283
+ // could not be escaped from the shell, because a PreToolUse hook fires before the command that
284
+ // would fix it. The cause is one character of scope: the provider read `package.json`
285
+ // relatively, and vigiles runs providers Β«via execSync in the hook's cwdΒ».
286
+ //
287
+ // πŸ”΄ AND THE SIBLINGS FAIL THE OTHER WAY, WHICH IS WHY THEY GET THEIR OWN ASSERTIONS. They are
288
+ // PostToolUse and answer `nothing()` when the root will not parse, so a drifted cwd does not
289
+ // block anything β€” it turns them OFF. Silence is a nudge's success state, so a dead nudge and a
290
+ // working one produce identical output. The lockout announces itself; this does not.
291
+ {
292
+ const dir = fixture({
293
+ paperlint: { [PAPERS_DIR_FIELD]: "docs/papers" },
294
+ });
295
+
296
+ // paper-edit-guard: still GUARDS from a foreign cwd, rather than denying everything.
297
+ const write = at(
298
+ dir,
299
+ "paper-edit-guard",
300
+ onBash(`sed -i s/a/b/ docs/papers/alpha/paper.tex`),
301
+ );
302
+ check("drift Β· guard still blocks a paper write", write.exitCode === 2);
303
+ const idle = adrift(dir, "paper-edit-guard", onBash("echo hi"));
304
+ check(
305
+ `drift Β· guard does NOT deny an unrelated command (rc=${idle.exitCode})`,
306
+ idle.exitCode === 0,
307
+ );
308
+ const guarded = adrift(
309
+ dir,
310
+ "paper-edit-guard",
311
+ onBash(`sed -i s/a/b/ docs/papers/alpha/paper.tex`),
312
+ );
313
+ check(
314
+ "drift Β· guard STILL blocks the paper write it exists for",
315
+ guarded.exitCode === 2,
316
+ );
317
+
318
+ // The two PostToolUse hooks: still SPEAK from a foreign cwd.
319
+ const nudge = adrift(
320
+ dir,
321
+ "paper-skills-nudge",
322
+ onEdit(`${dir}/docs/papers/alpha/paper.tex`),
323
+ );
324
+ check("drift Β· the skills nudge still fires", injected(nudge).length > 0);
325
+ const gates = adrift(
326
+ dir,
327
+ "paper-status-gates",
328
+ onEdit(`${dir}/docs/papers/alpha/PIPELINE-STATUS.md`),
329
+ );
330
+ check(
331
+ `drift Β· the status-gates hook still runs (rc=${gates.exitCode})`,
332
+ gates.exitCode === 0,
333
+ );
334
+ }
335
+
336
+ // ═══════════════════════════════════════════════════════════════════════════
337
+ // IV. THE CARRIER β€” a root that cannot be read DENIES, it does not default
338
+ // ═══════════════════════════════════════════════════════════════════════════
339
+ // πŸ”΄ THIS IS THE BLOCK THE EXTRACTION EXISTS FOR. Measured 2026-09-12: when the provider's
340
+ // command fails, `e.ctx.pkg` arrives as the EMPTY STRING and the runtime says nothing. So a
341
+ // `catch { use the default }` silently re-roots the gate at `papers/`, nothing under the real
342
+ // root matches, and every Bash write to a paper is allowed by a gate that still looks
343
+ // installed. An empty prefix must mean REFUSE, never "pass everything".
344
+ {
345
+ const harmless = onBash("echo hi");
346
+ // πŸ”΄ THE TWO PATHS ARE CHOSEN SO THE DECLARATION AND THE DEFAULT DISAGREE. A first draft of
347
+ // this block declared `docs/papers` and tested a write under it β€” which proves nothing,
348
+ // because the guard's prefix test is DEPTH-AGNOSTIC on purpose (`t.includes("/" + p + "/")`,
349
+ // so a relative path deeper in the tree still matches). `docs/papers/…` is therefore a hit
350
+ // under the default `papers` as well, and the "declared root is honoured" assertion passed
351
+ // for a gate that had never read the declaration. Caught by its own negative control,
352
+ // 2026-09-12. A declared root only demonstrates anything when the default would MISS it.
353
+ const DECLARED = "writing/drafts";
354
+ const declaredWrite = onBash(`echo x ${GT} ${DECLARED}/alpha/paper.md`);
355
+ const defaultWrite = onBash(`echo x ${GT} papers/alpha/paper.md`);
356
+
357
+ // (a) a declared root is used AS DECLARED β€” and the default is NOT.
358
+ {
359
+ const dir = fixture(
360
+ { paperlint: { [PAPERS_DIR_FIELD]: DECLARED } },
361
+ { papers: DECLARED },
362
+ );
363
+ check(
364
+ "carrier: a DECLARED root is honoured",
365
+ at(dir, "paper-edit-guard", declaredWrite).exitCode === 2,
366
+ );
367
+ check(
368
+ "carrier: …and the DEFAULT root is not guarded once something else is declared",
369
+ at(dir, "paper-edit-guard", defaultWrite).exitCode === 0,
370
+ );
371
+ check(
372
+ "carrier: and it still allows an unrelated command",
373
+ at(dir, "paper-edit-guard", harmless).exitCode === 0,
374
+ );
375
+ }
376
+ // (aΒ½) the key's OLD name (before 2.0.0) is still read β€” an upgrade must not unguard papers.
377
+ {
378
+ const dir = fixture(
379
+ { "research-paper-pipeline": { [PAPERS_DIR_FIELD]: DECLARED } },
380
+ { papers: DECLARED },
381
+ );
382
+ check(
383
+ "carrier: πŸ”΄ a root declared under the OLD key is still guarded",
384
+ at(dir, "paper-edit-guard", declaredWrite).exitCode === 2 &&
385
+ at(dir, "paper-edit-guard", defaultWrite).exitCode === 0,
386
+ );
387
+ }
388
+ {
389
+ const dir = fixture(
390
+ {
391
+ paperlint: { [PAPERS_DIR_FIELD]: DECLARED },
392
+ "research-paper-pipeline": { [PAPERS_DIR_FIELD]: "papers" },
393
+ },
394
+ { papers: DECLARED },
395
+ );
396
+ const r = at(dir, "paper-edit-guard", harmless);
397
+ check(
398
+ "carrier: both keys with different contents β€” the gate refuses and names both",
399
+ r.exitCode === 2 &&
400
+ /has both "paperlint" and "research-paper-pipeline"/.test(
401
+ r.stderr + r.stdout,
402
+ ),
403
+ );
404
+ }
405
+ // (b) no key at all β†’ the documented default, and the converse of (a).
406
+ {
407
+ const dir = fixture(null, { papers: "papers" });
408
+ check(
409
+ "carrier: no declaration β†’ the default root guards `papers/`",
410
+ at(dir, "paper-edit-guard", defaultWrite).exitCode === 2,
411
+ );
412
+ check(
413
+ "carrier: …and does NOT guard a root nobody declared",
414
+ at(dir, "paper-edit-guard", declaredWrite).exitCode === 0,
415
+ );
416
+ }
417
+ // (c) an explicit `null` is a KEYSTROKE, not an absence. `??` would read it as "undeclared"
418
+ // and silently substitute the default.
419
+ {
420
+ const dir = fixture({
421
+ paperlint: { [PAPERS_DIR_FIELD]: null },
422
+ });
423
+ const r = at(dir, "paper-edit-guard", harmless);
424
+ check(
425
+ `carrier: "${PAPERS_DIR_FIELD}": null REFUSES (rc=${r.exitCode})`,
426
+ r.exitCode === 2,
427
+ );
428
+ check(
429
+ "carrier: …and the refusal names the offending value",
430
+ /got null/.test(r.stderr),
431
+ );
432
+ }
433
+ // (d) the empty string β€” the value that makes every prefix test vacuously true.
434
+ {
435
+ const dir = fixture({
436
+ paperlint: { [PAPERS_DIR_FIELD]: "" },
437
+ });
438
+ const r = at(dir, "paper-edit-guard", harmless);
439
+ check(
440
+ `carrier: "${PAPERS_DIR_FIELD}": "" REFUSES (rc=${r.exitCode})`,
441
+ r.exitCode === 2,
442
+ );
443
+ check(
444
+ "carrier: …and explains that an empty prefix matches nothing",
445
+ /matches nothing/.test(r.stderr),
446
+ );
447
+ }
448
+ // (d2) the field's OLD name is refused and named β€” never read as a fallback, never ignored
449
+ // in favour of the default directory.
450
+ {
451
+ const dir = fixture({
452
+ paperlint: { [OLD_PAPERS_DIR_FIELD]: "writing/drafts" },
453
+ });
454
+ const r = at(dir, "paper-edit-guard", harmless);
455
+ check(
456
+ `carrier: the old field name "${OLD_PAPERS_DIR_FIELD}" REFUSES (rc=${r.exitCode})`,
457
+ r.exitCode === 2,
458
+ );
459
+ check(
460
+ "carrier: …and the refusal says what it was renamed to",
461
+ r.stderr.includes(
462
+ `"${OLD_PAPERS_DIR_FIELD}" was renamed to "${PAPERS_DIR_FIELD}" in package.json β†’ "paperlint"`,
463
+ ),
464
+ );
465
+ }
466
+ // (e) an unreadable package.json β€” the case that arrives by itself, mid-merge.
467
+ {
468
+ const dir = fixture(
469
+ { paperlint: { [PAPERS_DIR_FIELD]: DECLARED } },
470
+ { papers: DECLARED },
471
+ );
472
+ writeFileSync(join(dir, "package.json"), '{ "name": "c" <<<<<<< HEAD\n');
473
+ const r = at(dir, "paper-edit-guard", harmless);
474
+ check(
475
+ `carrier: an unparseable package.json REFUSES (rc=${r.exitCode})`,
476
+ r.exitCode === 2,
477
+ );
478
+ check(
479
+ "carrier: …and the refusal carries the way OUT (a file write, not a command)",
480
+ /Edit or Write/.test(r.stderr) &&
481
+ /do not pass through this gate/.test(r.stderr),
482
+ );
483
+ }
484
+ // (f) a trailing slash the consumer typed β€” `prefix + "/"` would become `//`, matching
485
+ // nothing. This defect made an earlier version of the guard a silent no-op.
486
+ {
487
+ const dir = fixture(
488
+ { paperlint: { [PAPERS_DIR_FIELD]: DECLARED + "/" } },
489
+ { papers: DECLARED },
490
+ );
491
+ check(
492
+ "carrier: a TRAILING SLASH in the declaration still guards",
493
+ at(dir, "paper-edit-guard", declaredWrite).exitCode === 2,
494
+ );
495
+ }
496
+ }
497
+
498
+ // ═══════════════════════════════════════════════════════════════════════════
499
+ // V. paper-skills-nudge β€” it must LAND, not merely fire
500
+ // ═══════════════════════════════════════════════════════════════════════════
501
+ {
502
+ const dir = fixture({
503
+ paperlint: { [PAPERS_DIR_FIELD]: "docs/papers" },
504
+ });
505
+ const lands = (label, p) => {
506
+ const r = at(dir, "paper-skills-nudge", onEdit(p));
507
+ const ctx = injected(r);
508
+ check(
509
+ `nudge LANDS on ${label} (${r.stdout.length}b stdout)`,
510
+ ctx.length > 0,
511
+ );
512
+ check(
513
+ `nudge's text on ${label} is the checklist`,
514
+ /Pre-submit checklist/.test(ctx),
515
+ );
516
+ check(`nudge on ${label} exits 0`, r.exitCode === 0);
517
+ };
518
+ const silent = (label, p) => {
519
+ const r = at(dir, "paper-skills-nudge", onEdit(p));
520
+ check(
521
+ `nudge SILENT on ${label} (${r.stdout.length}b stdout, ${r.stderr.length}b stderr)`,
522
+ r.stdout.length === 0 && r.stderr.length === 0 && r.exitCode === 0,
523
+ );
524
+ };
525
+ lands("a relative paper.md", "docs/papers/alpha/paper.md");
526
+ lands("a .tex", "docs/papers/alpha/paper.tex");
527
+ // πŸ”΄ THE ABSOLUTE SPELLING. The live harness sends absolute paths; a helper that only builds
528
+ // relative ones let an anchored predecessor pass every test while being dead in production.
529
+ lands("an ABSOLUTE paper.md", join(dir, "docs/papers/alpha/paper.md"));
530
+ silent("a README under the papers root", "docs/papers/README.md");
531
+ silent("a file outside the papers root", "README.md");
532
+ // The advisory's own asymmetry: it goes quiet where the gate refuses.
533
+ {
534
+ const broken = fixture(
535
+ { paperlint: { [PAPERS_DIR_FIELD]: null } },
536
+ { papers: "papers" },
537
+ );
538
+ const r = at(
539
+ broken,
540
+ "paper-skills-nudge",
541
+ onEdit("docs/papers/alpha/paper.md"),
542
+ );
543
+ check(
544
+ "nudge is SILENT (not blocking) on an unusable declaration",
545
+ r.exitCode === 0 && r.stdout.length === 0,
546
+ );
547
+ // πŸ”΄ AND SILENT ON THE DEFAULT ROOT TOO β€” this is the half that catches `??`. With
548
+ // `declared ?? DEFAULT` an explicit `null` is read as Β«nothing was declaredΒ», the prefix
549
+ // becomes `papers/`, and the nudge starts firing about a tree the consumer never named.
550
+ // Without this case that mutation survives, because the assertion above passes either way.
551
+ const d = at(
552
+ broken,
553
+ "paper-skills-nudge",
554
+ onEdit("papers/alpha/paper.md"),
555
+ );
556
+ check(
557
+ "nudge: an explicit null is NOT read as an absence (no fallback to the default root)",
558
+ d.stdout.length === 0,
559
+ );
560
+ }
561
+ }
562
+
563
+ // ═══════════════════════════════════════════════════════════════════════════
564
+ // VI. paper-status-gates β€” the tool runs, and only for a validated directory
565
+ // ═══════════════════════════════════════════════════════════════════════════
566
+ {
567
+ const dir = fixture({
568
+ paperlint: { [PAPERS_DIR_FIELD]: "docs/papers" },
569
+ });
570
+ const fires = (label, p) => {
571
+ const r = at(dir, "paper-status-gates", onEdit(p));
572
+ check(
573
+ `gates FIRES on ${label} (${r.stderr.length}b stderr)`,
574
+ /fixture verdict line/.test(r.stderr),
575
+ );
576
+ check(`gates on ${label} exits 0`, r.exitCode === 0);
577
+ };
578
+ const silent = (label, p) => {
579
+ const r = at(dir, "paper-status-gates", onEdit(p));
580
+ check(
581
+ `gates SILENT on ${label}`,
582
+ r.stdout.length === 0 && r.stderr.length === 0 && r.exitCode === 0,
583
+ );
584
+ };
585
+ fires("a relative paper.md", "docs/papers/alpha/paper.md");
586
+ fires("an ABSOLUTE paper.md", join(dir, "docs/papers/alpha/paper.md"));
587
+ silent("a README under the papers root", "docs/papers/README.md");
588
+ silent("a file outside the papers root", "README.md");
589
+ // πŸ”΄ THE BOUNDARY IN `(?:^|/)`: a directory merely ENDING in the root's last segment must
590
+ // not smuggle a match. Without the boundary this is a hit.
591
+ silent(
592
+ "a look-alike root (`notdocs/papers/…`)",
593
+ "notdocs/papers/alpha/paper.md",
594
+ );
595
+ // A `.tex` whose directory name carries a shell metacharacter cannot reach a command.
596
+ silent(
597
+ "a directory name with a shell metacharacter",
598
+ "docs/papers/al;pha/paper.tex",
599
+ );
600
+ // πŸ”΄ The `??` discriminator again, for this hook's own copy of the carrier.
601
+ {
602
+ const broken = fixture(
603
+ { paperlint: { [PAPERS_DIR_FIELD]: null } },
604
+ { papers: "papers" },
605
+ );
606
+ check(
607
+ "gates: an explicit null is NOT read as an absence (no fallback to the default root)",
608
+ at(broken, "paper-status-gates", onEdit("papers/alpha/paper.md")).stderr
609
+ .length === 0,
610
+ );
611
+ }
612
+ // The declared root is regex-ESCAPED: `.` in a root must not act as a wildcard.
613
+ {
614
+ const dotted = fixture(
615
+ { paperlint: { [PAPERS_DIR_FIELD]: "docs.v2/papers" } },
616
+ { papers: "docs.v2/papers" },
617
+ );
618
+ check(
619
+ "gates: a root with a dot matches ITSELF",
620
+ /fixture verdict line/.test(
621
+ at(
622
+ dotted,
623
+ "paper-status-gates",
624
+ onEdit("docs.v2/papers/alpha/paper.md"),
625
+ ).stderr,
626
+ ),
627
+ );
628
+ check(
629
+ "gates: …and the dot is NOT a wildcard",
630
+ at(
631
+ dotted,
632
+ "paper-status-gates",
633
+ onEdit("docsXv2/papers/alpha/paper.md"),
634
+ ).stderr.length === 0,
635
+ );
636
+ }
637
+ }
638
+
639
+ // ═══════════════════════════════════════════════════════════════════════════
640
+ // VII. THE FORCED DUPLICATION β€” moved out, and why
641
+ // ═══════════════════════════════════════════════════════════════════════════
642
+ // The three hooks spell out CONFIG_KEY and DEFAULT_PAPERS_ROOT because a compiled hook may
643
+ // import only `vigiles/hook`. That duplication is checked in `lib/paper-config.harness.mjs`,
644
+ // next to the module the values now come from.
645
+ //
646
+ // πŸ”΄ IT MOVED BECAUSE ITS SCOPE WAS WRONG, NOT ITS CODE. Living here, it could only ever
647
+ // compare hooks with hooks. Measured 2026-09-18: two more files declared the same constants β€”
648
+ // `eslint-rules/papers.mjs` and `skills/paper-pipeline/scripts/consumer.mjs`, neither of them
649
+ // a hook, neither of them under any import restriction, and neither of them inside the
650
+ // comparison. They could have drifted in silence. A check named after one part of a set
651
+ // cannot notice the rest of the set.
652
+
653
+ // ═══════════════════════════════════════════════════════════════════════════
654
+ // VIII. paper-status-gates.sh β€” a TOOL, and it must refuse to be a hook
655
+ // ═══════════════════════════════════════════════════════════════════════════
656
+ {
657
+ const dir = fixture({
658
+ paperlint: { [PAPERS_DIR_FIELD]: "docs/papers" },
659
+ });
660
+ const sh = (args) =>
661
+ runHook(
662
+ `bash ${JSON.stringify(join(HOOKS, "paper-status-gates.sh"))} ${args}`,
663
+ STOP,
664
+ {
665
+ cwd: dir,
666
+ env: { CLAUDE_PROJECT_DIR: dir },
667
+ },
668
+ );
669
+ const usage = sh("");
670
+ check(
671
+ "the .sh called with no --surface says it is a tool",
672
+ /a tool, not a hook/.test(usage.stderr),
673
+ );
674
+ check("…and still exits 0 (it must never block)", usage.exitCode === 0);
675
+ const good = sh("--surface alpha");
676
+ check(
677
+ "the .sh surfaces the verdict for a real paper",
678
+ /fixture verdict line/.test(good.stderr),
679
+ );
680
+ check(
681
+ "…and reads the papers root from the DECLARATION",
682
+ good.exitCode === 0,
683
+ );
684
+ const missing = sh("--surface nosuchpaper");
685
+ check(
686
+ "the .sh is silent for a directory with no status file",
687
+ missing.stderr.length === 0,
688
+ );
689
+ // πŸ”΄ THE NASTY NAME NEEDS A REAL DIRECTORY BEHIND IT, or the assertion cannot fail. First
690
+ // draft just passed `a;b` and checked for silence β€” but a name that names nothing is silent
691
+ // anyway, for want of a status file, so the mutation that DELETES the name check survived
692
+ // green. The fixture now contains a paper literally called `a;b` with a status file in it:
693
+ // with the check, silence; without it, the verdict comes out. Measured 2026-09-12.
694
+ mkdirSync(join(dir, "docs/papers/a;b"), { recursive: true });
695
+ writeFileSync(
696
+ join(dir, "docs/papers/a;b/PIPELINE-STATUS.md"),
697
+ "**Readiness verdict:** βœ… fixture verdict line\n",
698
+ );
699
+ check(
700
+ "the .sh's positive control: a plain name with a status file DOES surface",
701
+ /fixture verdict line/.test(sh("--surface alpha").stderr),
702
+ );
703
+ const nasty = sh("--surface 'a;b'");
704
+ check(
705
+ "the .sh re-rejects a name with a metacharacter EVEN THOUGH that paper exists",
706
+ nasty.stderr.length === 0 && nasty.exitCode === 0,
707
+ );
708
+ }
709
+
710
+ console.log(`hooks.harness: ${n} assertions passed`);
711
+ } finally {
712
+ for (const d of tmps) rmSync(d, { recursive: true, force: true });
713
+ }