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
package/src/init.ts ADDED
@@ -0,0 +1,900 @@
1
+ /**
2
+ * `paperlint init` — the whole install, in the terminal it was typed in.
3
+ *
4
+ * 🔴 WHAT THIS COMMAND USED TO DO, AND WHY THAT WAS A DEFECT RATHER THAN A SHORTFALL. It wrote
5
+ * `rpp.json` with a GUESSED `"papers": "papers"` and never touched `package.json`. The three hooks
6
+ * read the papers directory out of `package.json` and nothing else, so a consumer who followed the
7
+ * documented install got a `paper-edit-guard` watching a directory that did not exist — and a guard
8
+ * watching nothing is byte-identical, from outside, to a guard that is working (issue #33).
9
+ *
10
+ * The yardstick is how many actions happen between "I want this" and "it works": two, `npm i` and
11
+ * this command (`docs/install.md`). Nothing is left to edit by hand.
12
+ *
13
+ * ── THE DECISIONS, AND HOW EACH ONE IS MADE ─────────────────────────────────
14
+ * papers directory MEASURED — `detectPapers` walks the repo for a directory whose CHILDREN
15
+ * carry a paper marker. Several hits is the only case a human is asked about.
16
+ * declaration WRITTEN into `package.json`, merged, never overwriting a value that is
17
+ * already there. Prior art: husky's `init` edits the consumer's package.json
18
+ * to add `prepare`. `rpp.json` is no longer created at all.
19
+ * skills LINKED — one relative symlink per shipped skill into `.claude/skills/`, the
20
+ * only place Claude Code looks for project skills (`link-skills.ts`). An
21
+ * entry of the same name that paperlint did not make is reported, never replaced.
22
+ * hooks WRITTEN into `.claude/settings.json` by vigiles' merge (`hooks-settings.ts`);
23
+ * asked [Y/n] of a human, YES without one — the guard is what the package is
24
+ * for, and the edit is idempotent and visible in `git diff`. `--no-hooks` skips.
25
+ * CI workflow ASKED, because writing a file into `.github/` is not guessable and not
26
+ * cheap to undo. Prior art: Playwright's initializer asks exactly this.
27
+ * Without a human: NO.
28
+ * first paper OFFERED only to a human and only when the papers directory holds none;
29
+ * without a human only `--paper <name>` creates one (`new-paper.ts`).
30
+ * external toolchain REPORTED, never installed. npm's own rule, quoted in husky's write-up:
31
+ * "The only valid use of install or preinstall scripts is for compilation."
32
+ *
33
+ * 🔴 NOTHING IS ASKED WITHOUT A HUMAN — stdin AND stdout a terminal, `CI` unset, no `--yes`
34
+ * (`interactivity`). A question in CI is not a question, it is a hang — or, with a closed stdin,
35
+ * an answer nobody gave. So the non-interactive path takes the stated default and SAYS which
36
+ * default it took and why nothing was asked, rather than pretending it asked.
37
+ */
38
+ // eslint-disable-next-line boundaries/dependencies -- legacy I/O, moves behind a port in #76
39
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
40
+ // eslint-disable-next-line boundaries/dependencies -- legacy I/O, moves behind a port in #76
41
+ import { spawnSync } from "node:child_process";
42
+ import { dirname, join, relative, resolve } from "node:path";
43
+ import { doctor, detectPapers, found, PROGRAMS } from "./doctor.ts";
44
+ import { PAPER_MARKERS, papersIn } from "./build.ts";
45
+ import { linkSkills, SKILLS_HOME, type LinkReport } from "./link-skills.ts";
46
+ import { actionRef } from "./action-ref.ts";
47
+ import { LEGACY_PACKAGE_NAME } from "../skills/paper-pipeline/scripts/consumer.mjs";
48
+ import {
49
+ FRESH_CLONE_NOTE,
50
+ SETTINGS_PATH,
51
+ UNINSTALL_PLUGIN,
52
+ shippedWiring,
53
+ wireHooks,
54
+ type Merge,
55
+ type WireResult,
56
+ } from "./hooks-settings.ts";
57
+ import {
58
+ DEFAULT_FORMAT,
59
+ isFormat,
60
+ nameProblem,
61
+ type PaperFormat,
62
+ } from "./new-paper.ts";
63
+ // The one source for the consumer's config key lives in the .mjs half of the package (the ESLint
64
+ // rules and the skill scripts import it too); its types are in lib/paper-config.d.mts.
65
+ import {
66
+ CONFIG_KEY,
67
+ DEFAULT_PAPERS_ROOT,
68
+ LEGACY_CONFIG_KEY,
69
+ PAPERS_DIR_FIELD,
70
+ declaredSettings,
71
+ renamedFieldMessage,
72
+ } from "../lib/paper-config.mjs";
73
+
74
+ /** How the papers directory was arrived at. Printed, because a guess must not read as a fact. */
75
+ export type PapersHow =
76
+ "detected" | "chosen" | "not-asked" | "no-answer" | "guessed";
77
+
78
+ /**
79
+ * 🔴 A QUESTION CAN FAIL, AND ITS FAILURE MUST NOT BE THE COMMAND'S. Measured 2026-09-18 on a
80
+ * real pseudo-terminal: `readline`'s `question()` REJECTS with `AbortError: Aborted with Ctrl+D`
81
+ * when the answer stream ends, and the rejection escaped `init` as a stack trace — after the
82
+ * declaration had already been written. So the install both succeeded and looked like a crash.
83
+ * An unanswered question is an answer: take the default and say so.
84
+ */
85
+ async function askOrDefault(
86
+ ask: (q: string) => Promise<string>,
87
+ question: string,
88
+ ): Promise<string | null> {
89
+ try {
90
+ return await ask(question);
91
+ } catch {
92
+ return null;
93
+ }
94
+ }
95
+
96
+ /**
97
+ * WHETHER A HUMAN IS AT THE OTHER END — one decision, made once, with its reason kept, because
98
+ * every default `init` takes is printed together with WHY nothing was asked.
99
+ *
100
+ * clig.dev: "Only use prompts or interactive elements if stdin is an interactive terminal".
101
+ * stdin alone is not enough: an agent that pipes the output has a TTY-less STDOUT and would
102
+ * never see the question it is being asked. `CI` covers runners that allocate a pseudo-terminal,
103
+ * and `--yes` is npm's and create-next-app's way to say "take the defaults" from a terminal.
104
+ */
105
+ export function interactivity({
106
+ stdinTTY,
107
+ stdoutTTY,
108
+ env,
109
+ yes,
110
+ }: {
111
+ stdinTTY: boolean;
112
+ stdoutTTY: boolean;
113
+ env: Readonly<Record<string, string | undefined>>;
114
+ yes: boolean;
115
+ }): { readonly interactive: boolean; readonly why: string } {
116
+ if (yes) return { interactive: false, why: "--yes was given" };
117
+ if (env["CI"]) return { interactive: false, why: "CI is set" };
118
+ if (!stdinTTY) return { interactive: false, why: "stdin is not a terminal" };
119
+ if (!stdoutTTY)
120
+ return { interactive: false, why: "stdout is not a terminal" };
121
+ return { interactive: true, why: "a terminal on both ends" };
122
+ }
123
+
124
+ /** The mode of THIS process — the one place `process` is read for it. */
125
+ export const processInteractivity = (yes: boolean) =>
126
+ interactivity({
127
+ // eslint-disable-next-line no-restricted-globals -- legacy I/O, moves behind a port in #76
128
+ stdinTTY: Boolean(process.stdin.isTTY),
129
+ // eslint-disable-next-line no-restricted-globals -- legacy I/O, moves behind a port in #76
130
+ stdoutTTY: Boolean(process.stdout.isTTY),
131
+ // eslint-disable-next-line no-restricted-globals -- legacy I/O, moves behind a port in #76
132
+ env: process.env,
133
+ yes,
134
+ });
135
+
136
+ export interface PapersChoice {
137
+ readonly papers: string;
138
+ readonly how: PapersHow;
139
+ readonly candidates: readonly string[];
140
+ }
141
+
142
+ /**
143
+ * One hit is used, several are asked about, none falls back to the documented default — and the
144
+ * fallback is labelled a guess in the same breath, because the whole class of defect this command
145
+ * exists to close is a guess that later reads as a measurement.
146
+ */
147
+ export async function choosePapers(
148
+ root: string,
149
+ {
150
+ ask,
151
+ interactive,
152
+ }: { ask?: (q: string) => Promise<string>; interactive: boolean },
153
+ ): Promise<PapersChoice> {
154
+ const candidates = detectPapers(root);
155
+ const first = candidates[0];
156
+ if (first === undefined)
157
+ return { papers: DEFAULT_PAPERS_ROOT, how: "guessed", candidates };
158
+ if (candidates.length === 1)
159
+ return { papers: first, how: "detected", candidates };
160
+ if (!interactive || !ask)
161
+ return { papers: first, how: "not-asked", candidates };
162
+
163
+ const menu = candidates
164
+ .map((c, i) => ` ${String(i + 1)}) ${c}`)
165
+ .join("\n");
166
+ const answer = await askOrDefault(
167
+ ask,
168
+ ` several directories look like papers roots:\n${menu}\n which one? [1] `,
169
+ );
170
+ const picked = candidates[Number((answer ?? "").trim()) - 1];
171
+ return picked === undefined
172
+ ? { papers: first, how: "no-answer", candidates }
173
+ : { papers: picked, how: "chosen", candidates };
174
+ }
175
+
176
+ export type DeclarationResult =
177
+ | {
178
+ readonly status: "written";
179
+ readonly path: string;
180
+ readonly papers: string;
181
+ /** The settings were under the old key and were moved to the new one. */
182
+ readonly migrated: boolean;
183
+ }
184
+ | {
185
+ readonly status: "kept";
186
+ readonly path: string;
187
+ readonly papers: unknown;
188
+ readonly migrated: boolean;
189
+ }
190
+ | {
191
+ readonly status: "unparsable";
192
+ readonly path: string;
193
+ readonly reason: string;
194
+ }
195
+ | { readonly status: "absent"; readonly path: string }
196
+ | {
197
+ readonly status: "renamed";
198
+ readonly path: string;
199
+ readonly message: string;
200
+ };
201
+
202
+ /**
203
+ * Writes ONE declaration, into the file every channel can already name.
204
+ *
205
+ * 🔴 A HOOK CANNOT IMPORT CODE AND CANNOT WALK UP A TREE LOOKING FOR A CONFIG. It can read a path
206
+ * it is able to spell, and the only path it can always spell is the project's own `package.json`.
207
+ * That asymmetry is the whole reason the declaration moved here rather than the readers moving to
208
+ * `rpp.json`: many readers against one (`docs/install.md`, "One declaration").
209
+ *
210
+ * ⚠️ Merged, not rewritten, and never over a value the consumer set — an `init` that silently
211
+ * replaces a setting is worse than an `init` that does nothing, because the consumer keeps
212
+ * believing the old value.
213
+ */
214
+ export function declarePapers(root: string, papers: string): DeclarationResult {
215
+ const path = join(root, "package.json");
216
+ if (!existsSync(path)) return { status: "absent", path };
217
+ const raw = readFileSync(path, "utf8");
218
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- #49: replace with a real type
219
+ let pkg: Record<string, any>;
220
+ try {
221
+ pkg = JSON.parse(raw);
222
+ } catch (e) {
223
+ return { status: "unparsable", path, reason: (e as Error).message };
224
+ }
225
+ const found = declaredSettings(pkg);
226
+ if (found.conflict !== null)
227
+ return { status: "renamed", path, message: found.conflict };
228
+ const message = renamedFieldMessage(found.settings);
229
+ if (message) return { status: "renamed", path, message };
230
+ // Settings under the old key move to the new one, in the same position in the file.
231
+ const migrated = found.legacy;
232
+ if (migrated) pkg = renameKey(pkg, LEGACY_CONFIG_KEY, CONFIG_KEY);
233
+ const write = (): void =>
234
+ // Two-space indent and the file's own trailing newline: a declaration is not a licence to
235
+ // reformat somebody else's file, and a one-line diff is a diff a consumer will actually read.
236
+ writeFileSync(
237
+ path,
238
+ JSON.stringify(pkg, null, 2) + (raw.endsWith("\n") ? "\n" : ""),
239
+ "utf8",
240
+ );
241
+ const existing = pkg?.[CONFIG_KEY]?.[PAPERS_DIR_FIELD];
242
+ if (existing !== undefined) {
243
+ if (migrated) write();
244
+ return { status: "kept", path, papers: existing, migrated };
245
+ }
246
+ pkg[CONFIG_KEY] = { ...(pkg[CONFIG_KEY] ?? {}), [PAPERS_DIR_FIELD]: papers };
247
+ write();
248
+ return { status: "written", path, papers, migrated };
249
+ }
250
+
251
+ /** `obj` with `from` renamed to `to`, keeping the key's position. */
252
+ function renameKey(
253
+ obj: Record<string, unknown>,
254
+ from: string,
255
+ to: string,
256
+ ): Record<string, unknown> {
257
+ return Object.fromEntries(
258
+ Object.entries(obj).map(([k, v]) => [k === from ? to : k, v]),
259
+ );
260
+ }
261
+
262
+ export type RppJsonResult = "absent" | "kept" | "filled" | "unparsable";
263
+
264
+ /**
265
+ * `rpp.json` is no longer CREATED — but a consumer who already has one keeps it working, and it
266
+ * gets the same `papers` value rather than being left to disagree with `package.json` in silence.
267
+ * Two declarations that disagree is the defect `paperlint doctor` was written to catch; writing the
268
+ * second one on purpose would be handing it new work.
269
+ */
270
+ export function syncRppJson(root: string, papers: string): RppJsonResult {
271
+ const path = join(root, "rpp.json");
272
+ if (!existsSync(path)) return "absent";
273
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- #49: replace with a real type
274
+ let cfg: Record<string, any>;
275
+ const raw = readFileSync(path, "utf8");
276
+ try {
277
+ cfg = JSON.parse(raw);
278
+ } catch {
279
+ return "unparsable";
280
+ }
281
+ if (cfg?.[PAPERS_DIR_FIELD] !== undefined) return "kept";
282
+ cfg[PAPERS_DIR_FIELD] = papers;
283
+ writeFileSync(
284
+ path,
285
+ JSON.stringify(cfg, null, 2) + (raw.endsWith("\n") ? "\n" : ""),
286
+ "utf8",
287
+ );
288
+ return "filled";
289
+ }
290
+
291
+ export const WORKFLOW_PATH = join(".github", "workflows", "papers.yml");
292
+
293
+ /** Where the action is pinned when no release tag is known — obviously a placeholder. */
294
+ export const UNPINNED_REF = "<commit-sha>";
295
+
296
+ /**
297
+ * The CI step, as a whole workflow, pinned to `ref` — the release tag of the running package
298
+ * (`actionRef`). With no tag known (`null`: a git checkout, `npm link`) it keeps the placeholder
299
+ * and says so: a wrong tag written confidently is worse than a placeholder that is obviously one.
300
+ */
301
+ export function workflowYaml(papers: string, ref: string | null): string {
302
+ return [
303
+ ref === null
304
+ ? `# Written by \`paperlint init\`. Replace ${UNPINNED_REF} with a commit or release tag of the action.`
305
+ : `# Written by \`paperlint init\`, pinned to ${ref} — the release you installed.`,
306
+ `name: papers`,
307
+ `on: [push, pull_request]`,
308
+ `jobs:`,
309
+ ` papers:`,
310
+ ` runs-on: ubuntu-latest`,
311
+ ` steps:`,
312
+ ` - uses: actions/checkout@v4`,
313
+ ` - uses: zernie/research-paper-pipeline@${ref ?? UNPINNED_REF}`,
314
+ ` with:`,
315
+ ` paths: ${papers}`,
316
+ ``,
317
+ ].join("\n");
318
+ }
319
+
320
+ export type WorkflowResult = "written" | "kept" | "declined" | "not-asked";
321
+
322
+ export async function offerWorkflow(
323
+ root: string,
324
+ papers: string,
325
+ {
326
+ ask,
327
+ interactive,
328
+ version,
329
+ }: {
330
+ ask?: (q: string) => Promise<string>;
331
+ interactive: boolean;
332
+ /** The running package's version; see `InitOptions.version`. */
333
+ version?: string;
334
+ },
335
+ ): Promise<WorkflowResult> {
336
+ const path = join(root, WORKFLOW_PATH);
337
+ if (existsSync(path)) return "kept";
338
+ if (!interactive || !ask) return "not-asked";
339
+ const answer = (
340
+ await askOrDefault(
341
+ ask,
342
+ ` add a GitHub Actions workflow that runs this in CI? [y/N] `,
343
+ )
344
+ )
345
+ ?.trim()
346
+ .toLowerCase();
347
+ // No answer — an empty line, or a stream that ended — is the safe default, which is "no file".
348
+ if (answer !== "y" && answer !== "yes") return "declined";
349
+ mkdirSync(dirname(path), { recursive: true });
350
+ writeFileSync(path, workflowYaml(papers, actionRef(version)), "utf8");
351
+ return "written";
352
+ }
353
+
354
+ /** What `init` says about the CI workflow — and, when none was written, the step to paste. */
355
+ export function reportWorkflow(
356
+ wf: WorkflowResult,
357
+ {
358
+ version,
359
+ papersDir,
360
+ why,
361
+ }: { version?: string; papersDir: string; why: string },
362
+ ): string[] {
363
+ const ref = actionRef(version);
364
+ const out: string[] = [];
365
+ if (wf === "written")
366
+ out.push(
367
+ ref === null
368
+ ? ` ✓ wrote ${WORKFLOW_PATH} — pin ${UNPINNED_REF} before pushing it`
369
+ : ` ✓ wrote ${WORKFLOW_PATH}, pinned to ${ref}`,
370
+ );
371
+ else if (wf === "kept")
372
+ out.push(
373
+ ` ✓ ${WORKFLOW_PATH} is already there — kept, nothing overwritten`,
374
+ );
375
+ else if (wf === "declined") out.push(` · declined — nothing written`);
376
+ else
377
+ out.push(
378
+ ` · ${why}, so nothing was asked. Default taken: NO file written.`,
379
+ );
380
+ if (wf !== "written" && wf !== "kept")
381
+ out.push(
382
+ ` to run the same checks in CI, add this step to a workflow:`,
383
+ ` - uses: zernie/research-paper-pipeline@${ref ?? UNPINNED_REF}`,
384
+ ` with:`,
385
+ ` paths: ${papersDir}`,
386
+ );
387
+ return out;
388
+ }
389
+
390
+ /**
391
+ * The external toolchain is REPORTED, never fetched. Each entry already carries what goes quiet
392
+ * without it, which is the only reason the list is worth printing: a missing checker and a
393
+ * passing checker produce the same silence.
394
+ */
395
+ export function missingPrograms(
396
+ run = spawnSync,
397
+ ): readonly (typeof PROGRAMS)[number][] {
398
+ return PROGRAMS.filter((p) => !found(p.bin, run));
399
+ }
400
+
401
+ /**
402
+ * What is left after `init` — commands only, all typed in the same terminal.
403
+ *
404
+ * 🔴 THE TWO `/plugin` LINES ARE GONE, AND THAT WAS THE POINT. They were the one step "that cannot
405
+ * be done from a terminal": typed into another program, invisible to `paperlint doctor`, impossible for
406
+ * an agent installing this package, and (as a repository-declared plugin) not installed in a cloud
407
+ * session at all. `init` now writes the same three hook commands into `.claude/settings.json`
408
+ * itself (`hooks-settings.ts`), so there is nothing left to type anywhere but here.
409
+ */
410
+ export function nextSteps(papersDir: string = DEFAULT_PAPERS_ROOT): string {
411
+ return [
412
+ ``,
413
+ `next: npx paperlint new <name> # start a paper in ${papersDir}/ from the template`,
414
+ ` npx paperlint lint # runs every rule over ${papersDir}`,
415
+ ``,
416
+ ].join("\n");
417
+ }
418
+
419
+ export type HooksOutcome =
420
+ | WireResult
421
+ | { readonly status: "skipped" }
422
+ | { readonly status: "declined" }
423
+ | { readonly status: "failed"; readonly reason: string };
424
+
425
+ /**
426
+ * The hooks step: decide, then wire. `hooks: false` is `--no-hooks`. The default without a human
427
+ * is YES — installing a package whose purpose includes an edit guard makes "yes" guessable, and
428
+ * the edit is idempotent and shows up in `git diff` (the rule `docs/install.md` took from
429
+ * Playwright: ask only about what cannot be guessed or is expensive).
430
+ */
431
+ export async function offerHooks(
432
+ root: string,
433
+ {
434
+ hooks,
435
+ interactive,
436
+ ask,
437
+ merge,
438
+ }: {
439
+ hooks: boolean;
440
+ interactive: boolean;
441
+ ask?: (q: string) => Promise<string>;
442
+ merge?: Merge;
443
+ },
444
+ ): Promise<HooksOutcome> {
445
+ if (!hooks) return { status: "skipped" };
446
+ if (interactive && ask) {
447
+ const answer = (
448
+ await askOrDefault(
449
+ ask,
450
+ ` wire the paper hooks into ${SETTINGS_PATH} (committed, shared with every clone)? [Y/n] `,
451
+ )
452
+ )
453
+ ?.trim()
454
+ .toLowerCase();
455
+ // An empty line or a stream that ended is the stated default, which here is YES.
456
+ if (answer === "n" || answer === "no") return { status: "declined" };
457
+ }
458
+ try {
459
+ // Called THROUGH the protocol object rather than as a detached method: the port is an
460
+ // object, and an implementation that one day reads `this` must not break here.
461
+ let m = merge;
462
+ if (!m) {
463
+ // eslint-disable-next-line boundaries/dependencies -- legacy layer, moves behind a port in #76
464
+ const { claudeCodeHookProtocol } = await import("vigiles/claude-code");
465
+ m = (existing, compiled, managedBy) =>
466
+ claudeCodeHookProtocol.mergeRegistrations(
467
+ existing,
468
+ compiled,
469
+ managedBy,
470
+ );
471
+ }
472
+ return wireHooks(root, m, shippedWiring());
473
+ } catch (e) {
474
+ return { status: "failed", reason: (e as Error).message };
475
+ }
476
+ }
477
+
478
+ /** The hooks section of init's report. Every default names the flag that changes it. */
479
+ export function reportHooks(
480
+ outcome: HooksOutcome,
481
+ { how, here }: { how: string; here: (p: string) => string },
482
+ ): string[] {
483
+ const out: string[] = [];
484
+ if (outcome.status === "skipped") {
485
+ out.push(` · --no-hooks — nothing written`);
486
+ return out;
487
+ }
488
+ if (outcome.status === "declined") {
489
+ out.push(
490
+ ` · declined — nothing written. \`npx paperlint init\` again wires them later`,
491
+ );
492
+ return out;
493
+ }
494
+ if (outcome.status === "failed") {
495
+ out.push(` ✗ not wired — ${outcome.reason}`);
496
+ out.push(
497
+ ` the hooks need vigiles to run at all; reinstall this package, then \`npx paperlint init\``,
498
+ );
499
+ return out;
500
+ }
501
+ if (outcome.status === "unparsable") {
502
+ out.push(
503
+ ` ✗ ${here(outcome.path)} does not parse — nothing written: ${outcome.reason}`,
504
+ );
505
+ return out;
506
+ }
507
+ if (outcome.status === "foreign") {
508
+ out.push(
509
+ ` ✓ already wired under another spelling in ${here(outcome.path)} — nothing written, so nothing runs twice:`,
510
+ );
511
+ for (const f of outcome.found) out.push(` ${f.name}: ${f.command}`);
512
+ if (outcome.missing.length > 0)
513
+ out.push(
514
+ ` ⚠ and NOT wired in any form: ${outcome.missing.join(", ")} — add them in that same form`,
515
+ );
516
+ out.push(
517
+ ` to switch to the form init writes, delete those commands and run \`npx paperlint init\` again`,
518
+ );
519
+ } else {
520
+ out.push(
521
+ outcome.status === "written"
522
+ ? ` ✓ wired ${outcome.names.join(", ")} into ${here(outcome.path)}`
523
+ : ` ✓ already wired in ${here(outcome.path)} — nothing changed`,
524
+ );
525
+ out.push(` ${how}`);
526
+ if (outcome.status === "written" && outcome.replaced > 0)
527
+ out.push(
528
+ ` replaced ${String(outcome.replaced)} command(s) that pointed into ${LEGACY_PACKAGE_NAME}, the package's old name`,
529
+ );
530
+ out.push(` ${FRESH_CLONE_NOTE}`);
531
+ }
532
+ if (outcome.plugin.length > 0)
533
+ out.push(
534
+ ` ⚠ this project also enables the plugin (${outcome.plugin.join(", ")}) — with it every hook runs twice.`,
535
+ ` the plugin no longer carries the hooks: ${UNINSTALL_PLUGIN}`,
536
+ ` and remove it from "enabledPlugins" in ${SETTINGS_PATH}`,
537
+ );
538
+ return out;
539
+ }
540
+
541
+ /**
542
+ * The skills section of init's report: what was linked, what already was, and — BY NAME — what
543
+ * was left alone. A skipped entry does not fail init: the name is taken by something the consumer
544
+ * made, replacing it would be worse than not linking, and doctor's report below repeats the gap.
545
+ */
546
+ export function reportSkillLinks(
547
+ report: LinkReport,
548
+ here: (p: string) => string,
549
+ ): string[] {
550
+ const out: string[] = [
551
+ ``,
552
+ `skills (Claude Code finds project skills in ${SKILLS_HOME}/, not in node_modules)`,
553
+ ];
554
+ if (!report.ok) {
555
+ out.push(` ⚠ nothing linked — ${report.error}`);
556
+ out.push(
557
+ ` install the package into this project (\`npm i -D …\`), then \`npx paperlint init\` again`,
558
+ );
559
+ return out;
560
+ }
561
+ const by = (s: string) => report.links.filter((l) => l.status === s);
562
+ const replaced = by("replaced");
563
+ const created = [...by("created"), ...replaced];
564
+ const present = by("present");
565
+ const skipped = by("foreign");
566
+ // Only a read-only call leaves anything `missing`; counted anyway, so the sum always adds up.
567
+ const missing = by("missing");
568
+ const mark = skipped.length || missing.length ? "⚠" : "✓";
569
+ out.push(
570
+ ` ${mark} ${String(report.links.length)} shipped: ${String(created.length)} linked now, ` +
571
+ `${String(present.length)} already linked, ${String(skipped.length)} skipped` +
572
+ (missing.length ? `, ${String(missing.length)} NOT linked` : ``),
573
+ );
574
+ if (report.example !== null)
575
+ out.push(
576
+ ` ${join(here(report.home), "<name>")} → ${join(dirname(report.example), "<name>")}`,
577
+ );
578
+ if (replaced.length)
579
+ out.push(
580
+ ` ${String(replaced.length)} of them replaced a link into ${LEGACY_PACKAGE_NAME}, the package's old name`,
581
+ );
582
+ if (skipped.length) {
583
+ out.push(
584
+ ` left untouched — the name is taken by something paperlint did not make:`,
585
+ );
586
+ for (const l of skipped)
587
+ out.push(` ${l.name} — ${l.reason ?? "occupied"}`);
588
+ out.push(
589
+ ` those skills are NOT available in Claude Code until the entry is moved or removed`,
590
+ );
591
+ }
592
+ return out;
593
+ }
594
+
595
+ export interface InitOptions {
596
+ log?: typeof console.log;
597
+ err?: typeof console.error;
598
+ cwd?: string;
599
+ /** Asks one question. Injected so the prompt is assertable without a pseudo-terminal. */
600
+ ask?: (question: string) => Promise<string>;
601
+ /**
602
+ * Whether a human is at the other end. Defaults to `interactivity()` over this process: stdin
603
+ * AND stdout are terminals, `CI` is unset, and no `--yes`.
604
+ */
605
+ interactive?: boolean;
606
+ /** `--yes`: take every default without asking. */
607
+ yes?: boolean;
608
+ /** `false` is `--no-hooks`. */
609
+ hooks?: boolean;
610
+ /** vigiles' merge. Injected only so a test can observe or replace it. */
611
+ merge?: Merge;
612
+ /** `--paper <name>`: create this paper, even without a terminal. */
613
+ paper?: string | null;
614
+ /** `--format tex|md` for that paper. */
615
+ format?: PaperFormat | null;
616
+ /**
617
+ * Creates one paper and lints it — `paperlint new`'s own routine, passed in by the CLI so `init` and
618
+ * `new` cannot drift into two implementations.
619
+ */
620
+ createPaper?: (
621
+ papersRoot: string,
622
+ name: string,
623
+ format: PaperFormat,
624
+ ) => Promise<number>;
625
+ run?: typeof spawnSync;
626
+ /**
627
+ * What `paperlint lint` would resolve from the declaration, asked of the CLI's OWN reader. A second
628
+ * implementation here would be a second source of truth — the very defect `doctor` reports.
629
+ */
630
+ resolveCliPapers?: (root: string) => string | null;
631
+ /** Links the skills. Injected only so a test can stand in for the installed package. */
632
+ link?: (root: string) => LinkReport;
633
+ /**
634
+ * The version of the running package, read by the CLI from its own `package.json`. The CI
635
+ * workflow is pinned to its release tag (`actionRef`); absent or unreleased, the placeholder.
636
+ */
637
+ version?: string;
638
+ }
639
+
640
+ /** Reads one line from a real terminal. Kept out of `init` so the command stays testable. */
641
+ export async function askOnTerminal(question: string): Promise<string> {
642
+ // eslint-disable-next-line boundaries/dependencies -- legacy I/O, moves behind a port in #76
643
+ const { createInterface } = await import("node:readline/promises");
644
+ // eslint-disable-next-line no-restricted-globals -- legacy I/O, moves behind a port in #76
645
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
646
+ try {
647
+ return await rl.question(question);
648
+ } finally {
649
+ rl.close();
650
+ }
651
+ }
652
+
653
+ // Documented in README.md#install-and-set-up — update it when this changes.
654
+ export async function init(
655
+ dir: string,
656
+ opts: InitOptions = {},
657
+ ): Promise<number> {
658
+ const {
659
+ log = console.log,
660
+ err = console.error,
661
+ // eslint-disable-next-line no-restricted-globals -- legacy I/O, moves behind a port in #76
662
+ cwd = process.cwd(),
663
+ ask = askOnTerminal,
664
+ yes = false,
665
+ hooks = true,
666
+ merge,
667
+ paper = null,
668
+ format = null,
669
+ createPaper,
670
+ run = spawnSync,
671
+ resolveCliPapers,
672
+ link = (r: string) => linkSkills(r),
673
+ version,
674
+ } = opts;
675
+ // An injected `interactive` is a test standing in for a terminal; its reason is the classic one.
676
+ const { interactive, why } =
677
+ opts.interactive === undefined
678
+ ? processInteractivity(yes)
679
+ : {
680
+ interactive: opts.interactive,
681
+ why: opts.interactive
682
+ ? "a terminal on both ends"
683
+ : "stdin is not a terminal",
684
+ };
685
+ const root = resolve(cwd, dir);
686
+ const here = (p: string): string => relative(cwd, p) || p;
687
+
688
+ log(``);
689
+ log(`paperlint init — each decision below says HOW it was decided`);
690
+
691
+ // ── 1. where the papers are ───────────────────────────────────────────────────────────
692
+ const choice = await choosePapers(root, { ask, interactive });
693
+ log(``);
694
+ log(`papers directory`);
695
+ if (choice.how === "detected")
696
+ log(
697
+ ` ✓ ${choice.papers} — measured: its subdirectories carry ${PAPER_MARKERS.join(" / ")}`,
698
+ );
699
+ else if (choice.how === "chosen")
700
+ log(
701
+ ` ✓ ${choice.papers} — you picked it out of ${String(choice.candidates.length)} candidates`,
702
+ );
703
+ else if (choice.how === "not-asked" || choice.how === "no-answer") {
704
+ log(
705
+ ` ✓ ${choice.papers} — ${String(choice.candidates.length)} candidates, ` +
706
+ (choice.how === "not-asked"
707
+ ? `${why} so nothing was asked`
708
+ : `no answer was given, so the first one was taken`),
709
+ );
710
+ log(
711
+ ` the others: ${choice.candidates.slice(1).join(", ")} — change it in package.json if this is the wrong one`,
712
+ );
713
+ } else {
714
+ log(
715
+ ` ⚠ ${choice.papers} — A GUESS. Nothing here looks like a papers directory yet.`,
716
+ );
717
+ log(
718
+ ` Nothing on disk was measured, so this is the documented default and not a finding.`,
719
+ );
720
+ }
721
+
722
+ // ── 2. one declaration, in package.json ───────────────────────────────────────────────
723
+ log(``);
724
+ log(`declaration`);
725
+ const decl = declarePapers(root, choice.papers);
726
+ if (decl.status === "written")
727
+ log(
728
+ ` ✓ ${here(decl.path)} → "${CONFIG_KEY}": { "${PAPERS_DIR_FIELD}": ${JSON.stringify(decl.papers)} }`,
729
+ );
730
+ else if (decl.status === "kept") {
731
+ if (typeof decl.papers !== "string") {
732
+ err(
733
+ ` ✗ ${here(decl.path)} declares ${PAPERS_DIR_FIELD} = ${JSON.stringify(decl.papers)} — it must be a directory path (a string)`,
734
+ );
735
+ return 2;
736
+ }
737
+ log(
738
+ ` ✓ ${here(decl.path)} already declares ${PAPERS_DIR_FIELD} = ${JSON.stringify(decl.papers)} — kept, nothing overwritten`,
739
+ );
740
+ } else if (decl.status === "renamed") {
741
+ err(` ✗ ${decl.message}`);
742
+ err(
743
+ ` nothing was written. Fix it in ${here(decl.path)}, then run init again.`,
744
+ );
745
+ return 2;
746
+ } else if (decl.status === "unparsable") {
747
+ err(` ✗ ${here(decl.path)} is not valid JSON: ${decl.reason}`);
748
+ err(
749
+ ` nothing was written. The hooks read their papers directory from this file and`,
750
+ );
751
+ err(
752
+ ` refuse every Bash command while it cannot be parsed — fix the JSON first.`,
753
+ );
754
+ return 2;
755
+ } else {
756
+ err(
757
+ ` ✗ no package.json at ${here(root)} — there is nowhere to put the declaration.`,
758
+ );
759
+ err(
760
+ ` The hooks can only read a path they are able to name, and that path is`,
761
+ );
762
+ err(
763
+ ` package.json. Run \`npm init -y\` here, then \`npx paperlint init\` again.`,
764
+ );
765
+ return 2;
766
+ }
767
+ if (decl.migrated)
768
+ log(
769
+ ` ✓ moved the settings from "${LEGACY_CONFIG_KEY}" (the old key) to "${CONFIG_KEY}"`,
770
+ );
771
+ log(
772
+ ` one declaration — the hooks, the rules and the CLI all read this one key`,
773
+ );
774
+ // Every step below uses the DECLARED directory. A kept declaration outranks what init
775
+ // measured or guessed: otherwise the first paper, the workflow and rpp.json would land in the
776
+ // guessed directory while lint and the hooks keep reading the declared one.
777
+ const papersDir =
778
+ decl.status === "kept" ? (decl.papers as string) : choice.papers;
779
+ const rpp = syncRppJson(root, papersDir);
780
+ if (rpp === "filled")
781
+ log(
782
+ ` ⚠ rpp.json was already here — gave it the same ${PAPERS_DIR_FIELD} value; it is deprecated`,
783
+ );
784
+ else if (rpp === "kept")
785
+ log(
786
+ ` ⚠ rpp.json was already here and already declares ${PAPERS_DIR_FIELD} — left untouched; it is deprecated`,
787
+ );
788
+ else if (rpp === "unparsable")
789
+ log(
790
+ ` ⚠ rpp.json is here and does not parse — left untouched; it is deprecated, delete it`,
791
+ );
792
+
793
+ // ── 3. the skills, linked where Claude Code looks for them ─────────────────────────────
794
+ for (const line of reportSkillLinks(link(root), here)) log(line);
795
+
796
+ // ── 4. the hooks, wired where Claude Code reads them ───────────────────────────────────
797
+ log(``);
798
+ log(
799
+ `hooks (Claude Code runs them from ${SETTINGS_PATH} — committed, shared with every clone)`,
800
+ );
801
+ const hooked = await offerHooks(root, { hooks, interactive, ask, merge });
802
+ for (const line of reportHooks(hooked, {
803
+ here,
804
+ how: interactive
805
+ ? `you were asked; \`--no-hooks\` skips this next time`
806
+ : `default taken: YES — ${why}, so nothing was asked. \`--no-hooks\` skips this`,
807
+ }))
808
+ log(line);
809
+
810
+ // ── 5. the one expensive, unguessable thing ───────────────────────────────────────────
811
+ log(``);
812
+ log(`CI`);
813
+ const wf = await offerWorkflow(root, papersDir, {
814
+ ask,
815
+ interactive,
816
+ version,
817
+ });
818
+ for (const line of reportWorkflow(wf, { version, papersDir, why })) log(line);
819
+
820
+ // ── 6. a first paper — offered only where there is none, and only to a human ──────────
821
+ log(``);
822
+ log(`first paper`);
823
+ const papersAbs = resolve(root, papersDir);
824
+ const hasPaper = papersIn(papersAbs).length > 0;
825
+ let wanted: string | null = paper;
826
+ // A paper that was asked for and not delivered decides the exit code (below): the doctor's
827
+ // code can be 0, and automation would read an unfulfilled `--paper` as done.
828
+ let paperCode = 0;
829
+ if (wanted === null && !hasPaper && interactive && createPaper) {
830
+ const answer = (
831
+ await askOrDefault(ask, ` create a first paper? name: [skip] `)
832
+ )?.trim();
833
+ wanted = answer ? answer : null;
834
+ }
835
+ if (wanted !== null && createPaper) {
836
+ const problem = nameProblem(wanted);
837
+ let fmt: PaperFormat = format ?? DEFAULT_FORMAT;
838
+ if (!problem && format === null && interactive) {
839
+ const f = (
840
+ await askOrDefault(ask, ` format: tex / md [${DEFAULT_FORMAT}] `)
841
+ )?.trim();
842
+ if (isFormat(f)) fmt = f;
843
+ }
844
+ if (problem) {
845
+ log(` ✗ ${problem} — no paper created`);
846
+ paperCode = 2;
847
+ } else {
848
+ const code = await createPaper(papersAbs, wanted, fmt);
849
+ if (code !== 0) {
850
+ log(` ⚠ the new paper's lint exited ${String(code)} — see above`);
851
+ paperCode = code;
852
+ }
853
+ }
854
+ } else if (hasPaper) log(` ✓ ${papersDir} already holds a paper`);
855
+ else
856
+ log(
857
+ ` · none yet${interactive ? "" : ` — ${why}, so nothing was asked`}. \`npx paperlint new <name>\` or \`--paper <name>\` creates one`,
858
+ );
859
+
860
+ // ── 7. the toolchain is reported, never installed ─────────────────────────────────────
861
+ log(``);
862
+ log(
863
+ `external programs (the skills shell out to these; \`paperlint lint\` needs none of them)`,
864
+ );
865
+ const missing = missingPrograms(run);
866
+ if (missing.length === 0)
867
+ log(` ✓ all ${String(PROGRAMS.length)} are on PATH`);
868
+ else {
869
+ // 🔴 THE NAMES AND THE COUNT HERE, THE CONSEQUENCES — IN THE doctor REPORT BELOW, AND THIS IS
870
+ // NOT ABOUT SAVING LINES. The first version printed here the same "✗ program — what goes silent
871
+ // without it" table that doctor prints twenty lines later. Not only did the reader see it
872
+ // twice — the harness assert could not tell one from the other and left a mutation that cut the
873
+ // remedy OUT of init green. One fact is printed by one author.
874
+ log(
875
+ ` ✗ ${String(missing.length)} of ${String(PROGRAMS.length)} missing: ` +
876
+ missing.map((p) => p.bin).join(", "),
877
+ );
878
+ log(
879
+ ` what each one is for is in the doctor report below. Nothing is installed for you —`,
880
+ );
881
+ log(
882
+ ` an install that can fail quietly is worse than a step that says what it needs:`,
883
+ );
884
+ for (const cmd of [...new Set(missing.map((p) => p.install))])
885
+ log(` ${cmd}`);
886
+ }
887
+
888
+ log(nextSteps(papersDir));
889
+
890
+ // ── 8. the install states its own condition ───────────────────────────────────────────
891
+ log(`── paperlint doctor ${"─".repeat(56)}`);
892
+ const cliPapers = resolveCliPapers ? resolveCliPapers(root) : papersDir;
893
+ const code = doctor({ log, cwd: root, projectDir: root, run, cliPapers });
894
+ if (code !== 0)
895
+ log(
896
+ `doctor exits ${String(code)} — the install is NOT finished. The lines marked ✗ above say what is\n` +
897
+ `left; re-run \`npx paperlint doctor\` once you have done them.`,
898
+ );
899
+ return paperCode !== 0 ? paperCode : code;
900
+ }