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,19 @@
1
+ /**
2
+ * verify-citations — the PAID tier: does this skill's description actually fire?
3
+ *
4
+ * COLOCATED ON PURPOSE (vigiles decides coverage by placement as of 2026-08-11).
5
+ * The prompts live in `.claude/lib/skill-trigger-cases.mjs` so all 21 cases
6
+ * are reviewed as one table where collisions between siblings are visible;
7
+ * copying them here would recreate the drift that rule exists to prevent.
8
+ *
9
+ * Measures recall (fires on its own territory) AND precision (stays quiet on a
10
+ * colliding sibling's territory), against the REAL `.claude` harness so the skill
11
+ * competes with every other installed description — an isolated run overstates
12
+ * recall and understates false positives.
13
+ *
14
+ * Costs money; not CI.
15
+ * node .claude/skills/verify-citations/verify-citations.eval.mjs [trials]
16
+ */
17
+ import { runSkillTriggerEval } from "../../lib/skill-eval-kit.mjs";
18
+
19
+ await runSkillTriggerEval("verify-citations");
@@ -0,0 +1,23 @@
1
+ /**
2
+ * verify-citations — the free, deterministic tier. No model, no network.
3
+ *
4
+ * COLOCATED ON PURPOSE. vigiles decides coverage by PLACEMENT as of 2026-08-11:
5
+ * a test that merely names a surface no longer counts, because that tier was
6
+ * crediting surfaces nothing touched. So each skill needs a file inside its own
7
+ * directory — this one.
8
+ *
9
+ * The assertions live in `.claude/lib/skill-checks.mjs` and are CALLED here with
10
+ * this skill's name. They are not copied: 22 copies of the same checks is the drift that
11
+ * module exists to avoid. (Until 2026-08-11 this was an env-var side channel into a
12
+ * 614-line file named after no surface; it is a function call now.)
13
+ *
14
+ * What this proves: this skill's frontmatter parses as strict YAML, its declared
15
+ * tool contract is sane, its pipeline wiring points at scripts that exist, and it
16
+ * announces/records under ITS OWN identity rather than a sibling's.
17
+ *
18
+ * What it does NOT prove: that the skill fires, or that its guidance produces a
19
+ * good result. Those need a real model — see `verify-citations.eval.mjs`.
20
+ */
21
+ import { checkSkill } from "../../lib/skill-checks.mjs";
22
+
23
+ await checkSkill("verify-citations");
package/src/CLAUDE.md ADDED
@@ -0,0 +1,51 @@
1
+ # CLAUDE.md — `src/`
2
+
3
+ The package's TypeScript: the CLI, the build, the toolchain installer, and the measuring that feeds
4
+ the facts file. Loaded when working under `src/`; the repository-wide rules are in the root
5
+ `CLAUDE.md`.
6
+
7
+ **`src/` is hexagonal on two axes, and the linter enforces both** (`eslint.config.mjs`; fixtures in
8
+ `test/fixtures/layers/`, run by `test/eslint-layers.test.ts`; issue #76).
9
+
10
+ **Axis A — knowledge. Core means the DOMAIN, not "no I/O".** A file belongs to an adapter when it
11
+ exists because of one external program or format: _if banal / pdf.js / TeX Live / ESLint / Claude
12
+ Code disappeared tomorrow, which files would change? Those are its adapter, wherever they sit
13
+ today._ The domain is what would not change if every one of them were swapped. Purity is not the
14
+ test — a parser for banal's JSON is pure and is still banal's.
15
+
16
+ | element | where | holds | may import |
17
+ | --------- | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------- |
18
+ | `domain` | `src/domain/` | values and decisions in the paper's vocabulary | `domain`; `ts-essentials`, `node:path`, `node:crypto` |
19
+ | `port` | `src/ports/` | interfaces **shaped by what the inside needs**, never by what a tool does (Graça: "created to fit the Application Core needs and not simply mimic the tools APIs") | `domain`, `port`; the same three externals |
20
+ | `app` | `src/*.ts` | use cases over ports; **no I/O defaults** — a use case without its deps does not compile | `domain`, `port`, `app`, the package's `.mjs`; Node's `util`/`url`/`module` |
21
+ | `adapter` | `src/adapters/<tool>/` | everything that knows `<tool>` exists, pure parsers included; used through its `index.ts` | `domain`, `port`, its own folder, its tool's packages, the package's `.mjs`. **Never** `app`, **never** another adapter |
22
+ | `root` | `src/cli.ts` | reads `process.*`, builds adapters, wires ports | anything |
23
+
24
+ Folders are ELEMENTS of `eslint-plugin-boundaries`, single files are CATEGORIES (`root`, `app`,
25
+ `test`, `io`): v7 matches element patterns as folders. Everyone — tests included — reaches an
26
+ adapter through its `index.ts`; the files inside one adapter import each other freely.
27
+
28
+ **Axis B — purity.** An effect (disk, process, network, env, clock, TTY) may be written only in
29
+ `src/adapters/<tool>/<x>.io.ts` and `src/cli.ts`. Every other file, adapters' parsers included,
30
+ takes a port. Test: _can a call here fail for a reason not in its arguments?_ Imports are checked
31
+ through the `io` file category in `boundaries/dependencies`; the `process` and `fetch` globals by
32
+ `no-restricted-globals`.
33
+
34
+ **The legacy app files are held by a ratchet, not exempted.** A site that still breaks either axis
35
+ carries `// eslint-disable-next-line <rule> -- legacy <I/O|layer>, moves behind a port in #76`.
36
+ Unused directives are errors (`reportUnusedDisableDirectives`), so a fixed site must drop its
37
+ comment; the per-file counts are frozen in `scripts/layer-legacy.frozen.json` and checked by
38
+ `scripts/layer-legacy-frozen.mjs` (`npm run check`, CI), which counts ESLint's own
39
+ `suppressedMessages` and fails when a file's count grows, a file not in the list carries one, or
40
+ anything under `src/domain/`, `src/ports/` or `src/adapters/` does. A NEW module that needs the
41
+ world is a new `*.io.ts` in the adapter of the program it talks to — never a new directive.
42
+
43
+ Parse at the boundary, into types that cannot hold an invalid state (a banal path that exists only
44
+ after its sha256 and probe passed; a `Ready` only an installer mints; an outcome as a discriminated
45
+ union, not a string); the inside never re-validates.
46
+
47
+ ⚠️ **Two settings are load-bearing.** `boundaries/root-path`: without it the plugin matches paths
48
+ against `process.cwd()`, so lint started from any other directory classifies nothing and passes.
49
+ `checkAllOrigins: true`: without it no `module:` policy is evaluated, so axis B and the library bans
50
+ report nothing — dropping it turns the fixtures `src/domain/reads-disk.ts` and
51
+ `src/adapters/one/disk.ts` clean (measured).
@@ -0,0 +1,26 @@
1
+ import assert from "node:assert/strict";
2
+ import { test } from "vitest";
3
+ import { actionRef } from "./action-ref.ts";
4
+
5
+ /** [the running package's version, the ref `paperlint init` may pin the action to]. */
6
+ const TABLE: readonly (readonly [string | undefined, string | null])[] = [
7
+ ["1.2.3", "v1.2.3"],
8
+ ["1.0.0", "v1.0.0"],
9
+ ["10.20.30", "v10.20.30"],
10
+ // Guards: a git checkout / `npm link` carries the unreleased placeholder — there is no tag for it.
11
+ ["0.0.0-semantically-released", null],
12
+ // Guards: only a plain release is known to have a tag; anything else stays the placeholder.
13
+ ["1.2.3-beta.1", null],
14
+ ["v1.2.3", null],
15
+ ["1.2", null],
16
+ ["", null],
17
+ ["latest", null],
18
+ [" 1.2.3", null],
19
+ // Guards: the CLI could not read its own manifest — no version, no pin.
20
+ [undefined, null],
21
+ ];
22
+
23
+ for (const [version, ref] of TABLE)
24
+ test(`actionRef(${JSON.stringify(version)})`, () => {
25
+ assert.equal(actionRef(version), ref);
26
+ });
@@ -0,0 +1,15 @@
1
+ /**
2
+ * The ref `paperlint init` pins the GitHub Action to, decided from the version of the package that is
3
+ * running. semantic-release tags every release `v<version>` on the commit it publishes, so the
4
+ * action at that tag is the same code as the installed package.
5
+ *
6
+ * Only a plain `X.Y.Z` has such a tag. A git checkout or `npm link` carries the unreleased
7
+ * placeholder `0.0.0-semantically-released`, a version that could not be read is `undefined`, and
8
+ * anything else is not known to be tagged: those return `null` ("no pin known"), and the caller
9
+ * keeps the placeholder. A wrong tag written confidently is worse than an obvious placeholder.
10
+ */
11
+ export function actionRef(version: string | undefined): string | null {
12
+ return version !== undefined && /^\d+\.\d+\.\d+$/.test(version)
13
+ ? `v${version}`
14
+ : null;
15
+ }
@@ -0,0 +1,63 @@
1
+ /**
2
+ * `failure.ts` — the one place a sentence about a banal failure is written, and the probe that
3
+ * accepts a banal by a measurement.
4
+ */
5
+ import assert from "node:assert/strict";
6
+ import { test } from "vitest";
7
+ import { describe, describeLine, PERL_MISSING } from "./failure.ts";
8
+ import { acceptProbe } from "./probe.ts";
9
+
10
+ test("describe: perl missing carries the fix", () => {
11
+ assert.deepEqual(describe({ kind: "perl-missing" }), [PERL_MISSING]);
12
+ assert.match(PERL_MISSING, /apt-get install perl/);
13
+ });
14
+
15
+ test("describe: an explicit $BANAL that is not there says so, and nothing else is suggested", () => {
16
+ // Guards: an explicit choice that is wrong is named, not replaced by "run paperlint toolchain".
17
+ const [line] = describe({
18
+ kind: "banal-missing",
19
+ missing: { kind: "explicit-not-found", path: "/x/banal" },
20
+ });
21
+ assert.equal(
22
+ line,
23
+ "banal not found: $BANAL names /x/banal, which does not exist",
24
+ );
25
+ });
26
+
27
+ test("describe: not installed names where it was looked for and the fix", () => {
28
+ const [line] = describe({
29
+ kind: "banal-missing",
30
+ missing: { kind: "not-installed", installed: "/c/banal" },
31
+ });
32
+ assert.equal(
33
+ line,
34
+ "banal not found: /c/banal — `npx paperlint toolchain` installs it",
35
+ );
36
+ });
37
+
38
+ test("describe: a sha256 mismatch shows both hashes on their own lines", () => {
39
+ const lines = describe({
40
+ kind: "sha-mismatch",
41
+ url: "https://x/banal",
42
+ expected: "a".repeat(64),
43
+ got: "b".repeat(64),
44
+ });
45
+ assert.equal(lines.length, 3);
46
+ assert.match(lines[0], /does not have the pinned sha256/);
47
+ assert.equal(lines[1], `expected ${"a".repeat(64)}`);
48
+ });
49
+
50
+ test("describeLine: a nonzero exit with no stderr says so", () => {
51
+ assert.equal(
52
+ describeLine({ kind: "process-failed", status: 3, stderrHead: "" }),
53
+ "banal failed (exit 3): no output",
54
+ );
55
+ });
56
+
57
+ test("acceptProbe: one page and a body size is accepted; anything else is `probe-rejected`", () => {
58
+ assert.ok(acceptProbe({ pages: [{}], bodyfontsize: 10.3 }).ok);
59
+ // Guards: acceptance is a MEASUREMENT — a banal that runs and measures nothing is refused.
60
+ const r = acceptProbe({ pages: [{}], bodyfontsize: null });
61
+ assert.equal(!r.ok && r.error.kind, "probe-rejected");
62
+ assert.equal(acceptProbe({ pages: [], bodyfontsize: 10 }).ok, false);
63
+ });
@@ -0,0 +1,118 @@
1
+ /**
2
+ * Every way getting a measurement from banal can fail, as ONE union — and `describe`, the only
3
+ * place a sentence about it is written. Callers branch on `kind`; nobody parses a message.
4
+ */
5
+ import type { Lines } from "../../domain/text.ts";
6
+ import type { BanalMissing } from "./locate.ts";
7
+
8
+ export type { BanalMissing } from "./locate.ts";
9
+
10
+ export type BanalFailure =
11
+ | { readonly kind: "perl-missing" }
12
+ | { readonly kind: "banal-missing"; readonly missing: BanalMissing }
13
+ | {
14
+ readonly kind: "process-failed";
15
+ readonly status: number;
16
+ readonly stderrHead: string;
17
+ }
18
+ | {
19
+ readonly kind: "signalled";
20
+ readonly signal: string;
21
+ readonly stderrHead: string;
22
+ }
23
+ | { readonly kind: "spawn-failed"; readonly message: string }
24
+ | { readonly kind: "timed-out"; readonly afterMs: number }
25
+ | { readonly kind: "no-json"; readonly head: string }
26
+ /** banal's own `{"error": true}` — printed with exit 0 for an input it could not read. */
27
+ | { readonly kind: "banal-error"; readonly stderrHead: string }
28
+ | { readonly kind: "unexpected-shape"; readonly issues: readonly string[] }
29
+ | { readonly kind: "probe-rejected"; readonly got: unknown }
30
+ | {
31
+ readonly kind: "download-failed";
32
+ readonly url: string;
33
+ readonly detail: string;
34
+ }
35
+ | {
36
+ readonly kind: "sha-mismatch";
37
+ readonly url: string;
38
+ readonly expected: string;
39
+ readonly got: string;
40
+ }
41
+ /** The installed file is there, and is not the pin. */
42
+ | { readonly kind: "not-pinned"; readonly path: string }
43
+ /** The installed, pinned banal did not measure the probe page. */
44
+ | {
45
+ readonly kind: "does-not-run";
46
+ readonly path: string;
47
+ readonly why: BanalFailure;
48
+ };
49
+
50
+ export const PERL_MISSING =
51
+ "perl is not installed — banal, the page-geometry script HotCRP runs, is a Perl program. " +
52
+ "Install perl (Debian/Ubuntu: apt-get install perl; macOS ships it) and run `npx paperlint toolchain`";
53
+
54
+ function missing(m: BanalMissing): string {
55
+ return m.kind === "explicit-not-found"
56
+ ? `banal not found: $BANAL names ${m.path}, which does not exist`
57
+ : `banal not found: ${m.installed} — \`npx paperlint toolchain\` installs it`;
58
+ }
59
+
60
+ type Kind = BanalFailure["kind"];
61
+ /** One sentence-writer per variant. A mapped type over `kind`, so a new variant without one is a compile error. */
62
+ type Describers = {
63
+ readonly [K in Kind]: (f: Extract<BanalFailure, { kind: K }>) => Lines;
64
+ };
65
+
66
+ const orNone = (s: string): string => s || "no output";
67
+
68
+ const DESCRIBE: Describers = {
69
+ "perl-missing": () => [PERL_MISSING],
70
+ "banal-missing": (f) => [missing(f.missing)],
71
+ "process-failed": (f) => [
72
+ `banal failed (exit ${String(f.status)}): ${orNone(f.stderrHead)}`,
73
+ ],
74
+ signalled: (f) => [`banal failed (${f.signal}): ${orNone(f.stderrHead)}`],
75
+ "spawn-failed": (f) => [`banal failed: ${f.message}`],
76
+ "timed-out": (f) => [`banal failed: no answer after ${String(f.afterMs)} ms`],
77
+ "no-json": (f) => [`banal printed no JSON: ${f.head || "nothing"}`],
78
+ "banal-error": (f) => [
79
+ `banal could not read the banal input XML: ${f.stderrHead}`,
80
+ ],
81
+ "unexpected-shape": (f) => [
82
+ `banal printed JSON that is not a measurement: ${f.issues.join("; ")}`,
83
+ ],
84
+ "probe-rejected": (f) => [
85
+ `banal ran on a one-page probe but measured nothing: ${JSON.stringify(f.got)}`,
86
+ ],
87
+ "download-failed": (f) => [
88
+ `could not download banal from ${f.url}: ${f.detail}`,
89
+ ],
90
+ "sha-mismatch": (f) => [
91
+ `banal from ${f.url} does not have the pinned sha256 — refusing to install it`,
92
+ `expected ${f.expected}`,
93
+ `got ${f.got}`,
94
+ ],
95
+ "not-pinned": (f) => [
96
+ `banal in ${f.path} is not the pinned one (sha256 differs)`,
97
+ ],
98
+ "does-not-run": (f) => [
99
+ `banal in ${f.path} does not run: ${describeLine(f.why)}`,
100
+ ],
101
+ };
102
+
103
+ /**
104
+ * What to tell a person. Most failures are one line; a sha256 mismatch shows both hashes.
105
+ *
106
+ * The one assertion below narrows the table's entry for `f.kind` to a function of `f`: the mapped
107
+ * type already pairs each kind with its own variant, and TypeScript cannot correlate the index with
108
+ * the argument on its own.
109
+ */
110
+ export function describe(f: BanalFailure): Lines {
111
+ const write = DESCRIBE[f.kind] as (f: BanalFailure) => Lines;
112
+ return write(f);
113
+ }
114
+
115
+ /** `describe` as one line, for a caller that has a single line to fill. */
116
+ export function describeLine(f: BanalFailure): string {
117
+ return describe(f).join("; ");
118
+ }
@@ -0,0 +1,119 @@
1
+ /**
2
+ * `adapters/banal/index.ts` — banal as the two ports the app sees. What `run.test.ts` checks in
3
+ * banal's own terms is checked here as the domain receives it: a `Geometry` whose provenance names
4
+ * banal and the rule that found it, a reason that is one line of text, a `Ready` that says what was
5
+ * verified. In-memory ports throughout; the real banal is `test/e2e/banal.mjs`.
6
+ */
7
+ import assert from "node:assert/strict";
8
+ import { test } from "vitest";
9
+ import { whyNoGeometry } from "../../domain/geometry.ts";
10
+ import type { AbsolutePath } from "../../domain/paths.ts";
11
+ import type { ProcessExit } from "../../ports/process.ts";
12
+ import { sha256Hex } from "../../domain/sha256.ts";
13
+ import {
14
+ exitedWith,
15
+ fixedDownload,
16
+ memoryFiles,
17
+ memoryPorts,
18
+ scriptedProcess,
19
+ } from "../memory/index.ts";
20
+ import type { Ready } from "../../ports/tool-installer.ts";
21
+ import { banalInstaller, banalMeasurer, parseBanalSettings } from "./index.ts";
22
+
23
+ const dirs = { home: "/h", tmp: "/t", cwd: "/r" };
24
+ const project = "/r" as AbsolutePath;
25
+ const MEASURED =
26
+ '{"papersize":[792,612],"columns":2,"bodyfontsize":9,"pages":[{},{"type":"bib","reffontsize":7}]}';
27
+
28
+ /** banal's measurer over in-memory ports: `/own/banal` on "disk", banal answering `exit`. */
29
+ function measurer(env: Record<string, string>, exit?: ProcessExit) {
30
+ const run = scriptedProcess(() => exit ?? exitedWith(MEASURED));
31
+ const io = memoryPorts({ run, files: memoryFiles({ "/own/banal": "" }) });
32
+ return { run, m: banalMeasurer(io, parseBanalSettings(env, dirs), project) };
33
+ }
34
+
35
+ test("no banal anywhere: unmeasured, and the reason says banal was not found", () => {
36
+ const g = measurer({}).m.measure([]);
37
+ assert.match(
38
+ g.kind === "unmeasured" ? whyNoGeometry(g) : "",
39
+ /^banal not found: /,
40
+ );
41
+ });
42
+
43
+ test("with $BANAL: banal gets the .xml and a quoted $PDFTOHTML, and the geometry is the domain's", () => {
44
+ const { m, run } = measurer({ BANAL: "/own/banal" });
45
+ const g = m.measure([]);
46
+ assert.equal(g.kind, "measured");
47
+ assert.deepEqual(g.kind === "measured" && g.by, {
48
+ tool: "banal",
49
+ path: "/own/banal",
50
+ how: "$BANAL",
51
+ });
52
+ assert.deepEqual(
53
+ g.kind === "measured" && [g.geometry.columns, g.geometry.bodyPages],
54
+ [2, 1],
55
+ );
56
+ const c = run.calls[0];
57
+ // Guards: the poppler-free path — banal is handed rpp's XML, never the PDF.
58
+ assert.match(c?.args.at(-1) ?? "", /\.xml$/);
59
+ assert.match(c?.env["PDFTOHTML"] ?? "", /^'.*pdftohtml'$/);
60
+ });
61
+
62
+ test("a failing banal: unmeasured, and the reason names exit, message and source", () => {
63
+ const { m } = measurer(
64
+ { BANAL: "/own/banal" },
65
+ { kind: "exited", status: 3, stdout: "", stderr: "boom\n" },
66
+ );
67
+ const g = m.measure([]);
68
+ // Guards: a banal that runs and fails is not reported as "not found".
69
+ assert.match(
70
+ g.kind === "unmeasured" ? whyNoGeometry(g) : "",
71
+ /^banal failed \(exit 3\): boom \(banal from \$BANAL: \/own\/banal\)$/,
72
+ );
73
+ });
74
+
75
+ // ── the installer ───────────────────────────────────────────────────────────────────────
76
+ const BODY = "print qq(...);\n";
77
+ /** What banal prints for the one-page probe — what acceptance requires. */
78
+ const PROBE = '{"bodyfontsize": 10.3, "columns": 1, "pages": [{}]}';
79
+ const source = {
80
+ url: "https://example.test/banal",
81
+ sha256: sha256Hex(new TextEncoder().encode(BODY)),
82
+ };
83
+
84
+ test("installer: ensure reports the download, and Ready says where and what was verified", () => {
85
+ const io = memoryPorts({
86
+ run: scriptedProcess((c) =>
87
+ c.args[0] === "-e" ? exitedWith("") : exitedWith(PROBE),
88
+ ),
89
+ download: fixedDownload(BODY),
90
+ });
91
+ const i = banalInstaller(io, parseBanalSettings({}, dirs), source);
92
+ assert.match(i.label, /^banal \d/);
93
+ const told: string[] = [];
94
+ const r = i.ensure((l) => told.push(l));
95
+ assert.ok(r.ok);
96
+ assert.equal(r.value.fresh, true);
97
+ assert.match(r.value.where, /\/banal$/);
98
+ assert.equal(
99
+ r.value.verified,
100
+ "sha256 verified, and it measured a probe page",
101
+ );
102
+ assert.equal(told.length, 1);
103
+ assert.equal(i.check().ok, true);
104
+ });
105
+
106
+ test("installer: a failure is lines of text, the first saying what happened", () => {
107
+ const io = memoryPorts({
108
+ run: scriptedProcess(() => ({ kind: "not-found", file: "perl" })),
109
+ });
110
+ const r = banalInstaller(io, parseBanalSettings({}, dirs), source).check();
111
+ assert.ok(!r.ok);
112
+ assert.match(r.error[0], /perl/);
113
+ });
114
+
115
+ test("a Ready cannot be written by hand — only an installer mints one", () => {
116
+ // @ts-expect-error — the brand: a literal is not a Ready (tsc -p tsconfig.test.json checks this line).
117
+ const forged: Ready = { where: "/x", fresh: true, verified: "trust me" };
118
+ assert.equal(forged.where, "/x");
119
+ });
@@ -0,0 +1,100 @@
1
+ /**
2
+ * banal's adapter — its ENTRY POINT. Everything under `src/adapters/banal/` exists because banal
3
+ * (HotCRP's page-geometry script, GPL, run as a separate program — see `./run.ts` for the licence
4
+ * boundary) exists; nothing outside this folder names it. What the rest of paperlint sees is two ports:
5
+ *
6
+ * `banalMeasurer` — `MeasureGeometry`: the page geometry of a paper's text boxes, or why none.
7
+ * `banalInstaller` — `ToolInstaller`: `paperlint toolchain`'s banal, pinned, verified and running.
8
+ *
9
+ * Both are composites over the generic ports a composition root builds (processes, files, scratch
10
+ * directories, downloads), so this file is pure; its settings are parsed from the environment value
11
+ * the root read (`parseBanalSettings`).
12
+ */
13
+ import type { Geometry, Provenance } from "../../domain/geometry.ts";
14
+ import type { AbsolutePath } from "../../domain/paths.ts";
15
+ import { err, ok } from "../../domain/result.ts";
16
+ import type { MeasureGeometry } from "../../ports/measure-geometry.ts";
17
+ import type { Ready, ToolInstaller } from "../../ports/tool-installer.ts";
18
+ import { describe } from "./failure.ts";
19
+ import { provenanceLabel, type BanalCandidate } from "./locate.ts";
20
+ import { pinLabel, type BanalSource } from "./pin.ts";
21
+ import {
22
+ checkBanal,
23
+ ensureBanal,
24
+ measureGeometry,
25
+ type BanalDeps,
26
+ type Installed,
27
+ type InstallDeps,
28
+ } from "./run.ts";
29
+ import type { BanalSettings } from "./settings.ts";
30
+
31
+ export {
32
+ BANAL_DIR_ENV,
33
+ BANAL_ENV,
34
+ parseBanalSettings,
35
+ type BanalSettings,
36
+ } from "./settings.ts";
37
+ export type { BanalDeps, InstallDeps } from "./run.ts";
38
+ export type { BanalSource } from "./pin.ts";
39
+
40
+ /** The domain's provenance for a banal: what the facts file writes, and which rule found it. */
41
+ const provenance = (c: BanalCandidate): Provenance => ({
42
+ tool: "banal",
43
+ path: c.path,
44
+ how: provenanceLabel(c.provenance),
45
+ });
46
+
47
+ /** banal as the `MeasureGeometry` port: `$BANAL`, the project's `vendor/banal`, then the cache. */
48
+ export function banalMeasurer(
49
+ d: BanalDeps,
50
+ s: BanalSettings,
51
+ projectRoot: AbsolutePath,
52
+ ): MeasureGeometry {
53
+ return {
54
+ measure(pages): Geometry {
55
+ const g = measureGeometry(d, s, projectRoot, pages);
56
+ return g.source === "banal"
57
+ ? { kind: "measured", by: provenance(g.by), geometry: g.geometry }
58
+ : {
59
+ kind: "unmeasured",
60
+ why: describe(g.why),
61
+ tried: g.tried ? provenance(g.tried) : null,
62
+ };
63
+ },
64
+ };
65
+ }
66
+
67
+ /** The one place banal's `Ready` is minted: from an `Installed`, whose `PinnedBanal` only run.ts mints. */
68
+ const ready = (i: Installed): Ready =>
69
+ ({
70
+ where: i.banal.path as string,
71
+ fresh: i.fresh,
72
+ verified: "sha256 verified, and it measured a probe page",
73
+ }) as Ready;
74
+
75
+ /**
76
+ * banal as the `ToolInstaller` port: download at the pin, check the sha256, accept by measuring a
77
+ * probe page. `source` is the pin unless a test passes a local file.
78
+ */
79
+ export function banalInstaller(
80
+ d: InstallDeps,
81
+ s: BanalSettings,
82
+ source?: BanalSource,
83
+ ): ToolInstaller {
84
+ return {
85
+ label: pinLabel(),
86
+ ensure(report) {
87
+ const r = ensureBanal(d, s, {
88
+ ...(source ? { source } : {}),
89
+ onDownload: report,
90
+ });
91
+ return r.ok ? ok(ready(r.value)) : err(describe(r.error));
92
+ },
93
+ check() {
94
+ const r = checkBanal(d, s, source);
95
+ return r.ok
96
+ ? ok(ready({ banal: r.value, fresh: false }))
97
+ : err(describe(r.error));
98
+ },
99
+ };
100
+ }
@@ -0,0 +1,20 @@
1
+ import assert from "node:assert/strict";
2
+ import { test } from "vitest";
3
+ import { sha256Hex } from "../../domain/sha256.ts";
4
+ import { installedState, verifyPin } from "./install.ts";
5
+
6
+ const bytes = new TextEncoder().encode("banal");
7
+ const source = { url: "file:///x", sha256: sha256Hex(bytes) };
8
+
9
+ test("verifyPin: the pinned bytes pass; others are refused with both hashes", () => {
10
+ assert.ok(verifyPin(bytes, source).ok);
11
+ const r = verifyPin(new TextEncoder().encode("other"), source);
12
+ assert.equal(!r.ok && r.error.expected, source.sha256);
13
+ });
14
+
15
+ test("installedState: absent, other bytes, pinned — one predicate for install and --check", () => {
16
+ assert.equal(installedState(null, source).kind, "absent");
17
+ // Guards: the pin is checked on every run — a changed file is not trusted.
18
+ assert.equal(installedState(new Uint8Array([1]), source).kind, "other-bytes");
19
+ assert.equal(installedState(bytes, source).kind, "pinned");
20
+ });
@@ -0,0 +1,41 @@
1
+ /**
2
+ * The install decisions, on bytes: is what is on disk the pin, and may downloaded bytes be kept.
3
+ * One predicate for `paperlint toolchain` and `paperlint toolchain --check`, so the two cannot disagree.
4
+ */
5
+ import type { Opaque } from "ts-essentials";
6
+ import { err, ok, type Result } from "../../domain/result.ts";
7
+ import { sha256Hex, type Sha256 } from "../../domain/sha256.ts";
8
+ import type { BanalSource } from "./pin.ts";
9
+
10
+ /** Bytes that hash to the pin. Minted by `verifyPin` only; the installer writes nothing else. */
11
+ export type PinnedBytes = Opaque<Uint8Array, "PinnedBytes">;
12
+
13
+ export interface ShaMismatch {
14
+ readonly expected: Sha256;
15
+ readonly got: Sha256;
16
+ }
17
+
18
+ export function verifyPin(
19
+ bytes: Uint8Array,
20
+ source: BanalSource,
21
+ ): Result<PinnedBytes, ShaMismatch> {
22
+ const got = sha256Hex(bytes);
23
+ return got === source.sha256
24
+ ? ok(bytes as PinnedBytes)
25
+ : err({ expected: source.sha256, got });
26
+ }
27
+
28
+ /** What is installed, judged against the pin. */
29
+ export type InstalledState =
30
+ | { readonly kind: "absent" }
31
+ | { readonly kind: "other-bytes"; readonly mismatch: ShaMismatch }
32
+ | { readonly kind: "pinned" };
33
+
34
+ export function installedState(
35
+ current: Uint8Array | null,
36
+ source: BanalSource,
37
+ ): InstalledState {
38
+ if (current === null) return { kind: "absent" };
39
+ const v = verifyPin(current, source);
40
+ return v.ok ? { kind: "pinned" } : { kind: "other-bytes", mismatch: v.error };
41
+ }