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,535 @@
1
+ /**
2
+ * consumer.mjs — WHERE THE CONSUMER IS, AND WHETHER I AM THE ENTRY POINT.
3
+ *
4
+ * Sibling of `eslint-rules/papers.mjs`. That module answers "where does the consumer keep its
5
+ * papers"; this one answers the two questions a script inside `node_modules` cannot answer for
6
+ * itself once it stops living in the repository it serves:
7
+ *
8
+ * 1. where is the consumer's repository root, now that walking `..` from here lands in
9
+ * `node_modules/` instead;
10
+ * 2. where does the run ledger live, now that the directory next to this file is wiped by
11
+ * `npm ci`;
12
+ * 3. was this file executed, or merely imported — a question whose usual answer stops
13
+ * working the moment the file is reached through a symlink;
14
+ * 4. by what path a SKILL'S PROSE names these scripts, so that a checker can tell an
15
+ * instruction that runs them from one that runs something else. See the fourth-carrier
16
+ * section at the bottom of this file.
17
+ *
18
+ * ── HOW THIS PACKAGE IS REACHED ─────────────────────────────────────────────
19
+ * A consumer keeps a symlink where the directory used to be:
20
+ *
21
+ * .claude/skills/paper-pipeline/scripts -> ../../../../node_modules/
22
+ * paperlint/skills/
23
+ * paper-pipeline/scripts
24
+ *
25
+ * so every command a skill's prose already names — `node .claude/skills/paper-pipeline/scripts/
26
+ * ledger.mjs …` — keeps working unchanged. That is the whole reason the symlink exists: the
27
+ * first consumer names those paths 306 times across 129 files, almost all of it in the PROSE of
28
+ * skills, which no refactor can rewrite.
29
+ *
30
+ * ── MEASURED 2026-09-12, and it decides every function below ────────────────
31
+ * Running `node .claude/skills/…/probe.mjs` through such a symlink:
32
+ *
33
+ * import.meta.url file:///…/node_modules/<pkg>/skills/…/probe.mjs ← REALPATH
34
+ * process.argv[1] /…/.claude/skills/…/probe.mjs ← SYMLINK PATH
35
+ * process.cwd() /…/<the consumer repository root>
36
+ *
37
+ * Node resolves the entry point to its realpath (`--preserve-symlinks-main` is off by default)
38
+ * but leaves `argv[1]` as typed. Three consequences, each one a function here:
39
+ *
40
+ * 🔴 `import.meta.url === \`file://${process.argv[1]}\`` — the main guard nine scripts in this
41
+ * directory used — is `false` through the symlink. NOT an error: the process exits 0 having
42
+ * done nothing. `ledger.mjs record …` would silently record nothing, `announce.mjs` would
43
+ * silently announce nothing, and every caller would read success. `isMain()` compares
44
+ * against the REALPATH of `argv[1]` instead, which is true through both routes.
45
+ *
46
+ * 🔴 `resolve(import.meta.dirname, "..", "..", "..", "..")` — the repository root, correct
47
+ * while these files lived in the consumer — now lands inside `node_modules`. It is replaced
48
+ * by `process.cwd()`, which is the consumer root because every one of those 306 invocations
49
+ * is `node .claude/skills/…` typed from the repository root. `consumerRoot()`.
50
+ *
51
+ * 🔴 `join(import.meta.dirname, "runs.jsonl")` — the ledger's default home — now points inside
52
+ * `node_modules`, which `npm ci` deletes. Appending there is SILENT DATA LOSS: the write
53
+ * succeeds, the rows are real, and they vanish at the next install with no error anywhere.
54
+ * `ledgerPath()` refuses to return that path at all.
55
+ *
56
+ * ── THE LEDGER, THREE RUNGS ─────────────────────────────────────────────────
57
+ * 1. `PIPELINE_LEDGER` in the environment — always wins. This is what test harnesses set to
58
+ * keep fixture rows out of real history, so it must outrank a declaration on disk.
59
+ * 2. `"paperlint": { "ledger": "…" }` in the CONSUMER's `package.json`, read
60
+ * from `process.cwd()`, resolved relative to it.
61
+ * 3. `runs.jsonl` beside this file — ONLY when this file is not inside `node_modules`, i.e.
62
+ * when the package is being developed in its own checkout. Inside `node_modules` with
63
+ * nothing declared, this THROWS.
64
+ *
65
+ * 🔴 WHY RUNG 3 THROWS RATHER THAN GUESSING. Every other candidate default is worse in the same
66
+ * direction. Writing beside the file loses data at the next `npm ci`. Writing to
67
+ * `<cwd>/runs.jsonl` puts an append-only journal at the top of someone's repository without
68
+ * asking. Writing nowhere makes `record()` a no-op, which is the stored "nothing was wrong" this
69
+ * ledger exists to abolish. A throw is the only one of the four that cannot be mistaken for
70
+ * having worked — and it carries the cure, naming the key and the file to put it in.
71
+ *
72
+ * ⚠️ THE EXAMPLES HERE ARE DELIBERATELY GENERIC (`docs/pipeline-runs.jsonl`). The first
73
+ * consumer's own directory names are absent by the same rule `papers.harness.mjs` part IV
74
+ * enforces for `papers.mjs`: a package that spells out one user's private tree has one user.
75
+ */
76
+ import {
77
+ existsSync,
78
+ readFileSync,
79
+ readdirSync,
80
+ readlinkSync,
81
+ realpathSync,
82
+ statSync,
83
+ } from "node:fs";
84
+ import { join, resolve, sep } from "node:path";
85
+ import { fileURLToPath, pathToFileURL } from "node:url";
86
+
87
+ // Re-exported, not re-declared — see the note in `lib/paper-config.mjs`. This file is not a hook
88
+ // and never needed its own copy.
89
+ import { CONFIG_KEY, settingsOf } from "../../../lib/paper-config.mjs";
90
+ export { CONFIG_KEY, settingsOf };
91
+
92
+ /**
93
+ * True when `metaUrl` belongs to the module Node was told to execute.
94
+ *
95
+ * Replaces `import.meta.url === \`file://${process.argv[1]}\``, which is false through a symlink
96
+ * and therefore turns a CLI into a no-op that exits 0. See the measurement above.
97
+ *
98
+ * @param metaUrl pass `import.meta.url` from the calling module.
99
+ */
100
+ export function isMain(metaUrl) {
101
+ const entry = process.argv[1];
102
+ if (!entry) return false;
103
+ let real;
104
+ try {
105
+ real = realpathSync(entry);
106
+ } catch {
107
+ // The entry point does not exist on disk — `node --eval`, a deleted file, a virtual loader.
108
+ // Nothing to be the main module OF, so fall back to the plain comparison rather than throw:
109
+ // being wrong here must never be louder than the program the caller actually asked for.
110
+ real = entry;
111
+ }
112
+ return metaUrl === pathToFileURL(real).href;
113
+ }
114
+
115
+ /**
116
+ * The root of the repository USING this package.
117
+ *
118
+ * `process.cwd()` rather than a walk up from `import.meta.dirname`, because the walk now ends in
119
+ * `node_modules`. `CLAUDE_PROJECT_DIR` wins when set: the agent harness exports it, and it is
120
+ * correct even when a hook or an editor starts the process somewhere other than the root.
121
+ */
122
+ export function consumerRoot({ env = process.env, cwd = process.cwd() } = {}) {
123
+ return env.CLAUDE_PROJECT_DIR || cwd;
124
+ }
125
+
126
+ /**
127
+ * The directory of venue TeX files THIS PACKAGE ships — `paper-guards.tex` and each venue's
128
+ * `<venue>.tex` — which a paper's preamble `\input`s and `paperlint build` puts on `TEXINPUTS`.
129
+ *
130
+ * Resolved from this file's own location, never from the caller's cwd: the directory travels
131
+ * with the package, wherever the package is installed (own checkout, `node_modules`, a plugin
132
+ * cache). `import.meta.url` is the realpath, so a symlinked skills directory changes nothing.
133
+ */
134
+ export function packageVenuesDir() {
135
+ return fileURLToPath(
136
+ new URL("../../submit-paper/references/venues", import.meta.url),
137
+ );
138
+ }
139
+
140
+ /**
141
+ * The package's npm name — the directory it lands in under `node_modules`, and the name Node
142
+ * resolves it by. Every path into the installed package is built from this, never re-spelled;
143
+ * `consumer.harness.mjs` checks it against `package.json`.
144
+ */
145
+ export const PACKAGE_NAME = "paperlint";
146
+
147
+ /**
148
+ * The name the package was published under before 2.0.0. Only for recognising what an older
149
+ * install left behind (hook commands, skill links, the config key) so `init` can replace it and
150
+ * `doctor` can name it.
151
+ */
152
+ export const LEGACY_PACKAGE_NAME = "research-paper-pipeline";
153
+
154
+ /**
155
+ * Where the package keeps the skills it ships, relative to the package root. One constant, read by
156
+ * the linker (`src/link-skills.ts`) and the install e2e, so the two cannot disagree; `files` in
157
+ * package.json must include it for the skills to reach the tarball.
158
+ */
159
+ export const SHIPPED_SKILLS_DIR = "skills";
160
+
161
+ /** The consumer's skills directory — the fixed Claude Code layout, under its root. */
162
+ export function consumerSkillsDir(opts) {
163
+ return join(consumerRoot(opts), ".claude", "skills");
164
+ }
165
+
166
+ /**
167
+ * The skills installed in `dir`: every entry that LEADS TO a directory holding a `SKILL.md`,
168
+ * sorted by name. The one answer to "which skills are here" — for the consumer's
169
+ * `.claude/skills/`, for this package's own declared skills directory, and for a copy of either.
170
+ *
171
+ * 🔴 IT FOLLOWS SYMLINKS, AND THAT IS THE WHOLE POINT (rpp#62). `paperlint init` (`src/link-skills.ts`)
172
+ * installs every skill as a link `.claude/skills/<name> -> …/skills/<name>`. A `Dirent` from
173
+ * `readdirSync(dir, { withFileTypes: true })` describes the entry itself, so `isDirectory()` is
174
+ * false for every link: five eval preflights asked the question that way, saw zero skills in
175
+ * every consumer, and refused to start. The writer of those links imports this function too, so
176
+ * the code that makes the fact and the code that reads it back cannot disagree about its shape.
177
+ *
178
+ * 🔴 A LINK THAT DOES NOT RESOLVE IS REFUSED — not skipped, not counted. Skipped, it reads as a
179
+ * skill that was never installed and sends the reader after the wrong cause; counted, it hands the
180
+ * caller a skill whose `SKILL.md` cannot be opened. Every caller is a check or a preflight, for
181
+ * which a broken install is an answer to report, not a detail to paper over. The error carries
182
+ * `code: "DANGLING_SKILL_LINK"` and `dangling: [{ name, target, cause }]` for a caller that
183
+ * formats its own report.
184
+ *
185
+ * ⚠️ Why this lives here and not in `src/link-skills.ts` beside the writer: the readers include
186
+ * `.mjs` scripts that run from `node_modules` in a consumer, and Node refuses to strip types
187
+ * there (`ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING`, measured on Node 22.22). Importing the
188
+ * built `dist/` instead would make every eval depend on a build being current. So the fact lives
189
+ * in the port and the TypeScript writer imports it, as `src/cli.ts` already does for `isMain`.
190
+ *
191
+ * @param dir the directory to list. A missing directory throws `ENOENT` from `readdirSync`.
192
+ * @returns the skill names, sorted.
193
+ */
194
+ export function installedSkills(dir) {
195
+ const names = [];
196
+ const dangling = [];
197
+ for (const name of readdirSync(dir)) {
198
+ const entry = join(dir, name);
199
+ let st;
200
+ try {
201
+ st = statSync(entry); // FOLLOWS the link — the question is where the entry LEADS
202
+ } catch (e) {
203
+ // The entry was listed a moment ago, so a failed stat is a link that leads nowhere
204
+ // (ENOENT) or in a circle (ELOOP) — not an absence.
205
+ dangling.push({
206
+ name,
207
+ target: readlinkOr(entry),
208
+ cause: e.code ?? "error",
209
+ });
210
+ continue;
211
+ }
212
+ if (st.isDirectory() && existsSync(join(entry, "SKILL.md")))
213
+ names.push(name);
214
+ }
215
+ if (dangling.length) {
216
+ const err = new Error(
217
+ `${dangling.length} skill link(s) in ${dir} lead nowhere:\n` +
218
+ dangling
219
+ .map((d) => ` ${d.name} -> ${d.target} (${d.cause})`)
220
+ .join("\n") +
221
+ `\nRe-run \`npx paperlint init\` if the package moved, or remove the link if the skill was ` +
222
+ `retired. This is refused rather than skipped: a skipped link reads as a skill that ` +
223
+ `was never installed.`,
224
+ );
225
+ err.code = "DANGLING_SKILL_LINK";
226
+ err.dangling = dangling;
227
+ throw err;
228
+ }
229
+ return names.sort();
230
+ }
231
+
232
+ /** Where a link points, for the message — or a placeholder when it is not a link at all. */
233
+ function readlinkOr(entry) {
234
+ try {
235
+ return readlinkSync(entry);
236
+ } catch {
237
+ return "(not a link)";
238
+ }
239
+ }
240
+
241
+ /** The consumer's parsed `package.json`, or `null` when there is none or it does not parse. */
242
+ export function consumerPkg(opts) {
243
+ const file = join(consumerRoot(opts), "package.json");
244
+ try {
245
+ return JSON.parse(readFileSync(file, "utf8"));
246
+ } catch {
247
+ return null;
248
+ }
249
+ }
250
+
251
+ /**
252
+ * True when `dir` lies inside a `node_modules` directory.
253
+ *
254
+ * Segment-wise, not `includes("node_modules")`: a repository legitimately named
255
+ * `my-node_modules-inspector` is not an installed package, and a substring test would refuse to
256
+ * write its ledger. Cheap to get right, and the failure it prevents is a throw in someone's face.
257
+ */
258
+ export function insideNodeModules(dir) {
259
+ return dir.split(sep).includes("node_modules");
260
+ }
261
+
262
+ /**
263
+ * Where the run ledger lives. Three rungs; see the docblock.
264
+ *
265
+ * @param hereDir the directory of the module that owns the ledger — pass `import.meta.dirname`.
266
+ * @returns an absolute path. Never a path inside `node_modules`.
267
+ */
268
+ export function ledgerPath(
269
+ hereDir,
270
+ { env = process.env, cwd = process.cwd() } = {},
271
+ ) {
272
+ // Rung 1 — the environment. Deliberately first: harnesses redirect the ledger BEFORE importing
273
+ // it, and a declaration on disk that could override that would put fixture rows in real history.
274
+ if (env.PIPELINE_LEDGER) return resolve(env.PIPELINE_LEDGER);
275
+
276
+ // Rung 2 — the consumer's declaration.
277
+ const root = consumerRoot({ env, cwd });
278
+ const declared = settingsOf(consumerPkg({ env, cwd }))?.ledger;
279
+ // 🔴 `declared === undefined`, NOT `declared ?? default` — the same distinction `papersRoot()`
280
+ // makes and for the same reason: `"ledger": null` is a keystroke, not an absence, and reading
281
+ // it as "nothing was declared" would silently pick a different file than the one asked for.
282
+ if (declared !== undefined) {
283
+ if (typeof declared !== "string" || declared.length === 0)
284
+ throw new TypeError(
285
+ `${CONFIG_KEY}: "ledger" must be a non-empty string, got ${JSON.stringify(declared)}`,
286
+ );
287
+ return resolve(root, declared);
288
+ }
289
+
290
+ // Rung 3 — beside this file, but only in the package's own checkout.
291
+ if (insideNodeModules(hereDir))
292
+ throw new Error(
293
+ `${CONFIG_KEY}: no ledger location is declared, and the default (a file beside this ` +
294
+ `module) is inside node_modules, which \`npm ci\` deletes.\n` +
295
+ `Declare where the ledger lives, in ${join(root, "package.json")}:\n` +
296
+ ` "${CONFIG_KEY}": { "ledger": "docs/pipeline-runs.jsonl" }\n` +
297
+ `or set PIPELINE_LEDGER for a single run.\n` +
298
+ `This is thrown rather than defaulted on purpose: appending to a path under ` +
299
+ `node_modules succeeds, so the rows would look recorded right up until the next ` +
300
+ `install removed them, with no error at any point.`,
301
+ );
302
+ return join(hereDir, "runs.jsonl");
303
+ }
304
+
305
+ /**
306
+ * `ledgerPath()` plus the assurance that the directory holding it exists.
307
+ *
308
+ * Split out rather than folded in, because the two failures are different: an undeclared ledger
309
+ * is a setup mistake the message above can fix, while a declared-but-missing directory may be
310
+ * the very thing the caller is about to create.
311
+ */
312
+ export function ledgerPathExisting(hereDir, opts) {
313
+ const p = ledgerPath(hereDir, opts);
314
+ if (!existsSync(p))
315
+ throw new Error(
316
+ `${CONFIG_KEY}: the ledger "${p}" does not exist. Create it (an empty file is a valid ` +
317
+ `empty ledger) or fix the "ledger" declaration in package.json.`,
318
+ );
319
+ return p;
320
+ }
321
+
322
+ // ═════════════════════════════════════════════════════════════════════════════════════════════
323
+ // THE FOURTH CARRIER — WHERE A SKILL'S PROSE NAMES THESE SCRIPTS
324
+ //
325
+ // `papersRoot()` answers "where does the consumer keep its papers"; `ledgerPath()` answers
326
+ // "where does the run journal live"; `citeChecks` (read in `run-mechanical.mjs`) answers "where
327
+ // are the consumer's own citation checkers". This answers the fourth: BY WHAT PATH does a skill
328
+ // instruct the model to run `announce.mjs` and `ledger.mjs`.
329
+ //
330
+ // 🔴 IT IS A DIFFERENT KIND OF PATH FROM THE OTHER THREE, and the difference decides the API.
331
+ // The other three are read by CODE and may be absolute. This one is compared against text a
332
+ // human wrote inside a SKILL.md — `node .claude/skills/paper-pipeline/scripts/ledger.mjs record
333
+ // …` — so it must stay relative to the consumer root and spelled with `/`, exactly as the prose
334
+ // spells it. Returning an absolute path here would make every `startsWith` test false and every
335
+ // check that depends on it pass over an empty set.
336
+ //
337
+ // ── WHY IT NEEDS A DECLARATION AT ALL ───────────────────────────────────────
338
+ // `.claude/skills` is fixed by the agent harness and `paper-pipeline/scripts` is this package's
339
+ // own convention, so the default is right for a consumer that keeps the customary symlink. It
340
+ // is NOT right for a consumer that mounts the scripts elsewhere, renames the skill, or has not
341
+ // made the symlink yet — and the failure in all three cases is the silent one: a prefix that
342
+ // matches nothing turns `for (… of commands) if (!c.script.startsWith(prefix)) continue;` into a
343
+ // loop with an empty body. Zero findings, exit 0, indistinguishable from a corpus that passed.
344
+ //
345
+ // ── THE TWO REFUSALS ────────────────────────────────────────────────────────
346
+ // 1. the resolved directory is inside `node_modules` — refused even though it exists. This is
347
+ // the tempting wrong fix after the move ("just point at the installed copy"), and it is
348
+ // wrong twice over: `npm ci` deletes that tree, and a skill's prose would then name a path
349
+ // no one can read in the repository. It is also how the DEFAULT fails: with the process
350
+ // started inside the installed package and `CLAUDE_PROJECT_DIR` unset, `consumerRoot()` is
351
+ // itself under `node_modules`, and the default resolves under it.
352
+ // 2. the resolved directory is not on disk — refused for the reason in the paragraph above:
353
+ // the wrong prefix produces no findings rather than wrong ones.
354
+ // ═════════════════════════════════════════════════════════════════════════════════════════════
355
+
356
+ /**
357
+ * The default. A consumer that declares nothing is assumed to keep the customary symlink at the
358
+ * customary place, so that the path a skill's prose already names keeps resolving.
359
+ */
360
+ export const DEFAULT_SCRIPTS_ROOT = ".claude/skills/paper-pipeline/scripts";
361
+
362
+ /**
363
+ * The root-relative path by which the consumer's skills name this package's pipeline scripts.
364
+ *
365
+ * @returns the path as DECLARED (relative, `/`-separated) — it is compared against prose, not
366
+ * opened. Use `join(consumerRoot(), …)` when you need to touch the file.
367
+ */
368
+ export function scriptsRoot({ env = process.env, cwd = process.cwd() } = {}) {
369
+ const root = consumerRoot({ env, cwd });
370
+ const declared = settingsOf(consumerPkg({ env, cwd }))?.scripts;
371
+ // 🔴 `declared === undefined`, NOT `declared ?? DEFAULT` — the same distinction `papersRoot()`
372
+ // and `ledgerPath()` make, for the same reason: `"scripts": null` is a keystroke, not an
373
+ // absence, and silently substituting the default for it hides a typo behind a working run.
374
+ const rel = declared === undefined ? DEFAULT_SCRIPTS_ROOT : declared;
375
+ if (typeof rel !== "string" || rel.length === 0)
376
+ throw new TypeError(
377
+ `${CONFIG_KEY}: "scripts" must be a non-empty string, got ${JSON.stringify(rel)}`,
378
+ );
379
+
380
+ const abs = resolve(root, rel);
381
+ if (insideNodeModules(abs))
382
+ throw new Error(
383
+ `${CONFIG_KEY}: the pipeline scripts path "${rel}" resolves to ${abs}, which is inside ` +
384
+ `node_modules.\n` +
385
+ `That path cannot be the one a skill names: \`npm ci\` deletes the tree, and the ` +
386
+ `instruction would point at a directory the repository does not track.\n` +
387
+ `Keep a symlink where the prose already looks —\n` +
388
+ ` ${join(root, DEFAULT_SCRIPTS_ROOT)} -> node_modules/${CONFIG_KEY}/skills/paper-pipeline/scripts\n` +
389
+ `— or declare the real, repository-relative location in ${join(root, "package.json")}:\n` +
390
+ ` "${CONFIG_KEY}": { "scripts": "path/to/pipeline/scripts" }\n` +
391
+ `(If nothing was declared, the root itself is under node_modules: run the command from ` +
392
+ `the consumer repository, or export CLAUDE_PROJECT_DIR.)`,
393
+ );
394
+ if (!existsSync(abs))
395
+ throw new Error(
396
+ `${CONFIG_KEY}: the pipeline scripts path "${rel}" does not exist under ${root}.\n` +
397
+ (declared === undefined
398
+ ? `Nothing was declared, so the default "${DEFAULT_SCRIPTS_ROOT}" was used. Create the ` +
399
+ `symlink there, or declare the real location in package.json:\n` +
400
+ ` "${CONFIG_KEY}": { "scripts": "path/to/pipeline/scripts" }`
401
+ : `It is declared in package.json under "${CONFIG_KEY}" as ` +
402
+ `"scripts": ${JSON.stringify(declared)}. Fix it there, or create the directory.`) +
403
+ `\nThis is thrown rather than ignored on purpose: this value is a PREFIX that callers ` +
404
+ `filter prose with, so a wrong one matches no instruction at all and every check built ` +
405
+ `on it reports zero findings — byte-identical to a corpus that was checked and passed.`,
406
+ );
407
+ return rel;
408
+ }
409
+
410
+ // ═════════════════════════════════════════════════════════════════════════════════════════════
411
+ // THE SIXTH CARRIER — WHICH WALL CLOCK THE CONSUMER SCHEDULES AGAINST
412
+ //
413
+ // `papersRoot()` answers "where are the papers", `ledgerPath()` "where is the journal",
414
+ // `citeChecks` "where are the consumer's citation checkers", `scriptsRoot()` "by what path does
415
+ // prose name these scripts", `triggerCases` "where are the consumer's own trigger cases". This
416
+ // answers the sixth: IN WHAT TIME ZONE does a deadline anchor get written.
417
+ //
418
+ // 🔴 WHY IT IS A DECLARATION AND NOT A CONSTANT. A deadline is the one quantity in this pipeline
419
+ // that is meaningless without a zone, and the zone belongs to the PERSON, not to the pipeline —
420
+ // the same CFP date is a different wall-clock hour for every author. A package that hard-codes
421
+ // one author's zone quietly schedules everyone else's submit-day at the wrong hour, and the
422
+ // failure surfaces as "I thought I had another day", which is the exact failure the AoE section
423
+ // of `plan-paper-timeline` exists to prevent.
424
+ //
425
+ // ── WHY THE DEFAULT IS `UTC` AND NOT A GUESS AT THE HOST'S ZONE ─────────────────
426
+ // `Intl.DateTimeFormat().resolvedOptions().timeZone` would return whatever zone the container
427
+ // happens to carry — which in CI is `UTC` and on a laptop is the laptop's, so the SAME repository
428
+ // would schedule differently depending on where the command ran. A fixed `UTC` is wrong in a way
429
+ // the reader can see and correct; a host-derived zone is wrong in a way that moves.
430
+ //
431
+ // ⚠️ VALIDATED, not merely typed. `Intl.DateTimeFormat` throws `RangeError` on an unknown zone, so
432
+ // a typo like `Europe/Berlinn` is caught here — before it reaches a calendar API that would either
433
+ // reject it a network round-trip later or, worse, accept a zone the author did not mean.
434
+ // ═════════════════════════════════════════════════════════════════════════════════════════════
435
+
436
+ /** The default. `UTC` is the only zone that is nobody's local time, so it cannot be mistaken for one. */
437
+ export const DEFAULT_TIMEZONE = "UTC";
438
+
439
+ /**
440
+ * The IANA time zone the consumer's deadline anchors are written in.
441
+ *
442
+ * @returns the zone as DECLARED, e.g. `"Europe/Berlin"` — suitable for a calendar API's
443
+ * `timeZone` field and for `Intl` options.
444
+ */
445
+ export function consumerTimezone({
446
+ env = process.env,
447
+ cwd = process.cwd(),
448
+ } = {}) {
449
+ const declared = settingsOf(consumerPkg({ env, cwd }))?.timezone;
450
+ // 🔴 `declared === undefined`, NOT `declared ?? DEFAULT` — the same distinction every carrier
451
+ // above makes: `"timezone": null` is a keystroke, not an absence.
452
+ const tz = declared === undefined ? DEFAULT_TIMEZONE : declared;
453
+ if (typeof tz !== "string" || tz.length === 0)
454
+ throw new TypeError(
455
+ `${CONFIG_KEY}: "timezone" must be a non-empty IANA zone string, got ${JSON.stringify(tz)}`,
456
+ );
457
+ try {
458
+ new Intl.DateTimeFormat("en-US", { timeZone: tz });
459
+ } catch {
460
+ throw new RangeError(
461
+ `${CONFIG_KEY}: "timezone" is ${JSON.stringify(tz)}, which is not an IANA time zone this ` +
462
+ `runtime knows (e.g. "Europe/Berlin", "America/New_York", "UTC").\n` +
463
+ `Fix it in package.json under "${CONFIG_KEY}".\n` +
464
+ `This throws rather than falling back to ${DEFAULT_TIMEZONE} on purpose: a silent fallback ` +
465
+ `would put every deadline anchor at the wrong hour while looking like it worked.`,
466
+ );
467
+ }
468
+ return tz;
469
+ }
470
+
471
+ // ═════════════════════════════════════════════════════════════════════════════════════════════
472
+ // THE SEVENTH CARRIER — WHOM THE SCHOLARLY APIs SHOULD CONTACT
473
+ //
474
+ // Crossref and friends run a "polite pool": send a `mailto:` in the User-Agent and you get the
475
+ // faster, more reliable tier, plus a warning by email instead of a silent block if your usage
476
+ // looks abusive.
477
+ //
478
+ // 🔴 WHY THIS CANNOT BE A CONSTANT, and it is not a privacy point but a correctness one. A
479
+ // hard-coded address makes EVERY user of this package send traffic attributed to ONE person: the
480
+ // rate-limit warnings go to someone who cannot act on them, and the person actually hammering
481
+ // the API never hears about it. (It is also how one author's personal address ends up in a
482
+ // public repository, which is the reason it was noticed.)
483
+ //
484
+ // ── WHY AN ABSENT VALUE IS A SILENT DEFAULT HERE, UNLIKE EVERY CARRIER ABOVE ────
485
+ // The others throw when undeclared because a missing path makes a FILTER match nothing, and a
486
+ // filter matching nothing reports "clean" about files it never opened. Nothing of that shape
487
+ // happens here: with no address the request simply goes to the public pool — slower and more
488
+ // rate-limited, which is visible as slowness, not as a wrong answer. Throwing would refuse to
489
+ // check citations at all over a courtesy header.
490
+ // ═════════════════════════════════════════════════════════════════════════════════════════════
491
+
492
+ /**
493
+ * The contact address for scholarly-API User-Agents, or `null` when none is declared.
494
+ *
495
+ * @returns the declared address, or `null` — callers omit the `mailto:` fragment on `null`.
496
+ */
497
+ export function consumerContactEmail({
498
+ env = process.env,
499
+ cwd = process.cwd(),
500
+ } = {}) {
501
+ const declared = settingsOf(consumerPkg({ env, cwd }))?.contactEmail;
502
+ if (declared === undefined || declared === null) return null;
503
+ if (typeof declared !== "string" || !declared.includes("@"))
504
+ throw new TypeError(
505
+ `${CONFIG_KEY}: "contactEmail" must be an email address, got ${JSON.stringify(declared)}. ` +
506
+ `Remove the key entirely to use the public pool.`,
507
+ );
508
+ return declared;
509
+ }
510
+
511
+ /**
512
+ * The individual script paths, derived from one root.
513
+ *
514
+ * 🔴 THE NAMES LIVE HERE, NOT IN THE CONSUMER — the same half that keeps `paperFiles()` honest.
515
+ * `announce.mjs` and `ledger.mjs` are this package's contract; a consumer that spelled them out
516
+ * itself would keep its copy of the list in step by hand, and a hand-kept list rots silently.
517
+ */
518
+ export function pipelineScripts(root) {
519
+ // 🔴 ONE TRAILING SLASH, NOT THE ONE THAT WAS TYPED. `scriptsRoot()` returns the declaration
520
+ // verbatim — it must, the value is compared against prose — so `"scripts": "tools/pipeline/"`
521
+ // is a perfectly reasonable thing for someone to write and reaches here with its own slash.
522
+ // Concatenating another gives `tools/pipeline//`, which matches NOTHING: every filter built on
523
+ // the prefix goes quiet and the checks report a clean corpus they never entered. The normalise
524
+ // belongs here rather than in the resolver, because here the value is being turned into a path
525
+ // and there it is still the consumer's own words.
526
+ const base = root.replace(/\/+$/, "");
527
+ return {
528
+ /** What every prose path under this root starts with. Trailing slash, for `startsWith`. */
529
+ prefix: `${base}/`,
530
+ /** Announces that a gate has started. */
531
+ announce: `${base}/announce.mjs`,
532
+ /** Appends the verdict row. */
533
+ ledger: `${base}/ledger.mjs`,
534
+ };
535
+ }