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,191 @@
1
+ /**
2
+ * `adapters/banal/run.ts` — running and installing banal, on in-memory ports: which `Command` it builds, what it stages,
3
+ * what it downloads and writes, and what it concludes. No perl, no disk, no network: the adapters
4
+ * record, and each assertion reads the record. Real perl and the real banal are `test/e2e/banal.mjs`;
5
+ * the adapters' own behaviour is `src/adapters/node/*.test.ts`.
6
+ */
7
+ import assert from "node:assert/strict";
8
+ import { test } from "vitest";
9
+ import {
10
+ exitedWith,
11
+ fixedDownload,
12
+ memoryFiles,
13
+ memoryPorts,
14
+ memoryWorkspace,
15
+ scriptedProcess,
16
+ } from "../memory/index.ts";
17
+ import { checkBanal, ensureBanal, measureGeometry } from "./run.ts";
18
+ import { sha256Hex } from "../../domain/sha256.ts";
19
+ import { installedBanal } from "./locate.ts";
20
+ import { parseBanalSettings } from "./settings.ts";
21
+ import type { AbsolutePath } from "../../domain/paths.ts";
22
+ import type { Command, ProcessExit } from "../../ports/process.ts";
23
+
24
+ const s = parseBanalSettings(
25
+ { BANAL: "/own/banal", PATH: "/bin" },
26
+ { home: "/h", tmp: "/t", cwd: "/w" },
27
+ );
28
+ const noExplicit = parseBanalSettings(
29
+ { PATH: "/bin" },
30
+ { home: "/h", tmp: "/t", cwd: "/w" },
31
+ );
32
+ const project = "/p" as AbsolutePath;
33
+ /** What banal prints for the one-page probe. */
34
+ const MEASURED = '{"bodyfontsize": 10.3, "columns": 1, "pages": [{}]}';
35
+ const banalAnswers = (e: ProcessExit) =>
36
+ scriptedProcess((c: Command) => (c.args[0] === "-e" ? exitedWith("") : e));
37
+
38
+ test("measureGeometry: perl runs the found banal on the staged XML, and the geometry comes back", () => {
39
+ const run = banalAnswers(exitedWith(MEASURED));
40
+ const workspace = memoryWorkspace();
41
+ const io = memoryPorts({
42
+ run,
43
+ workspace,
44
+ files: memoryFiles({ "/own/banal": "" }),
45
+ });
46
+ const g = measureGeometry(io, s, project, []);
47
+ assert.equal(g.source, "banal");
48
+ assert.equal(g.source === "banal" && g.geometry.bodyPt, 10.3);
49
+ const [c] = run.calls;
50
+ assert.equal(c?.file, "perl");
51
+ assert.equal(c?.args.at(-1), "/scratch/paper.xml");
52
+ // Guards: both files staged, once, and the scratch scope ended normally (cleanup ran).
53
+ assert.deepEqual(
54
+ workspace.written.map((w) => [w.name, w.mode]),
55
+ [
56
+ ["paper.xml", "read"],
57
+ ["pdftohtml", "exec"],
58
+ ],
59
+ );
60
+ assert.deepEqual(workspace.ended, ["returned"]);
61
+ });
62
+
63
+ test("measureGeometry: no banal anywhere is `banal-missing`, and nothing runs", () => {
64
+ const run = banalAnswers(exitedWith(MEASURED));
65
+ const g = measureGeometry(memoryPorts({ run }), noExplicit, project, []);
66
+ assert.equal(g.source === "none" && g.why.kind, "banal-missing");
67
+ assert.equal(run.calls.length, 0);
68
+ });
69
+
70
+ test("🔴 measureGeometry: perl not found is `perl-missing`, naming the banal that was tried", () => {
71
+ const io = memoryPorts({
72
+ run: scriptedProcess(() => ({ kind: "not-found", file: "perl" })),
73
+ files: memoryFiles({ "/own/banal": "" }),
74
+ });
75
+ const g = measureGeometry(io, s, project, []);
76
+ assert.equal(g.source === "none" && g.why.kind, "perl-missing");
77
+ assert.equal(g.source === "none" && g.tried?.path, "/own/banal");
78
+ });
79
+
80
+ test("measureGeometry: banal exiting 3 is `process-failed`", () => {
81
+ const io = memoryPorts({
82
+ run: banalAnswers({
83
+ kind: "exited",
84
+ status: 3,
85
+ stdout: "",
86
+ stderr: "boom",
87
+ }),
88
+ files: memoryFiles({ "/own/banal": "" }),
89
+ });
90
+ const g = measureGeometry(io, s, project, []);
91
+ assert.equal(g.source === "none" && g.why.kind, "process-failed");
92
+ });
93
+
94
+ // ── installing ──────────────────────────────────────────────────────────────────────────
95
+ const BODY = "print qq(...);\n";
96
+ const source = {
97
+ url: "https://example.test/banal",
98
+ sha256: sha256Hex(new TextEncoder().encode(BODY)),
99
+ };
100
+ const dest = installedBanal(noExplicit);
101
+
102
+ test("ensureBanal: downloads, verifies, writes and probes — fresh; a second run downloads nothing", () => {
103
+ const files = memoryFiles();
104
+ const download = fixedDownload(BODY);
105
+ const io = memoryPorts({
106
+ run: banalAnswers(exitedWith(MEASURED)),
107
+ files,
108
+ download,
109
+ });
110
+ const first = ensureBanal(io, noExplicit, { source });
111
+ assert.equal(first.ok && first.value.banal.path, dest);
112
+ assert.equal(first.ok && first.value.fresh, true);
113
+ assert.ok(files.map.has(dest));
114
+ // Guards: idempotence — an installed, matching banal is not downloaded again.
115
+ const second = ensureBanal(io, noExplicit, { source });
116
+ assert.equal(second.ok && second.value.fresh, false);
117
+ assert.equal(download.urls.length, 1);
118
+ });
119
+
120
+ test("🔴 ensureBanal: bytes with another sha256 are refused, and nothing is written", () => {
121
+ const files = memoryFiles();
122
+ const io = memoryPorts({
123
+ run: banalAnswers(exitedWith(MEASURED)),
124
+ files,
125
+ download: fixedDownload("something else"),
126
+ });
127
+ const r = ensureBanal(io, noExplicit, { source });
128
+ // Guards: the sha256 pin — a changed upstream file (or a proxy's error page) is never installed.
129
+ assert.equal(!r.ok && r.error.kind, "sha-mismatch");
130
+ assert.equal(files.map.size, 0);
131
+ });
132
+
133
+ test("ensureBanal: a failed download names the URL", () => {
134
+ const io = memoryPorts({
135
+ run: banalAnswers(exitedWith(MEASURED)),
136
+ download: fixedDownload({ detail: "curl exited 22" }),
137
+ });
138
+ const r = ensureBanal(io, noExplicit, { source });
139
+ assert.deepEqual(!r.ok && r.error, {
140
+ kind: "download-failed",
141
+ url: source.url,
142
+ detail: "curl exited 22",
143
+ });
144
+ });
145
+
146
+ test("🔴 ensureBanal: a banal that downloads fine but measures nothing fails, saying why", () => {
147
+ const io = memoryPorts({
148
+ run: banalAnswers(exitedWith("not json")),
149
+ download: fixedDownload(BODY),
150
+ });
151
+ const r = ensureBanal(io, noExplicit, { source });
152
+ // Guards: acceptance by a run, not by the download.
153
+ assert.equal(!r.ok && r.error.kind, "does-not-run");
154
+ assert.equal(
155
+ !r.ok && r.error.kind === "does-not-run" && r.error.why.kind,
156
+ "no-json",
157
+ );
158
+ });
159
+
160
+ test("🔴 ensureBanal and checkBanal without perl: `perl-missing`, and nothing downloaded", () => {
161
+ const download = fixedDownload(BODY);
162
+ const io = memoryPorts({
163
+ run: scriptedProcess(() => ({ kind: "not-found", file: "perl" })),
164
+ download,
165
+ });
166
+ const r = ensureBanal(io, noExplicit, { source });
167
+ // Guards: perl first — no download when the program that would run it is missing.
168
+ assert.equal(!r.ok && r.error.kind, "perl-missing");
169
+ assert.equal(download.urls.length, 0);
170
+ const c = checkBanal(io, noExplicit, source);
171
+ assert.equal(!c.ok && c.error.kind, "perl-missing");
172
+ });
173
+
174
+ test("checkBanal: absent, other bytes, pinned-and-running", () => {
175
+ const run = banalAnswers(exitedWith(MEASURED));
176
+ const absent = checkBanal(memoryPorts({ run }), noExplicit, source);
177
+ assert.equal(!absent.ok && absent.error.kind, "banal-missing");
178
+ // Guards: the pin is checked on every run — a changed file is reported, not trusted.
179
+ const tampered = checkBanal(
180
+ memoryPorts({ run, files: memoryFiles({ [dest]: "tampered" }) }),
181
+ noExplicit,
182
+ source,
183
+ );
184
+ assert.equal(!tampered.ok && tampered.error.kind, "not-pinned");
185
+ const good = checkBanal(
186
+ memoryPorts({ run, files: memoryFiles({ [dest]: BODY }) }),
187
+ noExplicit,
188
+ source,
189
+ );
190
+ assert.equal(good.ok, true);
191
+ });
@@ -0,0 +1,244 @@
1
+ /**
2
+ * BANAL — the page-geometry script HotCRP's format checker runs — fetched, verified and run.
3
+ *
4
+ * banal (Eddie Kohler, Geoffrey M. Voelker; https://github.com/kohler/hotcrp/blob/master/src/banal)
5
+ * measures a paper's page size, column count, body and reference font sizes and page types. paperlint
6
+ * runs the REAL banal, unmodified, so the numbers in the facts file are the numbers HotCRP shows a
7
+ * reviewer at upload — a reimplementation would drift from them.
8
+ *
9
+ * ── THE LICENCE BOUNDARY ────────────────────────────────────────────────────────
10
+ * banal is GPL-2.0-or-later; paperlint is MIT. paperlint therefore never contains banal: `paperlint toolchain`
11
+ * DOWNLOADS it from HotCRP at a pinned commit, checks its sha256, and stores it in rpp's cache, and
12
+ * paperlint EXECUTES it as a separate program (`perl banal …`), reading its JSON output. Nothing of banal
13
+ * is copied, vendored, linked or translated into this package.
14
+ *
15
+ * ── WITHOUT POPPLER ──────────────────────────────────────────────────────────────
16
+ * banal reads a PDF by running poppler's `pdftohtml -xml` — but it also accepts that XML directly:
17
+ * `banal_open_input` (banal 1.2, line 1815) takes a file ending in `.xml` and opens it as is. paperlint
18
+ * writes that XML itself from the page layout pdf.js read (`./xml.ts`) and hands banal the `.xml` file,
19
+ * beside a stub that answers banal's one question to poppler (`./invocation.ts`).
20
+ *
21
+ * ── THIS FILE ───────────────────────────────────────────────────────────────────
22
+ * Running and installing banal, over the generic ports (`src/ports/`) and this folder's pure
23
+ * modules; it imports nothing from `node:*` that touches the outside world. Failures stay banal's
24
+ * own union here, so the tests can branch on them; `./index.ts` turns them into the ports the app
25
+ * sees (`MeasureGeometry`, `ToolInstaller`).
26
+ */
27
+ import type { AbsolutePath } from "../../domain/paths.ts";
28
+ import type { PageGeometry } from "../../domain/geometry.ts";
29
+ import type { Download } from "../../ports/download.ts";
30
+ import type { Files } from "../../ports/files.ts";
31
+ import type { RunProcess } from "../../ports/process.ts";
32
+ import type { Workspace } from "../../ports/workspace.ts";
33
+ import { andThen, err, ok, type Result } from "../../domain/result.ts";
34
+ import type { BanalFailure } from "./failure.ts";
35
+ import {
36
+ banalCommand,
37
+ BANAL_RUN_MS,
38
+ stage,
39
+ stageBanalInput,
40
+ } from "./invocation.ts";
41
+ import { installedState, verifyPin } from "./install.ts";
42
+ import {
43
+ installedBanal,
44
+ lookupOrder,
45
+ pickBanal,
46
+ type BanalCandidate,
47
+ type LocatedBanal,
48
+ type PinnedBanal,
49
+ } from "./locate.ts";
50
+ import {
51
+ geometryOf,
52
+ parseBanalOutput,
53
+ type BanalMeasurement,
54
+ } from "./output.ts";
55
+ import { BANAL_PIN, pinLabel, type BanalSource } from "./pin.ts";
56
+ import { acceptProbe, PROBE_PAGE } from "./probe.ts";
57
+ import type { BanalSettings } from "./settings.ts";
58
+ import type { PageLayout } from "../../domain/page-layout.ts";
59
+
60
+ /** Seconds: banal is ~90 KB. */
61
+ export const BANAL_DOWNLOAD_SECONDS = 60;
62
+
63
+ /** The ports banal runs with. */
64
+ export interface BanalDeps {
65
+ readonly run: RunProcess;
66
+ readonly files: Files;
67
+ readonly workspace: Workspace;
68
+ }
69
+
70
+ /** The ports installing banal needs besides: it downloads. */
71
+ export interface InstallDeps extends BanalDeps {
72
+ readonly download: Download;
73
+ }
74
+
75
+ /**
76
+ * What measuring came to, in banal's own terms: the banal found and its geometry, or banal's
77
+ * failure and the banal that was tried. `./index.ts` projects it onto the domain's `Geometry`.
78
+ */
79
+ export type BanalOutcome =
80
+ | {
81
+ readonly source: "banal";
82
+ readonly by: BanalCandidate;
83
+ readonly geometry: PageGeometry;
84
+ }
85
+ | {
86
+ readonly source: "none";
87
+ readonly why: BanalFailure;
88
+ /** The banal that was run and failed; null when none was found. */
89
+ readonly tried: BanalCandidate | null;
90
+ };
91
+
92
+ /** Run `banal` on `pages` in a scratch directory that is gone when this returns. */
93
+ function runBanal(
94
+ d: BanalDeps,
95
+ s: BanalSettings,
96
+ banal: LocatedBanal,
97
+ pages: readonly PageLayout[],
98
+ ): Result<BanalMeasurement, BanalFailure> {
99
+ return d.workspace.within("rpp-banal-", (scratch) =>
100
+ parseBanalOutput(
101
+ d.run.run(
102
+ banalCommand(
103
+ banal,
104
+ stage(scratch, stageBanalInput(pages)),
105
+ s.processEnv,
106
+ ),
107
+ ),
108
+ ),
109
+ );
110
+ }
111
+
112
+ /**
113
+ * The page geometry banal measures for `pages` — the banal found by `$BANAL`, the project's
114
+ * `vendor/banal`, or `paperlint toolchain`'s — or why there is none.
115
+ */
116
+ export function measureGeometry(
117
+ d: BanalDeps,
118
+ s: BanalSettings,
119
+ projectRoot: AbsolutePath,
120
+ pages: readonly PageLayout[],
121
+ ): BanalOutcome {
122
+ const located = pickBanal(lookupOrder(s, projectRoot), (p) =>
123
+ d.files.isFile(p),
124
+ );
125
+ if (!located.ok)
126
+ return {
127
+ source: "none",
128
+ why: { kind: "banal-missing", missing: located.error },
129
+ tried: null,
130
+ };
131
+ const by: BanalCandidate = located.value;
132
+ const r = runBanal(d, s, located.value, pages);
133
+ return r.ok
134
+ ? { source: "banal", by, geometry: geometryOf(r.value) }
135
+ : { source: "none", why: r.error, tried: by };
136
+ }
137
+
138
+ // ── installing it (`paperlint toolchain`) ──────────────────────────────────────────────────
139
+
140
+ /** Does perl start? banal is a Perl program; without perl nothing below can succeed. */
141
+ function perlRuns(d: BanalDeps, s: BanalSettings): boolean {
142
+ const r = d.run.run({
143
+ file: "perl",
144
+ args: ["-e", "exit 0"],
145
+ env: s.processEnv,
146
+ timeoutMs: BANAL_RUN_MS,
147
+ });
148
+ return r.kind === "exited" && r.status === 0;
149
+ }
150
+
151
+ /**
152
+ * Does the installed, pin-verified banal RUN — accepted by measuring the probe page, not by a
153
+ * download's exit code. The one place a `PinnedBanal` is minted; callers verified the bytes first.
154
+ */
155
+ function probe(
156
+ d: BanalDeps,
157
+ s: BanalSettings,
158
+ path: AbsolutePath,
159
+ ): Result<PinnedBanal, BanalFailure> {
160
+ const at: BanalCandidate = { path, provenance: { kind: "cache" } };
161
+ const r = andThen(
162
+ runBanal(d, s, at as LocatedBanal, [PROBE_PAGE]),
163
+ acceptProbe,
164
+ );
165
+ return r.ok
166
+ ? ok(at as PinnedBanal)
167
+ : err({ kind: "does-not-run", path, why: r.error });
168
+ }
169
+
170
+ /** Download `source` into `dest`, keeping the bytes only when they hash to the pin. */
171
+ function install(
172
+ d: InstallDeps,
173
+ source: BanalSource,
174
+ dest: AbsolutePath,
175
+ ): Result<null, BanalFailure> {
176
+ const got = d.download.fetch(source.url, BANAL_DOWNLOAD_SECONDS * 1000);
177
+ if (!got.ok)
178
+ return err({
179
+ kind: "download-failed",
180
+ url: source.url,
181
+ detail: got.error.detail,
182
+ });
183
+ const pinned = verifyPin(got.value, source);
184
+ if (!pinned.ok)
185
+ return err({ kind: "sha-mismatch", url: source.url, ...pinned.error });
186
+ d.files.writeAtomic(dest, pinned.value);
187
+ return ok(null);
188
+ }
189
+
190
+ export interface Installed {
191
+ readonly banal: PinnedBanal;
192
+ /** True when this run downloaded it. */
193
+ readonly fresh: boolean;
194
+ }
195
+
196
+ export interface EnsureOptions {
197
+ /** What to download — the pin by default; a test passes a local file. */
198
+ readonly source?: BanalSource;
199
+ /** Told before a download starts (it can take a while). */
200
+ readonly onDownload?: (line: string) => void;
201
+ }
202
+
203
+ /**
204
+ * Make `paperlint toolchain`'s banal present, byte-identical to the pin, and RUNNING: perl is checked
205
+ * first, a file with the wrong sha256 is replaced, and acceptance is a probe run that has to measure
206
+ * a page — not the download's exit code.
207
+ */
208
+ export function ensureBanal(
209
+ d: InstallDeps,
210
+ s: BanalSettings,
211
+ o: EnsureOptions = {},
212
+ ): Result<Installed, BanalFailure> {
213
+ if (!perlRuns(d, s)) return err({ kind: "perl-missing" });
214
+ const source = o.source ?? BANAL_PIN;
215
+ const dest = installedBanal(s);
216
+ const present =
217
+ installedState(d.files.readBytes(dest), source).kind === "pinned";
218
+ if (!present) {
219
+ o.onDownload?.(` downloading ${pinLabel()}…`);
220
+ const done = install(d, source, dest);
221
+ if (!done.ok) return done;
222
+ }
223
+ const ran = probe(d, s, dest);
224
+ return ran.ok ? ok({ banal: ran.value, fresh: !present }) : ran;
225
+ }
226
+
227
+ /** `--check`: is the pinned banal installed and running? The same predicate as `ensureBanal`, no download. */
228
+ export function checkBanal(
229
+ d: BanalDeps,
230
+ s: BanalSettings,
231
+ source: BanalSource = BANAL_PIN,
232
+ ): Result<PinnedBanal, BanalFailure> {
233
+ if (!perlRuns(d, s)) return err({ kind: "perl-missing" });
234
+ const dest = installedBanal(s);
235
+ const state = installedState(d.files.readBytes(dest), source);
236
+ if (state.kind === "absent")
237
+ return err({
238
+ kind: "banal-missing",
239
+ missing: { kind: "not-installed", installed: dest },
240
+ });
241
+ if (state.kind === "other-bytes")
242
+ return err({ kind: "not-pinned", path: dest });
243
+ return probe(d, s, dest);
244
+ }
@@ -0,0 +1,31 @@
1
+ import assert from "node:assert/strict";
2
+ import { test } from "vitest";
3
+ import { parseBanalSettings } from "./settings.ts";
4
+
5
+ const dirs = { home: "/h", tmp: "/t", cwd: "/w" };
6
+
7
+ test("cacheDir: $RPP_BANAL_DIR, else $XDG_CACHE_HOME/rpp/banal, else ~/.cache/rpp/banal", () => {
8
+ assert.equal(
9
+ parseBanalSettings({ RPP_BANAL_DIR: "/x" }, dirs).cacheDir,
10
+ "/x",
11
+ );
12
+ assert.equal(
13
+ parseBanalSettings({ XDG_CACHE_HOME: "/c" }, dirs).cacheDir,
14
+ "/c/rpp/banal",
15
+ );
16
+ assert.equal(parseBanalSettings({}, dirs).cacheDir, "/h/.cache/rpp/banal");
17
+ });
18
+
19
+ test("an empty variable is unset, and a relative one is relative to the cwd the root read", () => {
20
+ const s = parseBanalSettings({ BANAL: "b/banal", RPP_BANAL_DIR: "" }, dirs);
21
+ assert.equal(s.explicit, "/w/b/banal");
22
+ assert.equal(s.cacheDir, "/h/.cache/rpp/banal");
23
+ assert.equal(parseBanalSettings({}, dirs).explicit, null);
24
+ });
25
+
26
+ test("processEnv drops undefined values: a child's environment is strings only", () => {
27
+ assert.deepEqual(
28
+ parseBanalSettings({ A: "1", B: undefined }, dirs).processEnv,
29
+ { A: "1" },
30
+ );
31
+ });
@@ -0,0 +1,55 @@
1
+ /**
2
+ * banal's settings, parsed from the environment VALUE the composition root read once
3
+ * (`domain/host.ts`): which banal the user named, where `paperlint toolchain` keeps it, where scratch
4
+ * directories go, and the environment perl and curl get. Nothing here reads the host itself.
5
+ */
6
+ import { join, resolve } from "node:path";
7
+ import {
8
+ childEnv,
9
+ type Environment,
10
+ type HostDirs,
11
+ } from "../../domain/host.ts";
12
+ import type { AbsolutePath } from "../../domain/paths.ts";
13
+
14
+ /** An explicit banal to use instead of rpp's (a path to the script). */
15
+ export const BANAL_ENV = "BANAL";
16
+ /** Where `paperlint toolchain` stores banal. Default: `$XDG_CACHE_HOME/rpp/banal`, else `~/.cache/rpp/banal`. */
17
+ export const BANAL_DIR_ENV = "RPP_BANAL_DIR";
18
+
19
+ export interface BanalSettings {
20
+ /** `$BANAL`: the user's explicit choice; null when unset. */
21
+ readonly explicit: AbsolutePath | null;
22
+ /** `$RPP_BANAL_DIR` › `$XDG_CACHE_HOME/rpp/banal` › `<home>/.cache/rpp/banal`. */
23
+ readonly cacheDir: AbsolutePath;
24
+ /** Where scratch directories go. */
25
+ readonly tmpDir: AbsolutePath;
26
+ /** The environment a child process (perl, curl) gets: every value a string, none undefined. */
27
+ readonly processEnv: Readonly<Record<string, string>>;
28
+ }
29
+
30
+ /** `resolve` with an absolute base never consults the process's cwd. */
31
+ const absolute = (cwd: string, p: string): AbsolutePath =>
32
+ resolve(cwd, p) as AbsolutePath;
33
+
34
+ /** An unset variable and an empty one mean the same: not set. */
35
+ const set = (v: string | undefined): string | null => (v ? v : null);
36
+
37
+ function cacheDirOf(env: Environment, d: HostDirs): string {
38
+ return (
39
+ set(env[BANAL_DIR_ENV]) ??
40
+ join(set(env["XDG_CACHE_HOME"]) ?? join(d.home, ".cache"), "rpp", "banal")
41
+ );
42
+ }
43
+
44
+ export function parseBanalSettings(
45
+ env: Environment,
46
+ dirs: HostDirs,
47
+ ): BanalSettings {
48
+ const explicit = set(env[BANAL_ENV]);
49
+ return {
50
+ explicit: explicit === null ? null : absolute(dirs.cwd, explicit),
51
+ cacheDir: absolute(dirs.cwd, cacheDirOf(env, dirs)),
52
+ tmpDir: absolute(dirs.cwd, dirs.tmp),
53
+ processEnv: childEnv(env),
54
+ };
55
+ }
@@ -0,0 +1,111 @@
1
+ /**
2
+ * `xml.ts` — the banal input XML (pdftohtml's dialect) the domain's text boxes become, and which of
3
+ * them a layout tool counts.
4
+ *
5
+ * Every page here is written by hand. The real-PDF half — pdf.js boxes of a committed fixture,
6
+ * written as XML, measured by the real banal and compared with banal on real pdftohtml — is
7
+ * `test/e2e/banal.mjs`, which runs where `paperlint toolchain` has installed banal.
8
+ */
9
+ import assert from "node:assert/strict";
10
+ import { test } from "vitest";
11
+ import * as L from "./xml.ts";
12
+ import type { PageLayout, TextBox } from "../../domain/page-layout.ts";
13
+
14
+ /** One vitest case per assertion, named by its label. */
15
+ const check = (label: string, cond: unknown, detail = "") => {
16
+ test(label, () => {
17
+ assert.ok(cond, detail ? `${label} — ${detail}` : label);
18
+ });
19
+ };
20
+
21
+ const box = (over: Partial<TextBox> = {}): TextBox => ({
22
+ top: 10,
23
+ left: 20,
24
+ width: 30,
25
+ height: 12,
26
+ size: 10,
27
+ font: "LinLibertineT",
28
+ text: "Hello",
29
+ upright: true,
30
+ fill: { kind: "unknown" },
31
+ ...over,
32
+ });
33
+ const page = (boxes: TextBox[], widthPt = 612, heightPt = 792): PageLayout => ({
34
+ widthPt,
35
+ heightPt,
36
+ boxes,
37
+ });
38
+
39
+ // ── the XML ─────────────────────────────────────────────────────────────────────────────
40
+ {
41
+ const xml = L.pdf2xml([
42
+ page([box(), box({ top: 22.0417, text: 'a < b & c > "d"' })]),
43
+ page([box({ text: "again" }), box({ size: 9, text: "smaller" })]),
44
+ ]);
45
+ // Guards: the exact dialect banal reads — header, zoom-3 integer page size, a font declared on the
46
+ // first page that uses it and before its text, sizes as round(size × 3), coordinates × 3 unrounded.
47
+ const expected = [
48
+ '<?xml version="1.0" encoding="UTF-8"?>',
49
+ '<!DOCTYPE pdf2xml SYSTEM "pdf2xml.dtd">',
50
+ '<pdf2xml producer="research-paper-pipeline (pdf.js)" version="24.02.0">',
51
+ '<page number="1" position="absolute" top="0" left="0" height="2376" width="1836">',
52
+ '\t<fontspec id="0" size="30" family="LinLibertineT" color="#000000"/>',
53
+ '<text top="30.000000" left="60.000000" width="90.000000" height="36.000000" font="0">Hello</text>',
54
+ '<text top="66.125100" left="60.000000" width="90.000000" height="36.000000" font="0">a &lt; b &amp; c &gt; &#34;d&#34;</text>',
55
+ "</page>",
56
+ '<page number="2" position="absolute" top="0" left="0" height="2376" width="1836">',
57
+ '\t<fontspec id="1" size="27" family="LinLibertineT" color="#000000"/>',
58
+ '<text top="30.000000" left="60.000000" width="90.000000" height="36.000000" font="0">again</text>',
59
+ '<text top="30.000000" left="60.000000" width="90.000000" height="36.000000" font="1">smaller</text>',
60
+ "</page>",
61
+ "</pdf2xml>",
62
+ "",
63
+ ].join("\n");
64
+ check(
65
+ "pdf2xml: the exact pdftohtml dialect, on two pages",
66
+ xml === expected,
67
+ `\n${xml}`,
68
+ );
69
+ }
70
+ {
71
+ const xml = L.pdf2xml([
72
+ page([
73
+ box({ text: "rotated", upright: false }),
74
+ box({ text: "ocr layer", fill: { kind: "invisible" } }),
75
+ box({ text: " " }),
76
+ box({ text: "watermark", fill: { kind: "rgb", hex: "#f5f5f5" } }),
77
+ box({ text: "body" }),
78
+ ]),
79
+ ]);
80
+ // Guards: condition 1 — pdftohtml writes rotated text with width 0 and banal drops it; writing it
81
+ // would count a rotated margin note as body text.
82
+ check("pdf2xml: rotated text is not written", !xml.includes("rotated"), xml);
83
+ // Guards: condition 2 — pdftohtml does not write render-mode-3 text (an OCR layer).
84
+ check(
85
+ "pdf2xml: invisible text is not written",
86
+ !xml.includes("ocr layer"),
87
+ xml,
88
+ );
89
+ // Guards: condition 3 — light text IS written, with its colour, because what counts as light is
90
+ // banal's decision (its own threshold), not a copy of it here.
91
+ check(
92
+ "pdf2xml: light text is written, with its colour in its own fontspec",
93
+ xml.includes('color="#f5f5f5"/>') &&
94
+ xml.includes(">watermark</text>") &&
95
+ xml.includes('color="#000000"/>'),
96
+ xml,
97
+ );
98
+ check(
99
+ "pdf2xml: a blank item is not written",
100
+ (xml.match(/<text /g) ?? []).length === 2,
101
+ xml,
102
+ );
103
+ }
104
+ check(
105
+ "xmlEscape: a newline becomes a space, as pdftohtml writes it",
106
+ L.xmlEscape("a\nb\r\nc") === "a b c",
107
+ );
108
+ check(
109
+ "pdf2xml: no pages is a well-formed empty document",
110
+ L.pdf2xml([]).endsWith("</pdf2xml>\n") && !L.pdf2xml([]).includes("<page"),
111
+ );