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,651 @@
1
+ /**
2
+ * `toolchain.ts` — the TeX Live installer, driven against a FAKE CTAN MIRROR ON DISK.
3
+ *
4
+ * 🔴 NOT ONE BYTE OF TeX LIVE IS DOWNLOADED HERE. The mirror is a directory served by `file://`
5
+ * URLs: the real `curl` fetches from it, the real `tar` lists and unpacks it, and the installer
6
+ * inside is `fixtures/toolchain-mirror/install-tl`, which lays out a tree whose `kpsewhich` and
7
+ * `tlmgr` answer from two text files. So what runs is rpp's own download, fallback, unpack,
8
+ * install and verify logic — the part that decides success — and what is faked is only TeX.
9
+ * The real TeX Live half is `test/e2e/toolchain.mjs`.
10
+ *
11
+ * Tables, in order:
12
+ * 1. the pure pieces (year, profile, tlmgr's unknown names, cache location);
13
+ * 2. the download: mirror fallback, archive check, the time limit;
14
+ * 3. the command: fresh install, second run is a no-op, --check, partial add, and the three
15
+ * ways an install must FAIL naming the package;
16
+ * 4. banal, the command's second half: a stand-in banal served from `file://`, never HotCRP —
17
+ * the installer's own tests are `banal.harness.mjs`; here only the wiring into the command.
18
+ */
19
+ import assert from "node:assert/strict";
20
+ import { spawnSync } from "node:child_process";
21
+ import { createHash } from "node:crypto";
22
+ import {
23
+ appendFileSync,
24
+ copyFileSync,
25
+ existsSync,
26
+ mkdirSync,
27
+ mkdtempSync,
28
+ readFileSync,
29
+ realpathSync,
30
+ rmSync,
31
+ writeFileSync,
32
+ } from "node:fs";
33
+ import { tmpdir } from "node:os";
34
+ import { dirname, join } from "node:path";
35
+ import { fileURLToPath, pathToFileURL } from "node:url";
36
+
37
+ const HERE = dirname(fileURLToPath(import.meta.url));
38
+ const FIXTURE = join(HERE, "..", "fixtures", "toolchain-mirror");
39
+ const T = await import(join(HERE, "toolchain.ts"));
40
+ // The harness is its own composition root for banal: `runToolchain` takes an installer, it does
41
+ // not build one.
42
+ const B = await import(join(HERE, "adapters", "banal", "index.ts"));
43
+ const N = await import(join(HERE, "adapters", "node", "index.ts"));
44
+ const C = await import(join(HERE, "adapters", "curl", "index.ts"));
45
+
46
+ let n = 0;
47
+ const check = (label, cond, detail = "") => {
48
+ assert.ok(cond, `${label}${detail ? ` — ${detail}` : ""}`);
49
+ n++;
50
+ };
51
+
52
+ // ── 1. pure pieces ──────────────────────────────────────────────────────────────────────
53
+ check(
54
+ "tlYear: the year from release-texlive.txt's first line",
55
+ T.tlYear("TeX Live (https://tug.org/texlive) version 2026\n\nmore") ===
56
+ "2026",
57
+ );
58
+ check(
59
+ "tlYear: a year on a LATER line is not the release",
60
+ T.tlYear("something else\nversion 2026") === null,
61
+ );
62
+ const profile = T.tlProfile("/c/2026");
63
+ check(
64
+ "tlProfile: scheme-basic, TEXDIR and every tree variable inside it, no docs, no sources",
65
+ profile.includes("selected_scheme scheme-basic\n") &&
66
+ profile.includes("TEXDIR /c/2026\n") &&
67
+ profile.includes("TEXMFHOME /c/2026/texmf-home\n") &&
68
+ profile.includes("tlpdbopt_install_docfiles 0\n") &&
69
+ profile.includes("tlpdbopt_install_srcfiles 0\n"),
70
+ profile,
71
+ );
72
+ check(
73
+ "unknownPackages: tlmgr's own wording names the package",
74
+ JSON.stringify(
75
+ T.unknownPackages(
76
+ "tlmgr install: package urw-base35 not present in repository.\npackage already present: acmart\n",
77
+ ),
78
+ ) === '["urw-base35"]',
79
+ );
80
+ check(
81
+ "cacheRoot: RPP_TEXLIVE_DIR wins",
82
+ T.cacheRoot({ RPP_TEXLIVE_DIR: "/ci/tl", XDG_CACHE_HOME: "/x" }, "/h") ===
83
+ "/ci/tl",
84
+ );
85
+ check(
86
+ "cacheRoot: then XDG_CACHE_HOME",
87
+ T.cacheRoot({ XDG_CACHE_HOME: "/x" }, "/h") === "/x/rpp/texlive",
88
+ );
89
+ check(
90
+ "cacheRoot: then ~/.cache",
91
+ T.cacheRoot({}, "/h") === "/h/.cache/rpp/texlive",
92
+ );
93
+ check(
94
+ "mirrorsFrom: RPP_CTAN_MIRROR is the ONLY mirror, trailing slash dropped",
95
+ JSON.stringify(T.mirrorsFrom({ RPP_CTAN_MIRROR: "https://m/tlnet/" })) ===
96
+ '["https://m/tlnet"]',
97
+ );
98
+ check(
99
+ "mirrorsFrom: several defaults, every one https",
100
+ T.mirrorsFrom({}).length > 1 &&
101
+ T.mirrorsFrom({}).every((m) => m.startsWith("https://")),
102
+ );
103
+ check("formatDuration", T.formatDuration(118_400) === "1m58s");
104
+
105
+ // tlmgr's two wordings for "the repository is another TeX Live year" (tlmgr.pl 7644-7648, 7655-7663).
106
+ const OLDER =
107
+ "tlmgr: Local TeX Live (2026) is older than remote repository (2027).\n" +
108
+ "Cross release updates are only supported with\n" +
109
+ " update-tlmgr-latest(.sh/.exe) -- --upgrade\n";
110
+ const INCOMPAT = (local, remote) =>
111
+ "tlmgr: The TeX Live versions of the local installation\n" +
112
+ "and the repository are not compatible:\n" +
113
+ ` local: ${local}\n` +
114
+ ` repository: ${remote} (https://m/tlnet)\n` +
115
+ "Perhaps that particular CTAN mirror is outdated? Just a guess.\n";
116
+ check(
117
+ "releaseGap: 'Local TeX Live (Y) is older than remote repository (Y)' — both years",
118
+ JSON.stringify(T.releaseGap(OLDER)) === '{"local":"2026","remote":"2027"}',
119
+ );
120
+ check(
121
+ "releaseGap: the 'not compatible' wording with a NEWER repository",
122
+ JSON.stringify(T.releaseGap(INCOMPAT("2026", "2027"))) ===
123
+ '{"local":"2026","remote":"2027"}',
124
+ );
125
+ // Guards: the direction of the year check — an outdated mirror would trigger a whole new install.
126
+ check(
127
+ "releaseGap: a STALE mirror (repository older than the tree) is not a new release",
128
+ T.releaseGap(INCOMPAT("2027", "2026")) === null,
129
+ );
130
+ check(
131
+ "releaseGap: ordinary tlmgr output is not a release gap",
132
+ T.releaseGap(
133
+ "tlmgr install: package urw-base35 not present in repository.\n",
134
+ ) === null,
135
+ );
136
+ // Guards: the complete-tree preference — an interrupted new-year install would otherwise be the
137
+ // tree the build uses.
138
+ check(
139
+ "usableTree: the newest COMPLETE tree, not simply the newest",
140
+ T.usableTree(["2027", "2026", "2025"], (y) => y !== "2027") === "2026",
141
+ );
142
+ check(
143
+ "usableTree: nothing complete — the newest, so packages go where the next build will look",
144
+ T.usableTree(["2027", "2026"], () => false) === "2027",
145
+ );
146
+ check("usableTree: an empty cache", T.usableTree([], () => true) === null);
147
+
148
+ // ── the fake mirror ────────────────────────────────────────────────────────────────────
149
+ const work = realpathSync(mkdtempSync(join(tmpdir(), "rpp-toolchain-h-")));
150
+ const good = join(work, "good");
151
+ const garbage = join(work, "garbage");
152
+ const staging = join(work, "staging", "install-tl-20260924");
153
+ mkdirSync(staging, { recursive: true });
154
+ mkdirSync(good);
155
+ mkdirSync(garbage);
156
+ for (const f of [
157
+ "install-tl",
158
+ "stub-pdflatex",
159
+ "stub-kpsewhich",
160
+ "stub-tlmgr",
161
+ "release-texlive.txt",
162
+ ])
163
+ copyFileSync(join(FIXTURE, f), join(staging, f));
164
+ spawnSync("chmod", [
165
+ "+x",
166
+ ...["install-tl", "stub-pdflatex", "stub-kpsewhich", "stub-tlmgr"].map((f) =>
167
+ join(staging, f),
168
+ ),
169
+ ]);
170
+ spawnSync(
171
+ "tar",
172
+ [
173
+ "czf",
174
+ join(good, "install-tl-unx.tar.gz"),
175
+ "-C",
176
+ dirname(staging),
177
+ "install-tl-20260924",
178
+ ],
179
+ { stdio: "inherit" },
180
+ );
181
+ copyFileSync(join(FIXTURE, "catalog.txt"), join(good, "catalog.txt"));
182
+ copyFileSync(join(FIXTURE, "release-year"), join(good, "release-year"));
183
+ writeFileSync(
184
+ join(garbage, "install-tl-unx.tar.gz"),
185
+ "<html>503 Service Unavailable</html>",
186
+ );
187
+ const url = (dir) => pathToFileURL(dir).href;
188
+ const MISSING = url(join(work, "no-such-mirror"));
189
+
190
+ /** A `spawnSync` that records every program it was asked to run. */
191
+ const recorder = () => {
192
+ const calls = [];
193
+ const run = (cmd, args, opts) => {
194
+ calls.push({ cmd, args });
195
+ return spawnSync(cmd, args, opts);
196
+ };
197
+ return { run, calls };
198
+ };
199
+
200
+ // ── 2. the download ─────────────────────────────────────────────────────────────────────
201
+ {
202
+ const lines = [];
203
+ const { run, calls } = recorder();
204
+ const dest = join(work, "dl.tar.gz");
205
+ const got = T.downloadInstaller(
206
+ { run, log: (l) => lines.push(l), env: process.env },
207
+ [MISSING, url(garbage), url(good)],
208
+ dest,
209
+ );
210
+ // Guards: the archive check — a mirror's error page would be unpacked as install-tl.
211
+ check(
212
+ "download: falls through a missing mirror and a non-archive to the good one",
213
+ got === url(good),
214
+ `${got}\n${lines.join("\n")}`,
215
+ );
216
+ check(
217
+ "download: both failures are SAID, one line each",
218
+ lines.length === 2 &&
219
+ lines[0].includes("no-such-mirror") &&
220
+ lines[1].includes("not a .tar.gz"),
221
+ lines.join("\n"),
222
+ );
223
+ const curls = calls.filter((c) => c.cmd === "curl");
224
+ // Guards: the per-download bond — a hung mirror would hold the job until the job's own timeout.
225
+ check(
226
+ "download: every curl carries its own time limit",
227
+ curls.length === 3 &&
228
+ curls.every(
229
+ (c) =>
230
+ c.args[c.args.indexOf("--max-time") + 1] ===
231
+ String(T.DOWNLOAD_SECONDS),
232
+ ),
233
+ );
234
+ check(
235
+ "🔴 download: TLS is never switched off",
236
+ curls.every((c) => !c.args.some((a) => a === "-k" || a === "--insecure")),
237
+ );
238
+ check(
239
+ "download: -f — an HTTP error page is a failure, not a file",
240
+ curls.every((c) => c.args[0].includes("f")),
241
+ );
242
+ }
243
+ {
244
+ const lines = [];
245
+ const got = T.downloadInstaller(
246
+ { run: spawnSync, log: (l) => lines.push(l), env: process.env },
247
+ [MISSING, url(garbage)],
248
+ join(work, "dl2.tar.gz"),
249
+ );
250
+ check("download: no good mirror → null, not a half file", got === null);
251
+ }
252
+
253
+ // ── 3. the command ─────────────────────────────────────────────────────────────────────
254
+ const TEX = {
255
+ packages: {
256
+ latex: ["latex.ltx", "article.cls"],
257
+ acmart: ["acmart.cls"],
258
+ },
259
+ tools: { texcount: ["texcount"] },
260
+ };
261
+ const root = join(work, "cache");
262
+ // 🔴 banal goes into the work directory, never the developer's cache: without RPP_BANAL_DIR the
263
+ // command installs into ~/.cache/rpp/banal, and a harness must not leave files in a home.
264
+ const banalDir = join(work, "banal");
265
+ const env = {
266
+ ...process.env,
267
+ RPP_TEXLIVE_DIR: root,
268
+ RPP_CTAN_MIRROR: url(good),
269
+ RPP_BANAL_DIR: banalDir,
270
+ };
271
+ // A stand-in banal: a Perl script that prints what banal prints for a one-page, 10 pt probe.
272
+ const fakeBanal = join(work, "fake-banal");
273
+ writeFileSync(
274
+ fakeBanal,
275
+ 'print qq({"bodyfontsize": 10.3, "columns": 1, "pages": [{}]}\\n);\n',
276
+ );
277
+ const BANAL = {
278
+ url: url(fakeBanal),
279
+ sha256: createHash("sha256").update(readFileSync(fakeBanal)).digest("hex"),
280
+ };
281
+ /** banal's installer over the real ports, every process going through the recorder. */
282
+ const installer = (e, source, run) => {
283
+ const s = B.parseBanalSettings(e, N.hostDirs());
284
+ const ports = N.nodeAdapters({ tmpDir: s.tmpDir, spawn: run });
285
+ const download = C.curlDownload({
286
+ run: ports.run,
287
+ env: s.processEnv,
288
+ tmpDir: s.tmpDir,
289
+ });
290
+ return B.banalInstaller({ ...ports, download }, s, source);
291
+ };
292
+ const cmd = ({ banal = BANAL, ...over } = {}) => {
293
+ const out = [];
294
+ const err = [];
295
+ const rec = recorder();
296
+ const code = T.runToolchain({
297
+ log: (l) => out.push(l),
298
+ err: (l) => err.push(l),
299
+ env,
300
+ run: rec.run,
301
+ platform: "linux",
302
+ tex: TEX,
303
+ banal: installer(over.env ?? env, banal, rec.run),
304
+ now: (() => {
305
+ let t = 0;
306
+ return () => (t += 1000);
307
+ })(),
308
+ ...over,
309
+ });
310
+ return { code, out: out.join("\n"), err: err.join("\n"), calls: rec.calls };
311
+ };
312
+
313
+ {
314
+ const r = cmd({ check: true });
315
+ check(
316
+ "--check on an empty cache: exit 1, says nothing is installed and what the command installs",
317
+ r.code === 1 &&
318
+ r.out.includes("no TeX Live in") &&
319
+ r.out.includes("`npx paperlint toolchain` installs 3 packages"),
320
+ r.out,
321
+ );
322
+ check("--check changes nothing", !existsSync(join(root, "2026")));
323
+ }
324
+ {
325
+ const r = cmd();
326
+ check("fresh install: exit 0", r.code === 0, `${r.out}\n${r.err}`);
327
+ check(
328
+ "fresh install: the tree is <cache>/<TeX Live year>",
329
+ existsSync(join(root, "2026", "bin", "x86_64-linux", "pdflatex")),
330
+ );
331
+ check(
332
+ "fresh install: the success line counts what was VERIFIED",
333
+ r.out.includes("✓ TeX Live 2026 is ready in") &&
334
+ r.out.includes(
335
+ "3 packages verified (3 files found by kpsewhich, 1 tools)",
336
+ ),
337
+ r.out,
338
+ );
339
+ const tlmgr = readFileSync(join(root, "2026", "tlmgr.log"), "utf8");
340
+ check(
341
+ "fresh install: tlmgr was asked for every declared package, from the mirror that served install-tl",
342
+ tlmgr.trim() === `${url(good)} acmart latex texcount`,
343
+ tlmgr,
344
+ );
345
+ }
346
+ {
347
+ const r = cmd();
348
+ // Guards: idempotence — the second run must not touch the network or say it did work.
349
+ check(
350
+ "second run: exit 0 and says there is nothing to do",
351
+ r.code === 0 &&
352
+ r.out.includes("already has all 3 declared packages — nothing to do"),
353
+ r.out,
354
+ );
355
+ check(
356
+ "second run: names the bin directory a script puts on PATH",
357
+ r.out.includes(` bin: ${join(root, "2026", "bin", "x86_64-linux")}`),
358
+ r.out,
359
+ );
360
+ check(
361
+ "second run: downloads nothing and installs nothing",
362
+ !r.calls.some((c) => c.cmd === "curl" || c.cmd.endsWith("tlmgr")),
363
+ JSON.stringify(r.calls.map((c) => c.cmd)),
364
+ );
365
+ }
366
+ {
367
+ const r = cmd({ check: true });
368
+ check(
369
+ "--check on a complete tree: exit 0",
370
+ r.code === 0 && r.out.includes("has all 3 declared packages"),
371
+ r.out,
372
+ );
373
+ }
374
+ {
375
+ // A package declared LATER: the tree exists, so only tlmgr runs, for that package alone.
376
+ const tex = {
377
+ ...TEX,
378
+ packages: { ...TEX.packages, libertine: ["libertine.sty"] },
379
+ };
380
+ const before = cmd({ check: true, tex });
381
+ check(
382
+ "--check names the missing package WITH its proof file",
383
+ before.code === 1 &&
384
+ before.out.includes(
385
+ "lacks 1 of 4 declared packages: libertine (libertine.sty)",
386
+ ),
387
+ before.out,
388
+ );
389
+ const r = cmd({ tex });
390
+ check("partial: exit 0", r.code === 0, `${r.out}\n${r.err}`);
391
+ check(
392
+ "partial: no download, one tlmgr call for the one missing package",
393
+ !r.calls.some((c) => c.cmd === "curl") &&
394
+ readFileSync(join(root, "2026", "tlmgr.log"), "utf8")
395
+ .trim()
396
+ .split("\n")
397
+ .at(-1) === `${url(good)} libertine`,
398
+ );
399
+ }
400
+ {
401
+ const r = cmd({
402
+ tex: { ...TEX, packages: { ...TEX.packages, bogus: ["bogus.sty"] } },
403
+ });
404
+ // Guards: the diagnosis — an invented package name would read as a missing file, not as a wrong
405
+ // name.
406
+ check(
407
+ "🔴 an unknown package name fails and is NAMED",
408
+ r.code === 1 && r.err.includes("tlmgr does not know: bogus"),
409
+ r.err,
410
+ );
411
+ }
412
+ {
413
+ const r = cmd({
414
+ tex: { ...TEX, packages: { ...TEX.packages, liar: ["liar.sty"] } },
415
+ });
416
+ // Guards: acceptance by result — tlmgr's exit code must not stand for the files; that is how an
417
+ // apt step once 'installed' nothing.
418
+ check(
419
+ "🔴 tlmgr 'installs' it but the proof file is absent: FAIL, package and file named",
420
+ r.code === 1 && r.err.includes("still lacks, after tlmgr: liar (liar.sty)"),
421
+ r.err,
422
+ );
423
+ }
424
+ {
425
+ // A tool whose program never appears in the bin directory.
426
+ appendFileSync(join(good, "catalog.txt"), "checkcites checkcites.lua\n");
427
+ const r = cmd({
428
+ tex: { ...TEX, tools: { ...TEX.tools, checkcites: ["checkcites"] } },
429
+ });
430
+ check(
431
+ "🔴 a declared tool with no executable fails, named",
432
+ r.code === 1 && r.err.includes("checkcites (checkcites)"),
433
+ r.err,
434
+ );
435
+ }
436
+ {
437
+ const r = cmd({ platform: "win32" });
438
+ check(
439
+ "Windows: a clear refusal, nothing touched",
440
+ r.code === 1 && r.err.includes("Windows is not supported"),
441
+ r.err,
442
+ );
443
+ }
444
+ {
445
+ const fresh = join(work, "cache-fail");
446
+ const r = cmd({
447
+ env: {
448
+ ...env,
449
+ RPP_TEXLIVE_DIR: fresh,
450
+ FAKE_INSTALL_TL_FAIL: "1",
451
+ },
452
+ });
453
+ check(
454
+ "install-tl failing: exit 1 and its own last lines are shown",
455
+ r.code === 1 &&
456
+ r.err.includes("install-tl failed") &&
457
+ r.err.includes("simulated failure"),
458
+ r.err,
459
+ );
460
+ }
461
+ {
462
+ const r = cmd({
463
+ env: {
464
+ ...env,
465
+ RPP_TEXLIVE_DIR: join(work, "cache-nomirror"),
466
+ RPP_CTAN_MIRROR: MISSING,
467
+ },
468
+ });
469
+ check(
470
+ "no mirror answers: exit 1, names the mirrors and the override",
471
+ r.code === 1 &&
472
+ r.err.includes("no CTAN mirror returned install-tl") &&
473
+ r.err.includes("RPP_CTAN_MIRROR"),
474
+ r.err,
475
+ );
476
+ }
477
+
478
+ // ── 4. a new TeX Live year on CTAN ─────────────────────────────────────────────────────
479
+ /**
480
+ * A mirror serving the TeX Live `year` release: an install-tl archive whose release-texlive.txt
481
+ * says `installerYear`, the catalog, and the repository's `release-year`. The two differ only for
482
+ * the case where the mirrors disagree.
483
+ */
484
+ const mirrorFor = (name, year, installerYear = year) => {
485
+ const dir = join(work, name);
486
+ const stage = join(
487
+ work,
488
+ `staging-${name}`,
489
+ `install-tl-${installerYear}0401`,
490
+ );
491
+ mkdirSync(dir, { recursive: true });
492
+ mkdirSync(stage, { recursive: true });
493
+ for (const f of [
494
+ "install-tl",
495
+ "stub-pdflatex",
496
+ "stub-kpsewhich",
497
+ "stub-tlmgr",
498
+ ])
499
+ copyFileSync(join(staging, f), join(stage, f));
500
+ writeFileSync(
501
+ join(stage, "release-texlive.txt"),
502
+ `TeX Live (https://tug.org/texlive) version ${installerYear}\n`,
503
+ );
504
+ spawnSync("tar", [
505
+ "czf",
506
+ join(dir, "install-tl-unx.tar.gz"),
507
+ "-C",
508
+ dirname(stage),
509
+ `install-tl-${installerYear}0401`,
510
+ ]);
511
+ copyFileSync(join(FIXTURE, "catalog.txt"), join(dir, "catalog.txt"));
512
+ writeFileSync(join(dir, "release-year"), `${year}\n`);
513
+ return url(dir);
514
+ };
515
+ const next = mirrorFor("good2027", "2027");
516
+ const years = join(work, "cache-years");
517
+ const withLibertine = {
518
+ ...TEX,
519
+ packages: { ...TEX.packages, libertine: ["libertine.sty"] },
520
+ };
521
+ const inYears = (mirror, over = {}) =>
522
+ cmd({
523
+ env: { ...env, RPP_TEXLIVE_DIR: years, RPP_CTAN_MIRROR: mirror },
524
+ ...over,
525
+ });
526
+ check(
527
+ "years: a 2026 tree to start from",
528
+ inYears(url(good)).code === 0 && existsSync(join(years, "2026")),
529
+ );
530
+ {
531
+ const r = inYears(next);
532
+ check(
533
+ "🔴 years: a complete 2026 tree and a 2027 mirror, nothing new declared — NO upgrade",
534
+ r.code === 0 &&
535
+ r.out.includes("TeX Live 2026") &&
536
+ r.out.includes("nothing to do") &&
537
+ !existsSync(join(years, "2027")) &&
538
+ !r.calls.some((c) => c.cmd === "curl" || c.cmd.endsWith("tlmgr")),
539
+ `${r.out}\n${JSON.stringify(r.calls.map((c) => c.cmd))}`,
540
+ );
541
+ }
542
+ {
543
+ const r = inYears(next, { tex: withLibertine });
544
+ // Guards: the recovery — a new TeX Live year would fail every newly declared package, loudly and
545
+ // forever.
546
+ check(
547
+ "🔴 years: a new package on a 2027 mirror — tlmgr's refusal installs a 2027 tree beside the old one",
548
+ r.code === 0 &&
549
+ r.out.includes(
550
+ "tlmgr refused: the repository is TeX Live 2027, the tree in",
551
+ ) &&
552
+ r.out.includes("✓ TeX Live 2027 is ready in") &&
553
+ existsSync(join(years, "2027", "bin", "x86_64-linux", "pdflatex")),
554
+ `${r.out}\n${r.err}`,
555
+ );
556
+ check(
557
+ "years: the new tree got EVERY declared package, not only the new one",
558
+ readFileSync(join(years, "2027", "tlmgr.log"), "utf8").trim() ===
559
+ `${next} acmart latex libertine texcount`,
560
+ );
561
+ // Guards: the notice — a superseded 300 MB tree would sit in the cache with nothing saying so.
562
+ check(
563
+ "years: the old tree is left in place, said so with its size and never deleted",
564
+ existsSync(join(years, "2026", "bin", "x86_64-linux", "pdflatex")) &&
565
+ r.out.includes(
566
+ `TeX Live 2026 in ${join(years, "2026")} is left in place and no longer used (`,
567
+ ) &&
568
+ r.out.includes("MB); delete that directory to free the space"),
569
+ r.out,
570
+ );
571
+ }
572
+ {
573
+ const r = inYears(next, { tex: withLibertine });
574
+ check(
575
+ "years: the next run uses 2027 and does nothing",
576
+ r.code === 0 &&
577
+ r.out.includes(
578
+ `TeX Live 2027 in ${join(years, "2027")} already has all 4`,
579
+ ),
580
+ r.out,
581
+ );
582
+ }
583
+ {
584
+ // tlmgr says 2027, but the installer the mirrors hand out is still 2026.
585
+ const split = join(work, "cache-split");
586
+ cmd({ env: { ...env, RPP_TEXLIVE_DIR: split } });
587
+ const r = cmd({
588
+ env: {
589
+ ...env,
590
+ RPP_TEXLIVE_DIR: split,
591
+ RPP_CTAN_MIRROR: mirrorFor("split2027", "2027", "2026"),
592
+ },
593
+ tex: withLibertine,
594
+ });
595
+ // Guards: the year guard — install-tl for the SAME year would run over the tree that is already
596
+ // there.
597
+ check(
598
+ "🔴 years: mirrors that disagree about the release fail, and install-tl is not run over the old tree",
599
+ r.code === 1 &&
600
+ r.err.includes("the mirrors disagree about the current release") &&
601
+ !r.calls.some((c) => c.cmd.endsWith("install-tl")),
602
+ r.err,
603
+ );
604
+ }
605
+
606
+ // ── 4. banal, the second half of the command ───────────────────────────────────────────
607
+ {
608
+ const dir = join(work, "banal-4");
609
+ const e = { ...env, RPP_BANAL_DIR: dir };
610
+ const before = cmd({ env: e, check: true });
611
+ // Guards: --check covers banal — a complete TeX Live alone must not read as a ready toolchain.
612
+ check(
613
+ "banal: --check with TeX Live complete and no banal exits 1 and names banal",
614
+ before.code === 1 && before.out.includes("banal not found"),
615
+ before.out,
616
+ );
617
+ const r = cmd({ env: e });
618
+ check(
619
+ "banal: the command installs it — exit 0, the ready line says the sha256 was verified",
620
+ r.code === 0 &&
621
+ r.out.includes("sha256 verified, and it measured a probe page"),
622
+ `${r.out}\n${r.err}`,
623
+ );
624
+ const again = cmd({ env: e });
625
+ check(
626
+ "banal: a second run downloads nothing and says so",
627
+ again.code === 0 &&
628
+ again.out.includes("is verified and runs — nothing to do") &&
629
+ !again.calls.some((c) => c.cmd === "curl"),
630
+ again.out,
631
+ );
632
+ check("banal: --check now passes", cmd({ env: e, check: true }).code === 0);
633
+ }
634
+ {
635
+ const r = cmd({
636
+ env: { ...env, RPP_BANAL_DIR: join(work, "banal-bad") },
637
+ banal: { ...BANAL, sha256: "0".repeat(64) },
638
+ });
639
+ // Guards: the two halves are independent — a banal failure fails the command, and TeX Live still
640
+ // reports ready rather than being hidden behind it.
641
+ check(
642
+ "🔴 banal with the wrong sha256: exit 1, the refusal named, and TeX Live still reported ready",
643
+ r.code === 1 &&
644
+ r.err.includes("does not have the pinned sha256") &&
645
+ r.out.includes("nothing to do"),
646
+ `${r.out}\n${r.err}`,
647
+ );
648
+ }
649
+
650
+ rmSync(work, { recursive: true, force: true });
651
+ console.log(`toolchain: ${n} checks passed`);