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/doctor.ts ADDED
@@ -0,0 +1,356 @@
1
+ /**
2
+ * `paperlint doctor` — the command that makes SILENCE VISIBLE.
3
+ *
4
+ * 🔴 WHY THIS EXISTS AT ALL, when none of the eight tools surveyed for `docs/install.md` ships a
5
+ * `doctor`. None of them needs one: a formatter that is misconfigured formats nothing and you see
6
+ * it immediately. This package has `paper-edit-guard`, whose success state IS silence — it reports
7
+ * nothing while guarding correctly and reports nothing while guarding a directory that does not
8
+ * exist. From outside, "installed" and "protecting you" are the same picture.
9
+ *
10
+ * Measured 2026-09-18, and this is the failure the command was written for: a consumer who follows
11
+ * the documented install exactly — `paperlint init`, then point the config at their papers — gets a guard
12
+ * that allows every Bash write to their paper sources. It only appears to work when the directory
13
+ * path happens to contain the segment `papers`, because that is the default the hook falls back to.
14
+ * (Issue #33.)
15
+ *
16
+ * 🔴 THE ROOT THE HOOK WOULD USE IS ASKED OF THE HOOK, NOT RE-DERIVED. `papersRoot` is imported
17
+ * from `paper-edit-guard.hook.mjs` itself. A second implementation here would be a second source of
18
+ * truth, which is the very defect this command reports — and it would drift silently, because a
19
+ * copy that disagrees with the original still prints a confident answer.
20
+ *
21
+ * Prior art for the shape: `brew doctor`, `flutter doctor`, `npm doctor`, `expo-doctor`.
22
+ */
23
+ // eslint-disable-next-line boundaries/dependencies -- legacy I/O, moves behind a port in #76
24
+ import { existsSync, readFileSync, readdirSync } from "node:fs";
25
+ import { join, relative, resolve } from "node:path";
26
+ // eslint-disable-next-line boundaries/dependencies -- legacy I/O, moves behind a port in #76
27
+ import { spawnSync } from "node:child_process";
28
+ // Types come from `paper-edit-guard.hook.d.mts`; see the block above for why the import points at
29
+ // the hook itself rather than at a shared module.
30
+ import {
31
+ papersRoot,
32
+ CONFIG_KEY,
33
+ DEFAULT_PAPERS_ROOT,
34
+ PAPERS_DIR_FIELD,
35
+ OLD_PAPERS_DIR_FIELD,
36
+ } from "../hooks/paper-edit-guard.hook.mjs";
37
+ import {
38
+ LEGACY_CONFIG_KEY,
39
+ LEGACY_KEY_MESSAGE,
40
+ declaredSettings,
41
+ } from "../lib/paper-config.mjs";
42
+ import { PAPER_MARKERS } from "./build.ts";
43
+ import { linkSkills, SKILLS_HOME, type LinkReport } from "./link-skills.ts";
44
+ import { doctorHooks } from "./hooks-settings.ts";
45
+
46
+ export interface Program {
47
+ readonly bin: string;
48
+ readonly from: string;
49
+ readonly without: string;
50
+ readonly install: string;
51
+ }
52
+
53
+ /**
54
+ * The programs the SKILLS shell out to. `paperlint lint` needs none of them — it reads files and reports.
55
+ * Every one of these fails quietly, which is the only reason the list is worth printing: a missing
56
+ * checker and a passing checker produce the same silence.
57
+ */
58
+ /**
59
+ * TeX Live is installed by paperlint itself, with every package the venue profiles declare. `paperlint build`
60
+ * uses that tree even when its `pdflatex` is not on PATH, so a ✗ here with the cache installed
61
+ * only means the skills' own shell calls will not find it.
62
+ */
63
+ export const TEX_INSTALL =
64
+ "npx paperlint toolchain (paperlint build uses its TeX Live without PATH; the skills need its bin on PATH)";
65
+
66
+ export const PROGRAMS: readonly Program[] = [
67
+ {
68
+ bin: "pdflatex",
69
+ from: "TeX Live",
70
+ without: "no PDF is produced",
71
+ install: TEX_INSTALL,
72
+ },
73
+ {
74
+ bin: "bibtex",
75
+ from: "TeX Live",
76
+ without: "the bibliography is not resolved",
77
+ install: TEX_INSTALL,
78
+ },
79
+ {
80
+ bin: "texcount",
81
+ from: "TeX Live",
82
+ without: "the length checks cannot run",
83
+ install: TEX_INSTALL,
84
+ },
85
+ {
86
+ bin: "java",
87
+ from: "any JRE",
88
+ without: "TeXtidote does not run, and nothing else spell-checks the text",
89
+ install: "apt-get install -y default-jre-headless",
90
+ },
91
+ {
92
+ bin: "perl",
93
+ from: "your system (macOS ships it)",
94
+ without:
95
+ "banal cannot run: page size, columns and font sizes are null in the facts file",
96
+ install:
97
+ "apt-get install -y perl, then npx paperlint toolchain (it fetches banal)",
98
+ },
99
+ {
100
+ bin: "python3",
101
+ from: "your system",
102
+ without: "the analysis and report scripts do not start",
103
+ install: "apt-get install -y python3",
104
+ },
105
+ ];
106
+
107
+ export const found = (bin: string, run = spawnSync): boolean =>
108
+ run("command", ["-v", bin], { shell: true, stdio: "ignore" }).status === 0;
109
+
110
+ /** Directories that look like papers, used to REPLACE a guessed default with a measurement. */
111
+ export function detectPapers(cwd: string, depth = 2): string[] {
112
+ const hits: string[] = [];
113
+ const skip = new Set(["node_modules", ".git", "dist", "_build", ".claude"]);
114
+ const walk = (dir: string, left: number): void => {
115
+ let entries;
116
+ try {
117
+ entries = readdirSync(dir, { withFileTypes: true });
118
+ } catch {
119
+ return;
120
+ }
121
+ for (const e of entries) {
122
+ if (!e.isDirectory() || e.name.startsWith(".") || skip.has(e.name))
123
+ continue;
124
+ const here = join(dir, e.name);
125
+ // A directory whose CHILDREN look like papers is the papers root — not the paper itself.
126
+ let children;
127
+ try {
128
+ children = readdirSync(here, { withFileTypes: true });
129
+ } catch {
130
+ continue;
131
+ }
132
+ // A dot-child is not a paper: `<papers>/.template/` holds the project's paper TEMPLATE,
133
+ // markers and all, and a folder holding only that is not a papers root yet.
134
+ const isPapersRoot = children.some(
135
+ (c) =>
136
+ c.isDirectory() &&
137
+ !c.name.startsWith(".") &&
138
+ PAPER_MARKERS.some((m) => existsSync(join(here, c.name, m))),
139
+ );
140
+ if (isPapersRoot) hits.push(relative(cwd, here));
141
+ else if (left > 0) walk(here, left - 1);
142
+ }
143
+ };
144
+ walk(resolve(cwd), depth);
145
+ return hits;
146
+ }
147
+
148
+ /**
149
+ * The declaration's verdict. The old field name and two differing keys are failures (every reader
150
+ * refuses them); the old KEY is read and named; a missing declaration is a warning.
151
+ */
152
+ function declarationVerdict(rawPkg: string): { lines: string[]; bad: number } {
153
+ const out: string[] = [];
154
+ const found = (() => {
155
+ try {
156
+ return declaredSettings(JSON.parse(rawPkg));
157
+ } catch {
158
+ return declaredSettings(undefined);
159
+ }
160
+ })();
161
+ if (found.conflict !== null)
162
+ return { lines: [` ✗ ${found.conflict}`], bad: 1 };
163
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- #49: replace with a real type
164
+ const settings = found.settings as Record<string, any> | undefined;
165
+ const key = found.legacy ? LEGACY_CONFIG_KEY : CONFIG_KEY;
166
+ const declared = settings?.[PAPERS_DIR_FIELD];
167
+ if (found.legacy) out.push(` ⚠ ${LEGACY_KEY_MESSAGE}`);
168
+ // The old field name is a failure, not a warning: every reader refuses it.
169
+ if (settings && Object.hasOwn(settings, OLD_PAPERS_DIR_FIELD))
170
+ return {
171
+ lines: [
172
+ ...out,
173
+ ` ✗ "${OLD_PAPERS_DIR_FIELD}" was renamed to "${PAPERS_DIR_FIELD}" in package.json → "${key}"`,
174
+ ],
175
+ bad: 1,
176
+ };
177
+ // 🔴 A MISSING DECLARATION IS A WARNING, NOT A REFUSAL, and that is a decision, not an
178
+ // oversight. Without it the hook takes the `papers` default; if the papers do live there, the
179
+ // install WORKS — just by coincidence, and it will break silently on the day the directory
180
+ // moves. Failing on a working install is not allowed here: for an `error`-level check a false
181
+ // positive costs more than a miss, because people do not fix it, they switch it off — together
182
+ // with the binary findings below, which the command was written for. A real breakage (the roots
183
+ // drifted apart, the directory does not exist) is caught where it is binary.
184
+ if (declared === undefined)
185
+ out.push(
186
+ ` ⚠ package.json has no "${CONFIG_KEY}": { "${PAPERS_DIR_FIELD}": … } — the hooks fall back to "${DEFAULT_PAPERS_ROOT}"`,
187
+ ` it works only while your papers happen to live there; declare it and it keeps working`,
188
+ );
189
+ else
190
+ out.push(
191
+ ` ✓ package.json → ${key}.${PAPERS_DIR_FIELD} = ${JSON.stringify(declared)}`,
192
+ );
193
+ return { lines: out, bad: 0 };
194
+ }
195
+
196
+ /**
197
+ * The papers-directory verdict: whether the CLI and the hooks agree, and whether the directory
198
+ * they name exists. `bad` counts the failures.
199
+ */
200
+ function papersVerdict(
201
+ root: string,
202
+ cliPapers: string | null,
203
+ hookSays: string | null,
204
+ ): { lines: string[]; bad: number } {
205
+ const out: string[] = [];
206
+ let bad = 0;
207
+ if (cliPapers && hookSays) {
208
+ const same = resolve(root, cliPapers) === resolve(root, hookSays);
209
+ out.push(
210
+ same
211
+ ? ` ✓ the same directory — what is linted is what is guarded`
212
+ : ` ✗ DIFFERENT directories. Every Bash write to ${cliPapers} passes the guard unseen.`,
213
+ );
214
+ if (!same) bad++;
215
+ }
216
+ // A declared directory that does not exist is a failure only when papers live SOMEWHERE ELSE:
217
+ // then every write to them passes the guard unseen (issue #33). With no papers anywhere, the
218
+ // project is simply new — `init --yes` declares the default and creates no paper — and failing
219
+ // it would teach people to ignore doctor.
220
+ if (hookSays && !existsSync(join(root, hookSays))) {
221
+ const guesses = detectPapers(root);
222
+ if (guesses.length) {
223
+ out.push(
224
+ ` ✗ ${hookSays} does not exist — the guard is watching nothing`,
225
+ );
226
+ out.push(` papers look like they live in: ${guesses.join(", ")}`);
227
+ bad++;
228
+ } else
229
+ out.push(
230
+ ` ⚠ ${hookSays} does not exist yet — no papers yet. \`npx paperlint new <name>\` creates the first one there`,
231
+ );
232
+ }
233
+ return { lines: out, bad };
234
+ }
235
+
236
+ export interface DoctorOptions {
237
+ log?: typeof console.log;
238
+ cwd?: string;
239
+ /** The project root the hooks are anchored to. Claude Code sets this; it defaults to `cwd`. */
240
+ projectDir?: string;
241
+ run?: typeof spawnSync;
242
+ /** The directory the CLI resolved from its own config, so both sides can be compared. */
243
+ cliPapers?: string | null;
244
+ /** Reads the skill links without writing any. Injected only so a test can fake the install. */
245
+ skillLinks?: (root: string) => LinkReport;
246
+ }
247
+
248
+ /**
249
+ * @returns 2 when the guard and the linter do not agree, or the declaration is unusable — those
250
+ * are binary and are the reason this command exists. A missing external program is a FACT, not a
251
+ * verdict: it is printed and does not fail the run, because which programs you need depends on
252
+ * which skills you use. A doctor that exits non-zero on an advisory line gets piped to /dev/null.
253
+ */
254
+ export function doctor({
255
+ log = console.log,
256
+ // eslint-disable-next-line no-restricted-globals -- legacy I/O, moves behind a port in #76
257
+ cwd = process.cwd(),
258
+ projectDir,
259
+ run = spawnSync,
260
+ cliPapers = null,
261
+ skillLinks = (r: string) => linkSkills(r, { write: false }),
262
+ }: DoctorOptions = {}): number {
263
+ const root = projectDir ?? cwd;
264
+ const pkgPath = join(root, "package.json");
265
+ const out: string[] = [
266
+ "",
267
+ "paperlint doctor — what is wired, and what only looks wired",
268
+ "",
269
+ ];
270
+ let bad = 0;
271
+
272
+ out.push("declaration");
273
+ const rawPkg = existsSync(pkgPath) ? readFileSync(pkgPath, "utf8") : "";
274
+ if (!rawPkg) {
275
+ out.push(` ✗ no package.json at ${root}`);
276
+ out.push(
277
+ ` the hooks read their papers directory from there and refuse without it`,
278
+ );
279
+ bad++;
280
+ } else {
281
+ const declaration = declarationVerdict(rawPkg);
282
+ out.push(...declaration.lines);
283
+ bad += declaration.bad;
284
+ }
285
+ if (existsSync(join(root, "rpp.json")))
286
+ out.push(` ⚠ rpp.json is present — deprecated; the hooks never read it`);
287
+
288
+ out.push("", "papers directory");
289
+ const hookRoot = rawPkg ? papersRoot(rawPkg) : null;
290
+ const hookSays = typeof hookRoot === "string" ? hookRoot : null;
291
+ out.push(
292
+ ` the CLI will lint ${cliPapers ?? "(nothing — no declaration found)"}`,
293
+ );
294
+ out.push(
295
+ ` the hooks will guard ${hookSays ?? "(nothing — the guard refuses and says why on first use)"}`,
296
+ );
297
+ const verdict = papersVerdict(root, cliPapers, hookSays);
298
+ out.push(...verdict.lines);
299
+ bad += verdict.bad;
300
+
301
+ // A skill that is not linked is ADVISORY, like a missing program: `paperlint lint`, the hooks and CI
302
+ // work without it, and an entry of the same name that `init` refused to replace is the
303
+ // consumer's own decision. What this section removes is the silence — before it, a consumer
304
+ // without links had no `/paper-pipeline` and nothing anywhere said so.
305
+ out.push(
306
+ "",
307
+ `skills (Claude Code finds project skills only in ${SKILLS_HOME}/)`,
308
+ );
309
+ const links = skillLinks(root);
310
+ if (!links.ok) out.push(` ⚠ not checked — ${links.error}`);
311
+ else {
312
+ const gaps = links.links.filter((l) => l.status !== "present");
313
+ const total = String(links.links.length);
314
+ if (gaps.length === 0)
315
+ out.push(
316
+ ` ✓ all ${total} shipped skills are reachable as ${join(SKILLS_HOME, "<name>")}`,
317
+ );
318
+ else {
319
+ out.push(
320
+ ` ⚠ ${String(gaps.length)} of ${total} shipped skills are NOT reachable as ${join(SKILLS_HOME, "<name>")}:`,
321
+ );
322
+ for (const g of gaps)
323
+ out.push(
324
+ ` ${g.name} — ${g.status === "missing" ? "not linked" : `${g.reason ?? "occupied"}, not the shipped skill`}`,
325
+ );
326
+ out.push(
327
+ ` \`npx paperlint init\` links the missing ones; it never replaces an entry it did not make`,
328
+ );
329
+ }
330
+ }
331
+
332
+ out.push(
333
+ "",
334
+ "external programs (the skills shell out to these; `paperlint lint` needs none of them)",
335
+ );
336
+ for (const p of PROGRAMS) {
337
+ if (found(p.bin, run)) out.push(` ✓ ${p.bin.padEnd(10)} ${p.from}`);
338
+ else {
339
+ out.push(` ✗ ${p.bin.padEnd(10)} ${p.from} — ${p.without}`);
340
+ out.push(` ${p.install}`);
341
+ }
342
+ }
343
+
344
+ // 🔴 THE HOOKS ARE READ FROM THE FILE THAT CARRIES THEM. Until `init` wrote them into
345
+ // `.claude/settings.json`, the only carrier was a plugin and this section said "cannot be
346
+ // checked from a terminal". Advisory, like the skills: `--no-hooks` is a choice, not a fault.
347
+ out.push("");
348
+ try {
349
+ out.push(...doctorHooks(root));
350
+ } catch (e) {
351
+ out.push(`hooks`, ` ⚠ not checked — ${(e as Error).message}`);
352
+ }
353
+ out.push("");
354
+ log(out.join("\n"));
355
+ return bad > 0 ? 2 : 0;
356
+ }
@@ -0,0 +1,108 @@
1
+ /**
2
+ * A paper's page geometry, as the domain sees it — measured by some measurer, or not measured and
3
+ * why — with no tool named. The facts file's flat schema-2 fields are DERIVED from it
4
+ * (`flatGeometry`), so `geometry_source` and the nine columns always come from the same branch.
5
+ */
6
+ import type { Lines } from "./text.ts";
7
+
8
+ /** What a page-geometry measurer found, in the domain's field names. */
9
+ export interface PageGeometry {
10
+ readonly pageWidthIn: number | null;
11
+ readonly pageHeightIn: number | null;
12
+ readonly columns: number | null;
13
+ readonly bodyPt: number | null;
14
+ readonly refPt: number | null;
15
+ readonly bodyPages: number;
16
+ readonly refPages: number;
17
+ readonly appendixPages: number;
18
+ readonly pagesByType: Readonly<Record<string, number>>;
19
+ }
20
+
21
+ /**
22
+ * Who measured: `tool` is what the facts file writes as `geometry_source`, `path` the program that
23
+ * ran, `how` which rule found it, in the adapter's words. Only a measurer's adapter builds one.
24
+ */
25
+ export interface Provenance {
26
+ readonly tool: string;
27
+ readonly path: string;
28
+ readonly how: string;
29
+ }
30
+
31
+ export type Geometry =
32
+ | {
33
+ readonly kind: "measured";
34
+ readonly by: Provenance;
35
+ readonly geometry: PageGeometry;
36
+ }
37
+ | {
38
+ readonly kind: "unmeasured";
39
+ readonly why: Lines;
40
+ /** The measurer that was run and failed; null when none was found. */
41
+ readonly tried: Provenance | null;
42
+ };
43
+
44
+ /** Why there is no geometry, in one line — naming the measurer that failed, when one ran. */
45
+ export function whyNoGeometry(
46
+ g: Extract<Geometry, { kind: "unmeasured" }>,
47
+ ): string {
48
+ const line = g.why.join("; ");
49
+ return g.tried
50
+ ? `${line} (${g.tried.tool} from ${g.tried.how}: ${g.tried.path})`
51
+ : line;
52
+ }
53
+
54
+ /** The geometry columns as the facts file spells them. */
55
+ export interface FlatGeometry {
56
+ readonly page_w_in: number | null;
57
+ readonly page_h_in: number | null;
58
+ readonly columns: number | null;
59
+ readonly body_pt: number | null;
60
+ readonly ref_pt: number | null;
61
+ readonly body_pages: number;
62
+ readonly ref_pages: number;
63
+ readonly appendix_pages: number;
64
+ readonly pages_by_type: Readonly<Record<string, number>>;
65
+ }
66
+
67
+ /** The geometry columns with nothing measured: every one null. */
68
+ export type NullGeometry = { readonly [K in keyof FlatGeometry]: null };
69
+
70
+ /**
71
+ * The flat schema-2 fields. `geometry_source` is tied to its branch: a measurer's name only beside
72
+ * measured columns, null only beside nine nulls — the file cannot name a measurer over an empty
73
+ * measurement.
74
+ */
75
+ export type FactsGeometryFields =
76
+ | ({ readonly geometry_source: string } & FlatGeometry)
77
+ | ({ readonly geometry_source: null } & NullGeometry);
78
+
79
+ export const NO_GEOMETRY: NullGeometry = {
80
+ page_w_in: null,
81
+ page_h_in: null,
82
+ columns: null,
83
+ body_pt: null,
84
+ ref_pt: null,
85
+ body_pages: null,
86
+ ref_pages: null,
87
+ appendix_pages: null,
88
+ pages_by_type: null,
89
+ };
90
+
91
+ /** The ONE place the domain's names become the file's: camelCase in, schema-2 snake_case out. */
92
+ const flat = (g: PageGeometry): FlatGeometry => ({
93
+ page_w_in: g.pageWidthIn,
94
+ page_h_in: g.pageHeightIn,
95
+ columns: g.columns,
96
+ body_pt: g.bodyPt,
97
+ ref_pt: g.refPt,
98
+ body_pages: g.bodyPages,
99
+ ref_pages: g.refPages,
100
+ appendix_pages: g.appendixPages,
101
+ pages_by_type: g.pagesByType,
102
+ });
103
+
104
+ /** The projection onto the file: one branch in, the matching fields out. */
105
+ export const flatGeometry = (g: Geometry): FactsGeometryFields =>
106
+ g.kind === "measured"
107
+ ? { geometry_source: g.by.tool, ...flat(g.geometry) }
108
+ : { geometry_source: null, ...NO_GEOMETRY };
@@ -0,0 +1,23 @@
1
+ /**
2
+ * The environment, as a VALUE. The composition root reads `process.env` and the host's directories
3
+ * once and passes them in; nothing inside reads the environment again, so no signature below the
4
+ * root carries `process`.
5
+ */
6
+
7
+ export type Environment = Readonly<Record<string, string | undefined>>;
8
+
9
+ /** The three directories the host knows and the environment does not say. All absolute. */
10
+ export interface HostDirs {
11
+ readonly home: string;
12
+ readonly tmp: string;
13
+ /** What a relative path from the environment is relative to. */
14
+ readonly cwd: string;
15
+ }
16
+
17
+ /** An environment as a child process gets it: every value a string, none undefined. */
18
+ export const childEnv = (env: Environment): Record<string, string> =>
19
+ Object.fromEntries(
20
+ Object.entries(env).filter(
21
+ (e): e is [string, string] => e[1] !== undefined,
22
+ ),
23
+ );
@@ -0,0 +1,32 @@
1
+ /**
2
+ * A page as text boxes: the page size in points and every text item with its position, size, font,
3
+ * fill and orientation. The domain's picture of a laid-out page — what a page-geometry measurer
4
+ * takes, with no tool named: pdf.js produces it (`adapters/pdfjs/fill.ts`, `pdf-facts.ts`), and
5
+ * banal's adapter writes it as the XML banal reads (`adapters/banal/xml.ts`).
6
+ */
7
+
8
+ /** The fill a text item was drawn with. `unknown` is drawn as black, as pdftohtml's default is. */
9
+ export type Fill =
10
+ | { readonly kind: "rgb"; readonly hex: string }
11
+ | { readonly kind: "invisible" }
12
+ | { readonly kind: "unknown" };
13
+
14
+ /** One text item, in points, top-down. `size` is the font size in points. */
15
+ export interface TextBox {
16
+ readonly top: number;
17
+ readonly left: number;
18
+ readonly width: number;
19
+ readonly height: number;
20
+ readonly size: number;
21
+ readonly font: string;
22
+ readonly text: string;
23
+ readonly upright: boolean;
24
+ readonly fill: Fill;
25
+ }
26
+
27
+ /** A page's size in points and every text item on it, in content order. */
28
+ export interface PageLayout {
29
+ readonly widthPt: number;
30
+ readonly heightPt: number;
31
+ readonly boxes: readonly TextBox[];
32
+ }
@@ -0,0 +1,5 @@
1
+ /** Paths as the domain holds them. */
2
+ import type { Opaque } from "ts-essentials";
3
+
4
+ /** An absolute path. Minted at the composition root (`abs`), so nothing inside resolves a cwd. */
5
+ export type AbsolutePath = Opaque<string, "AbsolutePath">;
@@ -0,0 +1,26 @@
1
+ import assert from "node:assert/strict";
2
+ import { test } from "vitest";
3
+ import { andThen, err, map, match, ok, type Result } from "./result.ts";
4
+
5
+ const half = (n: number): Result<number, string> =>
6
+ n % 2 === 0 ? ok(n / 2) : err(`${n} is odd`);
7
+
8
+ test("map and andThen run on ok and pass an error through untouched", () => {
9
+ assert.deepEqual(
10
+ map(ok(2), (n) => n + 1),
11
+ ok(3),
12
+ );
13
+ assert.deepEqual(andThen(ok(8), half), ok(4));
14
+ // Guards: an error short-circuits — the function after it is not called.
15
+ assert.deepEqual(
16
+ andThen(half(3), () => assert.fail("called after an error")),
17
+ err("3 is odd"),
18
+ );
19
+ });
20
+
21
+ test("match picks the branch by `ok`", () => {
22
+ const say = (r: Result<number, string>) =>
23
+ match(r, { ok: (n) => `got ${n}`, err: (e) => `no: ${e}` });
24
+ assert.equal(say(half(4)), "got 2");
25
+ assert.equal(say(half(5)), "no: 5 is odd");
26
+ });
@@ -0,0 +1,29 @@
1
+ /**
2
+ * The outcome of something that can fail, as a value: the shape paperlint already spoke in plain unions
3
+ * (`{ ok: true, … } | { ok: false, … }`), made generic. No classes and no fluent API — a `switch`
4
+ * on `ok` reads the same as every other union in the package.
5
+ */
6
+ export type Result<T, E> =
7
+ | { readonly ok: true; readonly value: T }
8
+ | { readonly ok: false; readonly error: E };
9
+
10
+ export const ok = <T>(value: T): Result<T, never> => ({ ok: true, value });
11
+ export const err = <E>(error: E): Result<never, E> => ({ ok: false, error });
12
+
13
+ export const map = <T, U, E>(r: Result<T, E>, f: (t: T) => U): Result<U, E> =>
14
+ r.ok ? ok(f(r.value)) : r;
15
+
16
+ export const andThen = <T, U, E, F>(
17
+ r: Result<T, E>,
18
+ f: (t: T) => Result<U, F>,
19
+ ): Result<U, E | F> => (r.ok ? f(r.value) : r);
20
+
21
+ export const match = <T, E, R>(
22
+ r: Result<T, E>,
23
+ on: { readonly ok: (t: T) => R; readonly err: (e: E) => R },
24
+ ): R => (r.ok ? on.ok(r.value) : on.err(r.error));
25
+
26
+ /** The `default` of an exhaustive `switch`: a new variant is a compile error, not a silent fall-through. */
27
+ export function assertNever(x: never): never {
28
+ throw new Error(`unhandled variant: ${JSON.stringify(x)}`);
29
+ }
@@ -0,0 +1,12 @@
1
+ import assert from "node:assert/strict";
2
+ import { test } from "vitest";
3
+ import { parseSha256, sha256Hex } from "./sha256.ts";
4
+
5
+ test("parseSha256 refuses a typo in a pin", () => {
6
+ assert.throws(() => parseSha256("abc"), /not a sha256/);
7
+ });
8
+
9
+ test("sha256Hex hashes bytes into a value parseSha256 accepts", () => {
10
+ const h = sha256Hex(new TextEncoder().encode("banal"));
11
+ assert.equal(parseSha256(h), h);
12
+ });
@@ -0,0 +1,21 @@
1
+ /**
2
+ * A sha256 as a value: bytes hashed, or a digest written down as text and checked where it is
3
+ * written. Tool-free — a pinned download, a facts file's `pdf_sha256` and a cache key all use it.
4
+ */
5
+ import { createHash } from "node:crypto";
6
+ import type { Opaque } from "ts-essentials";
7
+
8
+ /** 64 lowercase hex digits. Minted by `parseSha256` (a literal) or `sha256Hex` (bytes) only. */
9
+ export type Sha256 = Opaque<string, "Sha256">;
10
+
11
+ const SHA256 = /^[0-9a-f]{64}$/;
12
+
13
+ /** A sha256 written down as text — a pin, an option — checked where it is written, not where it is used. */
14
+ export function parseSha256(s: string): Sha256 {
15
+ if (!SHA256.test(s))
16
+ throw new Error(`not a sha256 (64 lowercase hex digits): ${s}`);
17
+ return s as Sha256;
18
+ }
19
+
20
+ export const sha256Hex = (bytes: Uint8Array): Sha256 =>
21
+ createHash("sha256").update(bytes).digest("hex") as Sha256;
@@ -0,0 +1,11 @@
1
+ /** Text shapes every layer speaks: a message of one or more lines, and the line a failure shows. */
2
+
3
+ /** A message: the first line says what happened, any further lines are detail. Never empty. */
4
+ export type Lines = readonly [string, ...string[]];
5
+
6
+ /** The first non-blank line of a stream — what a failure shows. */
7
+ export const firstLine = (s: string): string =>
8
+ s
9
+ .split("\n")
10
+ .map((l) => l.trim())
11
+ .find(Boolean) ?? "";