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,58 @@
1
+ /**
2
+ * The `Workspace` contract against a real temp directory: the directory exists inside the scope and
3
+ * is gone after it, whether the scope returned or threw; `write` puts a file in it, runnable when
4
+ * asked. What banal stages through it is `src/adapters/banal/invocation.test.ts`.
5
+ */
6
+ import assert from "node:assert/strict";
7
+ import { spawnSync } from "node:child_process";
8
+ import {
9
+ existsSync,
10
+ mkdirSync,
11
+ mkdtempSync,
12
+ readFileSync,
13
+ realpathSync,
14
+ rmSync,
15
+ statSync,
16
+ } from "node:fs";
17
+ import { tmpdir } from "node:os";
18
+ import { join } from "node:path";
19
+ import { afterAll as after, test } from "vitest";
20
+ import { tmpWorkspace } from "./workspace.io.ts";
21
+
22
+ const root = realpathSync(mkdtempSync(join(tmpdir(), "rpp-ws-test-")));
23
+ after(() => rmSync(root, { recursive: true, force: true }));
24
+
25
+ test("the directory exists inside the scope and is gone after it", () => {
26
+ const ws = tmpWorkspace(root);
27
+ const dir = ws.within("rpp-banal-", (s) => {
28
+ assert.ok(existsSync(s.dir));
29
+ return s.dir;
30
+ });
31
+ // Guards: cleanup — a paper's text must not pile up in the temp directory, one copy per build.
32
+ assert.equal(existsSync(dir), false);
33
+ });
34
+
35
+ test("the directory is gone after a throw too", () => {
36
+ let seen = "";
37
+ assert.throws(() =>
38
+ tmpWorkspace(root).within("rpp-banal-", (s) => {
39
+ seen = s.dir;
40
+ throw new Error("boom");
41
+ }),
42
+ );
43
+ assert.equal(existsSync(seen), false);
44
+ });
45
+
46
+ test("🔴 write: the file lands in the scope's directory, and `exec` makes it runnable — from a path with a space and a quote", () => {
47
+ const awkward = join(root, "it's a dir");
48
+ mkdirSync(awkward);
49
+ tmpWorkspace(awkward).within("rpp-ws-", (s) => {
50
+ const plain = s.write("data.txt", "hello", "read");
51
+ assert.equal(readFileSync(plain, "utf8"), "hello");
52
+ const script = s.write("run", "#!/bin/sh\necho ran\n", "exec");
53
+ // Guards: the exec bit — banal runs the staged stub as a program, and a 0644 file is refused.
54
+ const r = spawnSync(script, [], { encoding: "utf8" });
55
+ assert.equal(r.stdout, "ran\n");
56
+ assert.equal(statSync(plain).mode & 0o111, 0);
57
+ });
58
+ });
@@ -0,0 +1,111 @@
1
+ /**
2
+ * `fill.ts` — what is recovered from pdf.js's exports for each text item: whether it is upright, and
3
+ * the fill it was drawn in (the operator-list walk). Operator lists here are written by hand; the
4
+ * real-PDF half runs in `pdf-facts.harness.mjs` and `test/e2e/banal.mjs`.
5
+ */
6
+ import assert from "node:assert/strict";
7
+ import { test } from "vitest";
8
+ import { isDeepStrictEqual } from "node:util";
9
+ import * as L from "./fill.ts";
10
+ import type { OperatorList } from "./fill.ts";
11
+
12
+ /** One vitest case per assertion, named by its label. */
13
+ const check = (label: string, cond: unknown, detail = "") => {
14
+ test(label, () => {
15
+ assert.ok(cond, detail ? `${label} — ${detail}` : label);
16
+ });
17
+ };
18
+
19
+ // ── upright ─────────────────────────────────────────────────────────────────────────────
20
+ check("isUpright: the identity scaled", L.isUpright([10, 0, 0, 10, 5, 5]));
21
+ check(
22
+ "isUpright: mirrored vertically is still upright",
23
+ L.isUpright([10, 0, 0, -10, 5, 5]),
24
+ );
25
+ check("isUpright: rotated 90° is not", !L.isUpright([0, 10, -10, 0, 5, 5]));
26
+ check("isUpright: skewed is not", !L.isUpright([10, 0, 3, 10, 5, 5]));
27
+
28
+ // ── the fill walk ───────────────────────────────────────────────────────────────────────
29
+ const OPS = {
30
+ save: 1,
31
+ restore: 2,
32
+ setFillRGBColor: 3,
33
+ setTextRenderingMode: 4,
34
+ showText: 5,
35
+ showSpacedText: 6,
36
+ };
37
+ const glyphs = (s: string) => [[...s].map((unicode) => ({ unicode }))];
38
+ const ops = (...pairs: [number, unknown?][]): OperatorList => ({
39
+ fnArray: pairs.map((p) => p[0]),
40
+ argsArray: pairs.map((p) => p[1] ?? null),
41
+ });
42
+
43
+ check(
44
+ "hexOf: current pdf.js passes a hex string",
45
+ L.hexOf(["#E5E5E5"]) === "#e5e5e5",
46
+ );
47
+ check(
48
+ "hexOf: older pdf.js passes three numbers",
49
+ L.hexOf([229, 229, 229]) === "#e5e5e5",
50
+ );
51
+ {
52
+ const list = ops(
53
+ [OPS.save],
54
+ [OPS.setFillRGBColor, ["#f5f5f5"]],
55
+ [OPS.showText, glyphs("Light text")],
56
+ [OPS.restore],
57
+ [OPS.showText, glyphs("Dark")],
58
+ [OPS.setTextRenderingMode, [3]],
59
+ [OPS.showSpacedText, [[{ unicode: "O" }, -250, { unicode: "CR" }]]],
60
+ [OPS.setTextRenderingMode, [0]],
61
+ [OPS.showText, glyphs("After")],
62
+ );
63
+ const fills = L.fillsOf(OPS, list, ["Light text", "Dark", "OCR", "After"]);
64
+ // Guards: save/restore — the colour set inside a group ends with it; the next item is not light.
65
+ check(
66
+ "fillsOf: a colour set inside save/restore ends at restore",
67
+ isDeepStrictEqual(fills.slice(0, 2), [
68
+ { kind: "rgb", hex: "#f5f5f5" },
69
+ { kind: "unknown" },
70
+ ]),
71
+ JSON.stringify(fills),
72
+ );
73
+ // Guards: render mode 3 is invisible, and the mode is state, not a property of one operator.
74
+ check(
75
+ "fillsOf: render mode 3 is invisible, and mode 0 ends it",
76
+ fills[2]?.kind === "invisible" && fills[3]?.kind === "unknown",
77
+ JSON.stringify(fills),
78
+ );
79
+ }
80
+ {
81
+ const list = ops(
82
+ [OPS.setFillRGBColor, ["#ff0000"]],
83
+ [OPS.showText, glyphs("Hel")],
84
+ [OPS.showText, glyphs("lo world")],
85
+ );
86
+ check(
87
+ "fillsOf: an item drawn by two operators, and spaces that items and glyphs split differently",
88
+ L.fillsOf(OPS, list, ["Hello", "world"]).every(
89
+ (f) => f.kind === "rgb" && f.hex === "#ff0000",
90
+ ),
91
+ );
92
+ }
93
+ {
94
+ const list = ops(
95
+ [OPS.setFillRGBColor, ["#eeeeee"]],
96
+ [OPS.showText, glyphs("first")],
97
+ [OPS.showText, glyphs("second")],
98
+ );
99
+ const fills = L.fillsOf(OPS, list, ["nowhere", "second"]);
100
+ // Guards: the direction of a miss — an item that cannot be found is drawn as black (kept), and
101
+ // the walk does not lose its place for the items after it.
102
+ check(
103
+ "fillsOf: an unmatched item is `unknown` (kept, never dropped), and the next one still matches",
104
+ fills[0]?.kind === "unknown" && fills[1]?.kind === "rgb",
105
+ JSON.stringify(fills),
106
+ );
107
+ }
108
+ check(
109
+ "fillsOf: an empty item is `unknown`",
110
+ L.fillsOf(OPS, ops(), [" "])[0]?.kind === "unknown",
111
+ );
@@ -0,0 +1,141 @@
1
+ /**
2
+ * pdf.js's half of the page layout — PURE. pdf.js's text content carries no colour and no
3
+ * orientation flag, so two facts about each text item are recovered from what pdf.js exports:
4
+ * whether its transform draws it upright, and which fill its characters were drawn with (walked
5
+ * from the operator list). `pdf-facts.ts` reads the PDF and calls these; the result is the domain's
6
+ * `PageLayout` (`domain/page-layout.ts`). Split out of the banal XML writer (#75): this is what
7
+ * would change if pdf.js did — not banal.
8
+ */
9
+ import type { Fill } from "../../domain/page-layout.ts";
10
+
11
+ /** A transform `[a, b, c, d, e, f]` that draws text upright: no rotation and no skew. */
12
+ export function isUpright(m: readonly number[]): boolean {
13
+ const eps = 1e-6;
14
+ return Math.abs(m[1] ?? 0) < eps && Math.abs(m[2] ?? 0) < eps;
15
+ }
16
+
17
+ /** The operator codes the colour walk reads — pdf.js's `OPS`, reduced to what is used. */
18
+ export interface ColourOps {
19
+ readonly save: number;
20
+ readonly restore: number;
21
+ readonly setFillRGBColor: number;
22
+ readonly setTextRenderingMode: number;
23
+ readonly showText: number;
24
+ readonly showSpacedText: number;
25
+ }
26
+
27
+ /** pdf.js's operator list: parallel arrays of operator codes and their arguments. */
28
+ export interface OperatorList {
29
+ readonly fnArray: readonly number[];
30
+ readonly argsArray: readonly unknown[];
31
+ }
32
+
33
+ /** Text as the walk compares it: normalised, with no whitespace (items and glyphs split spaces differently). */
34
+ const squeeze = (s: string): string => s.normalize("NFKC").replace(/\s+/g, "");
35
+
36
+ /** `setFillRGBColor`'s argument: a `#rrggbb` string in current pdf.js, three 0-255 numbers in older ones. */
37
+ export function hexOf(args: unknown): string {
38
+ const a = Array.isArray(args) ? (args as unknown[]) : [];
39
+ if (typeof a[0] === "string") return a[0].toLowerCase();
40
+ const byte = (v: unknown): string =>
41
+ Math.round(typeof v === "number" ? v : 0)
42
+ .toString(16)
43
+ .padStart(2, "0");
44
+ return `#${byte(a[0])}${byte(a[1])}${byte(a[2])}`;
45
+ }
46
+
47
+ /** The characters a show-text operator draws, from its glyph objects. */
48
+ function glyphText(args: unknown): string {
49
+ const a = Array.isArray(args) ? (args as unknown[]) : [];
50
+ const glyphs = Array.isArray(a[0]) ? (a[0] as unknown[]) : [];
51
+ return glyphs
52
+ .map((g) =>
53
+ typeof g === "object" && g !== null && "unicode" in g
54
+ ? String((g as { unicode: unknown }).unicode ?? "")
55
+ : "",
56
+ )
57
+ .join("");
58
+ }
59
+
60
+ /** The graphics state the walk tracks. */
61
+ interface State {
62
+ fill: Fill;
63
+ mode: number;
64
+ }
65
+
66
+ /** Apply one state operator; false when `fn` is not one. */
67
+ function applyState(
68
+ ops: ColourOps,
69
+ fn: number,
70
+ args: unknown,
71
+ s: { cur: State; stack: State[] },
72
+ ): boolean {
73
+ if (fn === ops.save) s.stack.push({ ...s.cur });
74
+ else if (fn === ops.restore)
75
+ s.cur = s.stack.pop() ?? { fill: { kind: "unknown" }, mode: 0 };
76
+ else if (fn === ops.setFillRGBColor)
77
+ s.cur.fill = { kind: "rgb", hex: hexOf(args) };
78
+ else if (fn === ops.setTextRenderingMode)
79
+ s.cur.mode = Number((args as unknown[] | undefined)?.[0] ?? 0);
80
+ else return false;
81
+ return true;
82
+ }
83
+
84
+ /** Every drawn character, in content order, with the fill it was drawn in. */
85
+ export function drawnCharacters(
86
+ ops: ColourOps,
87
+ list: OperatorList,
88
+ ): { chars: string[]; fills: Fill[] } {
89
+ const s = { cur: { fill: { kind: "unknown" } as Fill, mode: 0 }, stack: [] };
90
+ const chars: string[] = [];
91
+ const fills: Fill[] = [];
92
+ list.fnArray.forEach((fn, i) => {
93
+ const args = list.argsArray[i];
94
+ if (applyState(ops, fn, args, s)) return;
95
+ if (fn !== ops.showText && fn !== ops.showSpacedText) return;
96
+ const fill: Fill = s.cur.mode === 3 ? { kind: "invisible" } : s.cur.fill;
97
+ for (const ch of squeeze(glyphText(args))) {
98
+ chars.push(ch);
99
+ fills.push(fill);
100
+ }
101
+ });
102
+ return { chars, fills };
103
+ }
104
+
105
+ /** How far ahead the walk looks for an item it cannot find where it expects it. */
106
+ export const LOOKAHEAD = 4000;
107
+
108
+ /** The position at or after `from` where `want` starts in `chars`, or -1 within the look-ahead. */
109
+ function findRun(
110
+ chars: readonly string[],
111
+ want: readonly string[],
112
+ from: number,
113
+ ): number {
114
+ const limit = Math.min(chars.length, from + LOOKAHEAD);
115
+ for (let q = from; q < limit; q++)
116
+ if (want.every((ch, k) => chars[q + k] === ch)) return q;
117
+ return -1;
118
+ }
119
+
120
+ /**
121
+ * The fill of each text item. pdf.js's text content carries no colour, so the operator list is
122
+ * walked for the characters each show-text operator draws, and the items — in the same content
123
+ * order — are matched against them. An item that cannot be matched gets `unknown` (drawn as black):
124
+ * a missed match can keep light text, never drop dark text.
125
+ */
126
+ export function fillsOf(
127
+ ops: ColourOps,
128
+ list: OperatorList,
129
+ items: readonly string[],
130
+ ): Fill[] {
131
+ const { chars, fills } = drawnCharacters(ops, list);
132
+ let at = 0;
133
+ return items.map((text): Fill => {
134
+ const want = [...squeeze(text)];
135
+ if (want.length === 0) return { kind: "unknown" };
136
+ const q = findRun(chars, want, at);
137
+ if (q < 0) return { kind: "unknown" };
138
+ at = q + want.length;
139
+ return fills[q] ?? { kind: "unknown" };
140
+ });
141
+ }
@@ -0,0 +1,314 @@
1
+ /**
2
+ * `build-engine.ts` — `paperlint build`'s engine step: facts gathered from a cache on disk and a PATH,
3
+ * the decision acted on. The TeX Lives here are directories with a `pdflatex` file and a fake
4
+ * runner standing in for their `kpsewhich`; the question and the install are injected.
5
+ *
6
+ * Tables, in order:
7
+ * 1. the pure texts: the question, the refusal line, what counts as "yes";
8
+ * 2. prepareEngine: use the cache · use the system · refuse without a human · ask, decline,
9
+ * accept and install · dry-run never asks or installs.
10
+ */
11
+ import assert from "node:assert/strict";
12
+ import {
13
+ chmodSync,
14
+ mkdirSync,
15
+ mkdtempSync,
16
+ realpathSync,
17
+ rmSync,
18
+ writeFileSync,
19
+ } from "node:fs";
20
+ import { tmpdir } from "node:os";
21
+ import { basename, delimiter, dirname, join } from "node:path";
22
+ import { fileURLToPath } from "node:url";
23
+
24
+ const HERE = dirname(fileURLToPath(import.meta.url));
25
+ const E = await import(join(HERE, "build-engine.ts"));
26
+
27
+ let n = 0;
28
+ const check = (label, cond, detail = "") => {
29
+ assert.ok(cond, `${label}${detail ? ` — ${detail}` : ""}`);
30
+ n++;
31
+ };
32
+
33
+ // ── 1. texts ────────────────────────────────────────────────────────────────────────────
34
+ const noCache = {
35
+ supported: true,
36
+ cache: null,
37
+ system: null,
38
+ interactive: true,
39
+ required: ["acmart", "libertine"],
40
+ };
41
+ check(
42
+ "question: no TeX Live at all — size, time, and where it goes",
43
+ E.question(noCache, "/c") ===
44
+ "TeX Live is not installed (needed to compile paper.tex, ~230 MB, ~2 min). Install it now into /c? [Y/n] ",
45
+ );
46
+ check(
47
+ "question: a partial cache — only its gaps are offered",
48
+ E.question(
49
+ { ...noCache, cache: { label: "c", bin: "/c/b", missing: ["kastrup"] } },
50
+ "/c",
51
+ ) === "TeX Live in /c lacks 1 package(s): kastrup. Install them now? [Y/n] ",
52
+ );
53
+ const refused = E.refusal(
54
+ { kind: "refuse", reason: "not-installed", missing: ["acmart", "libertine"] },
55
+ {
56
+ ...noCache,
57
+ interactive: false,
58
+ system: { label: "s", bin: "/usr/bin", missing: ["cm-super"] },
59
+ },
60
+ );
61
+ // Guards: the one line a CI log shows — without `npx paperlint toolchain` in it, the reader does not know
62
+ // the cure.
63
+ check(
64
+ "refusal: ONE line naming `npx paperlint toolchain`, the missing packages, and why the system TeX was passed over",
65
+ !refused.includes("\n") &&
66
+ refused.includes("run `npx paperlint toolchain`") &&
67
+ refused.includes("missing: 2 package(s): acmart, libertine") &&
68
+ refused.includes("/usr/bin/pdflatex lacks: cm-super"),
69
+ refused,
70
+ );
71
+ check(
72
+ "refusal on an unsupported platform says so",
73
+ E.refusal(
74
+ { kind: "refuse", reason: "unsupported", missing: ["x"] },
75
+ noCache,
76
+ ).includes("Windows is not supported"),
77
+ );
78
+ for (const [a, yes] of [
79
+ // Guards: the default answer — [Y/n] promises that Enter installs.
80
+ ["", true],
81
+ ["y", true],
82
+ ["YES", true],
83
+ [" y ", true],
84
+ ["n", false],
85
+ ["no", false],
86
+ ["maybe", false],
87
+ [null, false],
88
+ ])
89
+ check(`isYes(${JSON.stringify(a)}) is ${yes}`, E.isYes(a) === yes);
90
+
91
+ // ── 2. prepareEngine ────────────────────────────────────────────────────────────────────
92
+ const work = realpathSync(mkdtempSync(join(tmpdir(), "rpp-engine-h-")));
93
+ const TEX = {
94
+ packages: { acmart: ["acmart.cls"], libertine: ["libertine.sty"] },
95
+ tools: {},
96
+ };
97
+ const ALL = "/t/acmart.cls\n/t/libertine.sty\n";
98
+ /**
99
+ * A TeX Live on disk: a bin directory with a RUNNABLE pdflatex file. The execute bit is load-bearing:
100
+ * a pdflatex that cannot be started is not a TeX Live (`isExecutable` in engine.ts).
101
+ */
102
+ const texAt = (dir) => {
103
+ mkdirSync(dir, { recursive: true });
104
+ writeFileSync(join(dir, "pdflatex"), "");
105
+ chmodSync(join(dir, "pdflatex"), 0o755);
106
+ return dir;
107
+ };
108
+ const cacheBin = texAt(join(work, "cache", "2026", "bin", "x86_64-linux"));
109
+ const sysBin = texAt(join(work, "usr", "bin"));
110
+ const emptyBin = join(work, "nothing");
111
+ mkdirSync(emptyBin);
112
+
113
+ /** kpsewhich answers per tree: `found[bin]` is what it prints. */
114
+ const fakeRun = (found) => (cmd) => ({
115
+ stdout: found[dirname(cmd)] ?? "",
116
+ status: 0,
117
+ });
118
+ const go = async (o) => {
119
+ const out = [];
120
+ const err = [];
121
+ const asked = [];
122
+ let installs = 0;
123
+ const r = await E.prepareEngine({
124
+ tex: TEX,
125
+ log: (l) => out.push(l),
126
+ err: (l) => err.push(l),
127
+ ask: async (q) => {
128
+ asked.push(q);
129
+ return o.answer ?? "";
130
+ },
131
+ install: () => {
132
+ installs++;
133
+ return o.onInstall ? o.onInstall() : { ok: false };
134
+ },
135
+ platform: "linux",
136
+ ...o,
137
+ });
138
+ return {
139
+ r,
140
+ out: out.join("\n"),
141
+ err: err.join("\n"),
142
+ asked,
143
+ installs: () => installs,
144
+ };
145
+ };
146
+ const envWith = (cacheRoot, path) => ({
147
+ RPP_TEXLIVE_DIR: cacheRoot,
148
+ PATH: path,
149
+ });
150
+
151
+ {
152
+ const x = await go({
153
+ env: envWith(join(work, "cache"), sysBin),
154
+ run: fakeRun({ [cacheBin]: ALL, [sysBin]: ALL }),
155
+ });
156
+ // Guards: the cache being USED — without its bin first on PATH, the build runs whatever pdflatex
157
+ // the machine has.
158
+ check(
159
+ "a complete cache: used, named, and PATH starts with its bin",
160
+ x.r.ok &&
161
+ x.out.includes("engine: TeX Live 2026 — paperlint cache") &&
162
+ x.r.env.PATH.split(delimiter)[0] === cacheBin,
163
+ `${x.out} ${JSON.stringify(x.r)}`,
164
+ );
165
+ }
166
+ {
167
+ // Two TeX Live years in the cache: a 2027 install that stopped after install-tl, and 2026.
168
+ const years = join(work, "years");
169
+ const old = texAt(join(years, "2026", "bin", "x86_64-linux"));
170
+ const cut = texAt(join(years, "2027", "bin", "x86_64-linux"));
171
+ const x = await go({
172
+ env: envWith(years, emptyBin),
173
+ run: fakeRun({ [old]: ALL, [cut]: "" }),
174
+ });
175
+ // Guards: the year choice — an interrupted new-year install must not stand in for a complete
176
+ // older one.
177
+ check(
178
+ "🔴 two years, the newer incomplete: the newest COMPLETE one builds",
179
+ x.r.ok &&
180
+ x.out.includes("engine: TeX Live 2026 — paperlint cache") &&
181
+ x.r.env.PATH.split(delimiter)[0] === old,
182
+ x.out,
183
+ );
184
+ const y = await go({
185
+ env: envWith(years, emptyBin),
186
+ run: fakeRun({ [old]: ALL, [cut]: ALL }),
187
+ });
188
+ check(
189
+ "two years, both complete: the newer one",
190
+ y.r.ok && y.r.env.PATH.split(delimiter)[0] === cut,
191
+ y.out,
192
+ );
193
+ }
194
+ {
195
+ const x = await go({
196
+ env: envWith(join(work, "cache"), sysBin),
197
+ run: fakeRun({ [cacheBin]: "/t/acmart.cls\n", [sysBin]: ALL }),
198
+ });
199
+ check(
200
+ "a partial cache, a complete system TeX: the system one, PATH untouched",
201
+ x.r.ok &&
202
+ x.out.includes(
203
+ `engine: TeX Live on PATH (${join(sysBin, "pdflatex")})`,
204
+ ) &&
205
+ x.r.env.PATH === sysBin,
206
+ x.out,
207
+ );
208
+ }
209
+ {
210
+ const x = await go({
211
+ env: envWith(join(work, "none"), `${emptyBin}${delimiter}${sysBin}`),
212
+ run: fakeRun({ [sysBin]: "/t/acmart.cls\n" }),
213
+ interactive: false,
214
+ });
215
+ // Guards: refusing without a terminal — CI and agents would otherwise block on a question nobody
216
+ // can answer.
217
+ check(
218
+ "🔴 no human, nothing qualifies: refused in one line, NOTHING asked, NOTHING installed",
219
+ !x.r.ok &&
220
+ x.r.code === 1 &&
221
+ x.err.includes("npx paperlint toolchain") &&
222
+ x.err.includes("libertine") &&
223
+ x.asked.length === 0 &&
224
+ x.installs() === 0,
225
+ x.err,
226
+ );
227
+ }
228
+ {
229
+ const x = await go({
230
+ env: envWith(join(work, "none"), emptyBin),
231
+ run: fakeRun({}),
232
+ interactive: true,
233
+ answer: "n",
234
+ });
235
+ check(
236
+ "a terminal, declined: asked ONCE, nothing installed, refusal printed",
237
+ !x.r.ok &&
238
+ x.asked.length === 1 &&
239
+ x.installs() === 0 &&
240
+ x.err.includes("npx paperlint toolchain"),
241
+ JSON.stringify(x),
242
+ );
243
+ }
244
+ {
245
+ const x = await go({
246
+ env: envWith(join(work, "none"), emptyBin),
247
+ run: fakeRun({}),
248
+ interactive: true,
249
+ ask: async () => {
250
+ throw new Error("Aborted with Ctrl+D");
251
+ },
252
+ });
253
+ check(
254
+ "a question that cannot be answered (Ctrl+D) is a no, not a crash",
255
+ !x.r.ok && x.installs() === 0,
256
+ );
257
+ }
258
+ // The accept path needs the fake kpsewhich to answer for a tree that appears DURING the run.
259
+ const x_run = { found: {} };
260
+ {
261
+ const fresh = join(work, "fresh2");
262
+ const x = await go({
263
+ env: envWith(fresh, emptyBin),
264
+ run: (cmd) => ({ stdout: x_run.found[dirname(cmd)] ?? "", status: 0 }),
265
+ interactive: true,
266
+ answer: "",
267
+ onInstall: () => {
268
+ const bin = texAt(join(fresh, "2026", "bin", "x86_64-linux"));
269
+ x_run.found[bin] = ALL;
270
+ return {
271
+ ok: true,
272
+ tree: { dir: dirname(dirname(bin)), year: "2026", bin },
273
+ };
274
+ },
275
+ });
276
+ check(
277
+ "a terminal, Enter (default yes): installs once, then builds with the cache",
278
+ x.r.ok &&
279
+ x.installs() === 1 &&
280
+ x.out.includes("engine: TeX Live 2026 — paperlint cache") &&
281
+ basename(x.r.env.PATH.split(delimiter)[0]) === "x86_64-linux",
282
+ `${x.out}\n${x.err}`,
283
+ );
284
+ }
285
+ {
286
+ const x = await go({
287
+ env: envWith(join(work, "none"), emptyBin),
288
+ run: fakeRun({}),
289
+ interactive: true,
290
+ answer: "y",
291
+ onInstall: () => ({ ok: false }),
292
+ });
293
+ check("an install that fails stops the build", !x.r.ok && x.installs() === 1);
294
+ }
295
+ {
296
+ const x = await go({
297
+ env: envWith(join(work, "none"), emptyBin),
298
+ run: fakeRun({}),
299
+ interactive: true,
300
+ dryRun: true,
301
+ });
302
+ // Guards: --dry-run as a plan without side effects.
303
+ check(
304
+ "--dry-run: prints what would stop the build, never asks, never installs",
305
+ x.r.ok &&
306
+ x.asked.length === 0 &&
307
+ x.installs() === 0 &&
308
+ x.out.includes("engine: none — a real run would stop here"),
309
+ x.out,
310
+ );
311
+ }
312
+
313
+ rmSync(work, { recursive: true, force: true });
314
+ console.log(`build-engine: ${n} checks passed`);