paperlint 3.1.2 → 3.2.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 (374) hide show
  1. package/.github/workflows/ci.yml +12 -7
  2. package/CLAUDE.md +3 -3
  3. package/CONTRIBUTING.md +34 -5
  4. package/dist/adapters/banal/index.d.ts.map +1 -1
  5. package/dist/adapters/banal/index.js +9 -5
  6. package/dist/adapters/banal/index.js.map +1 -1
  7. package/dist/adapters/banal/invocation.d.ts +1 -1
  8. package/dist/adapters/node/files.io.d.ts.map +1 -1
  9. package/dist/adapters/node/files.io.js +2 -1
  10. package/dist/adapters/node/files.io.js.map +1 -1
  11. package/dist/adapters/node/process.io.d.ts +0 -5
  12. package/dist/adapters/node/process.io.d.ts.map +1 -1
  13. package/dist/adapters/node/process.io.js +4 -3
  14. package/dist/adapters/node/process.io.js.map +1 -1
  15. package/dist/adapters/pdfjs/fill.d.ts.map +1 -1
  16. package/dist/adapters/pdfjs/fill.js +13 -7
  17. package/dist/adapters/pdfjs/fill.js.map +1 -1
  18. package/dist/adapters/references/index.d.ts.map +1 -1
  19. package/dist/adapters/references/index.js +4 -5
  20. package/dist/adapters/references/index.js.map +1 -1
  21. package/dist/adapters/references/pool.d.ts.map +1 -1
  22. package/dist/adapters/references/pool.js +4 -5
  23. package/dist/adapters/references/pool.js.map +1 -1
  24. package/dist/adapters/references/reach.io.d.ts.map +1 -1
  25. package/dist/adapters/references/reach.io.js +2 -1
  26. package/dist/adapters/references/reach.io.js.map +1 -1
  27. package/dist/build-engine.js +2 -2
  28. package/dist/build-engine.js.map +1 -1
  29. package/dist/build.d.ts.map +1 -1
  30. package/dist/build.js +14 -11
  31. package/dist/build.js.map +1 -1
  32. package/dist/cli.d.ts.map +1 -1
  33. package/dist/cli.js +31 -34
  34. package/dist/cli.js.map +1 -1
  35. package/dist/doctor.d.ts.map +1 -1
  36. package/dist/doctor.js +3 -2
  37. package/dist/doctor.js.map +1 -1
  38. package/dist/domain/lookup-cache.d.ts.map +1 -1
  39. package/dist/domain/lookup-cache.js +10 -8
  40. package/dist/domain/lookup-cache.js.map +1 -1
  41. package/dist/domain/text.d.ts +12 -0
  42. package/dist/domain/text.d.ts.map +1 -1
  43. package/dist/domain/text.js +17 -0
  44. package/dist/domain/text.js.map +1 -1
  45. package/dist/engine.d.ts.map +1 -1
  46. package/dist/engine.js +4 -3
  47. package/dist/engine.js.map +1 -1
  48. package/dist/eslint-rules/bib-reachable-entry.d.ts +22 -0
  49. package/dist/eslint-rules/bib-reachable-entry.d.ts.map +1 -0
  50. package/dist/eslint-rules/bib-reachable-entry.js +75 -0
  51. package/dist/eslint-rules/bib-reachable-entry.js.map +1 -0
  52. package/dist/eslint-rules/install-path-literals.d.ts +80 -0
  53. package/dist/eslint-rules/install-path-literals.d.ts.map +1 -0
  54. package/dist/eslint-rules/install-path-literals.js +105 -0
  55. package/dist/eslint-rules/install-path-literals.js.map +1 -0
  56. package/dist/eslint-rules/latex-language.d.ts +136 -0
  57. package/dist/eslint-rules/latex-language.d.ts.map +1 -0
  58. package/dist/eslint-rules/latex-language.js +639 -0
  59. package/dist/eslint-rules/latex-language.js.map +1 -0
  60. package/dist/eslint-rules/paper-research-question.d.ts +31 -0
  61. package/dist/eslint-rules/paper-research-question.d.ts.map +1 -0
  62. package/dist/eslint-rules/paper-research-question.js +188 -0
  63. package/dist/eslint-rules/paper-research-question.js.map +1 -0
  64. package/dist/eslint-rules/paper-stages.d.ts +83 -0
  65. package/dist/eslint-rules/paper-stages.d.ts.map +1 -0
  66. package/dist/eslint-rules/paper-stages.js +358 -0
  67. package/dist/eslint-rules/paper-stages.js.map +1 -0
  68. package/dist/eslint-rules/paper-typography.d.ts +74 -0
  69. package/dist/eslint-rules/paper-typography.d.ts.map +1 -0
  70. package/dist/eslint-rules/paper-typography.js +404 -0
  71. package/dist/eslint-rules/paper-typography.js.map +1 -0
  72. package/dist/eslint-rules/papers.d.ts +42 -0
  73. package/dist/eslint-rules/papers.d.ts.map +1 -0
  74. package/dist/eslint-rules/papers.js +145 -0
  75. package/dist/eslint-rules/papers.js.map +1 -0
  76. package/dist/eslint-rules/pdf-last-page-balance.d.ts +72 -0
  77. package/dist/eslint-rules/pdf-last-page-balance.d.ts.map +1 -0
  78. package/dist/eslint-rules/pdf-last-page-balance.js +237 -0
  79. package/dist/eslint-rules/pdf-last-page-balance.js.map +1 -0
  80. package/dist/eslint-rules/review-frontmatter.d.ts +57 -0
  81. package/dist/eslint-rules/review-frontmatter.d.ts.map +1 -0
  82. package/dist/eslint-rules/review-frontmatter.js +126 -0
  83. package/dist/eslint-rules/review-frontmatter.js.map +1 -0
  84. package/dist/eslint-rules/review-frontmatter.schema.json +36 -0
  85. package/dist/eslint-rules/rule-context.d.ts +55 -0
  86. package/dist/eslint-rules/rule-context.d.ts.map +1 -0
  87. package/dist/eslint-rules/rule-context.js +9 -0
  88. package/dist/eslint-rules/rule-context.js.map +1 -0
  89. package/dist/eslint-rules/sibling-frontmatter.d.ts +25 -0
  90. package/dist/eslint-rules/sibling-frontmatter.d.ts.map +1 -0
  91. package/dist/eslint-rules/sibling-frontmatter.js +16 -0
  92. package/dist/eslint-rules/sibling-frontmatter.js.map +1 -0
  93. package/dist/eslint-rules/sibling-frontmatter.schema.json +14 -0
  94. package/dist/eslint-rules/temp-root-realpath.d.ts +71 -0
  95. package/dist/eslint-rules/temp-root-realpath.d.ts.map +1 -0
  96. package/dist/eslint-rules/temp-root-realpath.js +139 -0
  97. package/dist/eslint-rules/temp-root-realpath.js.map +1 -0
  98. package/dist/eslint-rules/tex-build.d.ts +35 -0
  99. package/dist/eslint-rules/tex-build.d.ts.map +1 -0
  100. package/dist/eslint-rules/tex-build.js +313 -0
  101. package/dist/eslint-rules/tex-build.js.map +1 -0
  102. package/dist/facts-file.d.ts.map +1 -1
  103. package/dist/facts-file.js +2 -1
  104. package/dist/facts-file.js.map +1 -1
  105. package/dist/hooks-settings.d.ts +1 -1
  106. package/dist/hooks-settings.d.ts.map +1 -1
  107. package/dist/hooks-settings.js +14 -9
  108. package/dist/hooks-settings.js.map +1 -1
  109. package/dist/init.d.ts +5 -5
  110. package/dist/init.d.ts.map +1 -1
  111. package/dist/init.js +15 -7
  112. package/dist/init.js.map +1 -1
  113. package/dist/latex-loop.d.ts +2 -2
  114. package/dist/latex-loop.d.ts.map +1 -1
  115. package/dist/latex-loop.js +2 -2
  116. package/dist/latex-loop.js.map +1 -1
  117. package/dist/lib/agent-cli-version.d.ts +65 -0
  118. package/dist/lib/agent-cli-version.d.ts.map +1 -0
  119. package/dist/lib/agent-cli-version.js +77 -0
  120. package/dist/lib/agent-cli-version.js.map +1 -0
  121. package/dist/lib/check.d.ts +19 -0
  122. package/dist/lib/check.d.ts.map +1 -0
  123. package/dist/lib/check.js +52 -0
  124. package/dist/lib/check.js.map +1 -0
  125. package/dist/lib/markdown.d.ts +180 -0
  126. package/dist/lib/markdown.d.ts.map +1 -0
  127. package/dist/lib/markdown.js +302 -0
  128. package/dist/lib/markdown.js.map +1 -0
  129. package/dist/lib/paper-config.d.ts +84 -0
  130. package/dist/lib/paper-config.d.ts.map +1 -0
  131. package/dist/lib/paper-config.js +142 -0
  132. package/dist/lib/paper-config.js.map +1 -0
  133. package/dist/lib/skill-eval-fixture.d.ts +29 -0
  134. package/dist/lib/skill-eval-fixture.d.ts.map +1 -0
  135. package/dist/lib/skill-eval-fixture.js +62 -0
  136. package/dist/lib/skill-eval-fixture.js.map +1 -0
  137. package/dist/link-skills.d.ts.map +1 -1
  138. package/dist/link-skills.js +5 -4
  139. package/dist/link-skills.js.map +1 -1
  140. package/dist/new-paper.d.ts.map +1 -1
  141. package/dist/new-paper.js +2 -2
  142. package/dist/new-paper.js.map +1 -1
  143. package/dist/paper-settings.d.ts.map +1 -1
  144. package/dist/paper-settings.js +5 -4
  145. package/dist/paper-settings.js.map +1 -1
  146. package/dist/pdf-facts.d.ts.map +1 -1
  147. package/dist/pdf-facts.js +2 -1
  148. package/dist/pdf-facts.js.map +1 -1
  149. package/dist/pdf-geometry.d.ts.map +1 -1
  150. package/dist/pdf-geometry.js +1 -1
  151. package/dist/pdf-geometry.js.map +1 -1
  152. package/dist/presets.d.ts.map +1 -1
  153. package/dist/presets.js +3 -2
  154. package/dist/presets.js.map +1 -1
  155. package/dist/references.d.ts.map +1 -1
  156. package/dist/references.js +11 -6
  157. package/dist/references.js.map +1 -1
  158. package/dist/rules-config.d.ts.map +1 -1
  159. package/dist/rules-config.js +6 -3
  160. package/dist/rules-config.js.map +1 -1
  161. package/dist/structure.js +1 -1
  162. package/dist/structure.js.map +1 -1
  163. package/dist/tex-requirements.d.ts.map +1 -1
  164. package/dist/tex-requirements.js +8 -3
  165. package/dist/tex-requirements.js.map +1 -1
  166. package/dist/toolchain.d.ts.map +1 -1
  167. package/dist/toolchain.js +21 -16
  168. package/dist/toolchain.js.map +1 -1
  169. package/dist/venue-rules.js +1 -1
  170. package/dist/venue-rules.js.map +1 -1
  171. package/docs/e2e.md +7 -7
  172. package/docs/install.md +1 -1
  173. package/docs/prior-art/README.md +1 -1
  174. package/docs/prior-art/package-location.md +5 -5
  175. package/docs/prior-art/repro/README.md +1 -1
  176. package/docs/prior-art/test-tooling.md +2 -2
  177. package/docs/testing.md +11 -8
  178. package/docs/toolchain.md +1 -1
  179. package/eslint-rules/{bib-reachable-entry.mjs → bib-reachable-entry.ts} +4 -3
  180. package/eslint-rules/install-path-literals.harness.mjs +1 -1
  181. package/eslint-rules/{install-path-literals.mjs → install-path-literals.ts} +16 -8
  182. package/eslint-rules/latex-language.harness.mjs +2 -2
  183. package/eslint-rules/latex-language.test.mjs +7 -1
  184. package/eslint-rules/{latex-language.mjs → latex-language.ts} +297 -109
  185. package/eslint-rules/paper-research-question.harness.mjs +3 -3
  186. package/eslint-rules/{paper-research-question.mjs → paper-research-question.ts} +34 -15
  187. package/eslint-rules/paper-stages.harness.mjs +2 -2
  188. package/eslint-rules/paper-stages.test.mjs +8 -2
  189. package/eslint-rules/{paper-stages.mjs → paper-stages.ts} +74 -29
  190. package/eslint-rules/paper-typography.test.mjs +3 -3
  191. package/eslint-rules/{paper-typography.mjs → paper-typography.ts} +132 -54
  192. package/eslint-rules/papers.harness.mjs +2 -2
  193. package/eslint-rules/{papers.mjs → papers.ts} +7 -4
  194. package/eslint-rules/pdf-last-page-balance.harness.mjs +3 -3
  195. package/eslint-rules/{pdf-last-page-balance.mjs → pdf-last-page-balance.ts} +88 -31
  196. package/eslint-rules/review-frontmatter.test.mjs +2 -2
  197. package/eslint-rules/{review-frontmatter.mjs → review-frontmatter.ts} +37 -15
  198. package/eslint-rules/rule-context.ts +62 -0
  199. package/eslint-rules/{sibling-frontmatter.mjs → sibling-frontmatter.ts} +1 -1
  200. package/eslint-rules/temp-root-realpath.harness.mjs +1 -1
  201. package/eslint-rules/{temp-root-realpath.mjs → temp-root-realpath.ts} +19 -6
  202. package/eslint-rules/tex-build.harness.mjs +2 -2
  203. package/eslint-rules/{tex-build.mjs → tex-build.ts} +4 -3
  204. package/eslint.config.mjs +139 -30
  205. package/fixtures/pdf-facts/README.md +2 -2
  206. package/fixtures/real-markdown-paper/{baseline.mjs → baseline.ts} +31 -10
  207. package/hooks/{hooks.harness.mjs → hooks.harness.ts} +51 -34
  208. package/hooks/paper-edit-guard.hook.mjs +1 -1
  209. package/hooks/{paper-edit-guard.test.mjs → paper-edit-guard.test.ts} +3 -7
  210. package/hooks/paper-skills-nudge.hook.mjs +1 -1
  211. package/hooks/{paper-skills-nudge.test.mjs → paper-skills-nudge.test.ts} +2 -6
  212. package/hooks/{paper-status-gates.test.mjs → paper-status-gates.test.ts} +2 -6
  213. package/lib/agent-cli-version.harness.mjs +1 -1
  214. package/lib/{agent-cli-version.mjs → agent-cli-version.ts} +32 -18
  215. package/lib/check.test.ts +14 -9
  216. package/lib/{check.mjs → check.ts} +22 -9
  217. package/lib/markdown.test.mjs +7 -7
  218. package/lib/{markdown.mjs → markdown.ts} +68 -29
  219. package/lib/paper-config.harness.mjs +8 -8
  220. package/lib/{paper-config.mjs → paper-config.ts} +19 -14
  221. package/lib/skill-checks.mjs +1 -1
  222. package/lib/skill-checks.test.mjs +1 -1
  223. package/lib/skill-corpus.mjs +1 -1
  224. package/lib/skill-eval-kit.mjs +1 -1
  225. package/package.json +42 -10
  226. package/scripts/{check.harness.mjs → check.harness.ts} +111 -10
  227. package/scripts/{check.test.mjs → check.test.ts} +13 -10
  228. package/scripts/{check.mjs → check.ts} +116 -42
  229. package/scripts/coverage-config.test.ts +80 -37
  230. package/scripts/eslint-report-guard.test.mjs +1 -1
  231. package/scripts/harness-api.frozen.json +14 -15
  232. package/scripts/harness-api.test.ts +8 -5
  233. package/scripts/layer-legacy-frozen.test.ts +17 -13
  234. package/scripts/{layer-legacy-frozen.mjs → layer-legacy-frozen.ts} +128 -39
  235. package/scripts/release-config.test.ts +12 -4
  236. package/scripts/rules-see-files.test.mjs +1 -1
  237. package/scripts/test.test.ts +78 -0
  238. package/scripts/test.ts +87 -0
  239. package/skills/grade-paper-writing/prose-lint.mjs +1 -1
  240. package/skills/grade-paper-writing/{prose-lint.test.mjs → prose-lint.test.ts} +2 -2
  241. package/skills/paper-pipeline/description-language.eval.mjs +1 -1
  242. package/skills/paper-pipeline/framing-vs-vocabulary.eval.mjs +1 -1
  243. package/skills/paper-pipeline/grade-paper-writing-ablation.eval.mjs +1 -1
  244. package/skills/paper-pipeline/pipeline-firing.eval.mjs +1 -1
  245. package/skills/paper-pipeline/pipeline-language.eval.mjs +1 -1
  246. package/skills/paper-pipeline/scripts/README.md +1 -1
  247. package/skills/paper-pipeline/scripts/announce.test.mjs +1 -1
  248. package/skills/paper-pipeline/scripts/artifact-coverage.mjs +1 -1
  249. package/skills/paper-pipeline/scripts/{artifact-coverage.test.mjs → artifact-coverage.test.ts} +2 -2
  250. package/skills/paper-pipeline/scripts/consumer.mjs +1 -5
  251. package/skills/paper-pipeline/scripts/consumer.test.mjs +1 -1
  252. package/skills/paper-pipeline/scripts/extract-ref-facts.harness.mjs +1 -1
  253. package/skills/paper-pipeline/scripts/extract-ref-facts.mjs +1 -4
  254. package/skills/paper-pipeline/scripts/extract-ref-facts.test.mjs +1 -1
  255. package/skills/paper-pipeline/scripts/fixtures/{scorecard.mjs → scorecard.ts} +36 -7
  256. package/skills/paper-pipeline/scripts/{generated-code.test.mjs → generated-code.test.ts} +3 -3
  257. package/skills/paper-pipeline/scripts/ledger.cli.test.mjs +1 -1
  258. package/skills/paper-pipeline/scripts/pipeline-check.cli.test.mjs +2 -2
  259. package/skills/paper-pipeline/scripts/pipeline-check.harness.mjs +2 -2
  260. package/skills/paper-pipeline/scripts/pipeline-check.mjs +1 -4
  261. package/skills/paper-pipeline/scripts/population-map.mjs +1 -4
  262. package/skills/paper-pipeline/scripts/population-map.test.mjs +1 -1
  263. package/skills/paper-pipeline/scripts/{round-diff.harness.mjs → round-diff.harness.ts} +45 -18
  264. package/skills/paper-pipeline/scripts/round-diff.mjs +1 -1
  265. package/skills/paper-pipeline/scripts/round-diff.test.mjs +1 -1
  266. package/skills/paper-pipeline/scripts/run-mechanical.test.mjs +1 -1
  267. package/skills/paper-pipeline/scripts/status.test.mjs +1 -1
  268. package/skills/plan-paper-timeline/fixtures/{fake-google-calendar.mjs → fake-google-calendar.ts} +101 -70
  269. package/skills/plan-paper-timeline/plan-paper-timeline.effects.harness.mjs +3 -3
  270. package/skills/render-paper/{extract-pdf-facts.harness.mjs → extract-pdf-facts.harness.ts} +34 -9
  271. package/skills/render-paper/render-paper.harness.mjs +2 -2
  272. package/skills/tighten-paper/structure.mjs +1 -1
  273. package/skills/tighten-paper/{structure.test.mjs → structure.test.ts} +4 -3
  274. package/skills/verify-citations/scripts/{bib-authors.cli.test.mjs → bib-authors.cli.test.ts} +14 -4
  275. package/skills/verify-citations/scripts/bib-authors.d.mts +63 -0
  276. package/skills/verify-citations/scripts/bib-authors.mjs +1 -4
  277. package/skills/verify-citations/scripts/{verify-cites.cli.test.mjs → verify-cites.cli.test.ts} +32 -9
  278. package/skills/verify-citations/scripts/verify-cites.d.mts +58 -0
  279. package/skills/verify-citations/scripts/verify-cites.mjs +11 -1
  280. package/skills/verify-citations/scripts/verify-cites.net.test.mjs +35 -0
  281. package/skills/verify-citations/scripts/verify-cites.test.mjs +14 -0
  282. package/src/CLAUDE.md +1 -1
  283. package/src/adapters/banal/index.test.ts +3 -6
  284. package/src/adapters/banal/index.ts +7 -4
  285. package/src/adapters/banal/invocation.test.ts +3 -1
  286. package/src/adapters/banal/invocation.ts +1 -1
  287. package/src/adapters/banal/output.test.ts +1 -1
  288. package/src/adapters/banal/run.test.ts +3 -3
  289. package/src/adapters/banal/xml.test.ts +1 -1
  290. package/src/adapters/curl/download.test.ts +4 -2
  291. package/src/adapters/node/files.io.ts +2 -2
  292. package/src/adapters/node/files.test.ts +3 -1
  293. package/src/adapters/node/process.io.ts +4 -3
  294. package/src/adapters/node/process.test.ts +1 -1
  295. package/src/adapters/node/workspace.test.ts +3 -1
  296. package/src/adapters/pdfjs/fill.test.ts +2 -2
  297. package/src/adapters/pdfjs/fill.ts +14 -7
  298. package/src/adapters/references/index.test.ts +31 -38
  299. package/src/adapters/references/index.ts +26 -58
  300. package/src/adapters/references/pool.ts +3 -5
  301. package/src/adapters/references/reach.io.ts +2 -1
  302. package/src/build-engine.harness.mjs +1 -1
  303. package/src/build-engine.test.ts +2 -2
  304. package/src/build-engine.ts +2 -2
  305. package/src/build.harness.mjs +2 -2
  306. package/src/build.test.ts +15 -14
  307. package/src/build.ts +28 -19
  308. package/src/cli.harness.mjs +6 -6
  309. package/src/cli.test.ts +13 -6
  310. package/src/cli.ts +39 -37
  311. package/src/doctor.harness.mjs +3 -3
  312. package/src/doctor.ts +3 -2
  313. package/src/domain/lookup-cache.ts +21 -11
  314. package/src/domain/result.test.ts +2 -2
  315. package/src/domain/text.test.ts +32 -0
  316. package/src/domain/text.ts +23 -0
  317. package/src/engine.harness.mjs +2 -2
  318. package/src/engine.ts +4 -3
  319. package/src/facts-file.test.ts +9 -7
  320. package/src/facts-file.ts +2 -1
  321. package/src/{hooks-settings.harness.mjs → hooks-settings.harness.ts} +39 -20
  322. package/src/hooks-settings.ts +20 -9
  323. package/src/init.test.ts +12 -9
  324. package/src/init.ts +22 -12
  325. package/src/{latex-log.harness.mjs → latex-log.harness.ts} +14 -14
  326. package/src/{latex-loop.harness.mjs → latex-loop.harness.ts} +64 -36
  327. package/src/latex-loop.ts +3 -3
  328. package/src/{link-skills.harness.mjs → link-skills.harness.ts} +19 -19
  329. package/src/link-skills.test.ts +3 -3
  330. package/src/link-skills.ts +5 -4
  331. package/src/lint-fix.test.ts +4 -6
  332. package/src/linted-set.test.ts +2 -2
  333. package/src/new-paper.harness.mjs +2 -2
  334. package/src/new-paper.test.ts +8 -10
  335. package/src/new-paper.ts +2 -2
  336. package/src/paper-files.test.ts +3 -5
  337. package/src/paper-settings-commands.test.ts +5 -4
  338. package/src/paper-settings.test.ts +5 -5
  339. package/src/paper-settings.ts +8 -9
  340. package/src/{pdf-facts.harness.mjs → pdf-facts.harness.ts} +9 -8
  341. package/src/pdf-facts.test.ts +10 -11
  342. package/src/pdf-facts.ts +2 -1
  343. package/src/{pdf-geometry.harness.mjs → pdf-geometry.harness.ts} +19 -12
  344. package/src/pdf-geometry.ts +3 -1
  345. package/src/presets.ts +3 -2
  346. package/src/references.test.ts +51 -43
  347. package/src/references.ts +14 -7
  348. package/src/{rules-config.harness.mjs → rules-config.harness.ts} +7 -13
  349. package/src/rules-config.ts +7 -4
  350. package/src/rules-off.test.ts +6 -4
  351. package/src/{structure.harness.mjs → structure.harness.ts} +26 -15
  352. package/src/structure.ts +1 -1
  353. package/src/{tex-requirements.harness.mjs → tex-requirements.harness.ts} +50 -22
  354. package/src/tex-requirements.ts +8 -5
  355. package/src/toolchain.harness.mjs +2 -2
  356. package/src/toolchain.test.ts +1 -1
  357. package/src/toolchain.ts +31 -17
  358. package/src/venue-rules.test.ts +62 -64
  359. package/src/venue-rules.ts +1 -1
  360. package/tsconfig.json +6 -3
  361. package/tsconfig.pkg.json +10 -0
  362. package/lib/check.d.mts +0 -17
  363. package/lib/paper-config.d.mts +0 -31
  364. package/scripts/layer-legacy-frozen.d.mts +0 -36
  365. package/scripts/rules-are-content-only.harness.mjs +0 -105
  366. package/scripts/rules-are-content-only.mjs +0 -148
  367. package/scripts/rules-are-content-only.test.mjs +0 -61
  368. package/scripts/test.mjs +0 -44
  369. package/scripts/test.test.mjs +0 -52
  370. /package/bin/{paperlint.test.mjs → paperlint.test.ts} +0 -0
  371. /package/lib/{skill-eval-fixture.mjs → skill-eval-fixture.ts} +0 -0
  372. /package/skills/grade-paper-writing/__snapshots__/{prose-lint.test.mjs.snap → prose-lint.test.ts.snap} +0 -0
  373. /package/skills/osf-artifact-upload/{osf-artifact-upload.harness.mjs → osf-artifact-upload.harness.ts} +0 -0
  374. /package/skills/tighten-paper/__snapshots__/{structure.test.mjs.snap → structure.test.ts.snap} +0 -0
@@ -15,18 +15,27 @@ import {
15
15
  } from "node:fs";
16
16
  import { tmpdir } from "node:os";
17
17
  import { join, dirname } from "node:path";
18
- import { fileURLToPath } from "node:url";
19
- import { createChecker } from "../lib/check.mjs";
18
+ import { z } from "zod";
19
+ import { createChecker } from "../lib/check.ts";
20
20
 
21
- const HERE = dirname(fileURLToPath(import.meta.url));
21
+ /** `asEslintResults` promises only `unknown[]`; these are the fields the checks read. */
22
+ const EslintResults = z.array(
23
+ z.object({
24
+ errorCount: z.number(),
25
+ warningCount: z.number(),
26
+ messages: z.array(
27
+ z.object({ ruleId: z.string().nullable(), severity: z.number() }),
28
+ ),
29
+ }),
30
+ );
22
31
  const { checkStructure, formatStructure, asEslintResults, STRUCTURE_DEFAULTS } =
23
- await import(join(HERE, "structure.ts"));
32
+ await import("./structure.ts");
24
33
 
25
34
  const check = createChecker();
26
35
 
27
36
  const root = realpathSync(mkdtempSync(join(tmpdir(), "paperlint-struct-")));
28
37
  const papers = join(root, "papers");
29
- const paper = (name, files) => {
38
+ const paper = (name: string, files: readonly string[]) => {
30
39
  const dir = join(papers, name);
31
40
  mkdirSync(dir, { recursive: true });
32
41
  for (const f of files) {
@@ -50,7 +59,7 @@ try {
50
59
  writeFileSync(join(papers, ".hidden", "paper.tex"), "x");
51
60
 
52
61
  const f = checkStructure([papers], undefined, { cwd: root });
53
- const at = (name) => f.filter((x) => x.file.endsWith(name));
62
+ const at = (name: string) => f.filter((x) => x.file.endsWith(name));
54
63
 
55
64
  check(
56
65
  "a complete paper directory — NOT A SINGLE finding",
@@ -63,24 +72,26 @@ try {
63
72
  check(
64
73
  "a missing scorecard — a finding",
65
74
  at("no-scorecard").length === 1 &&
66
- /missing `PIPELINE-STATUS\.md`/.test(at("no-scorecard")[0].message),
75
+ /missing `PIPELINE-STATUS\.md`/.test(
76
+ at("no-scorecard")[0]?.message ?? "",
77
+ ),
67
78
  );
68
79
  check(
69
80
  "and the message names the CONSEQUENCE, not a restatement of the condition",
70
- /are skipped/.test(at("no-scorecard")[0].message) &&
81
+ /are skipped/.test(at("no-scorecard")[0]?.message ?? "") &&
71
82
  /stage, source and research-question checks/.test(
72
- at("no-scorecard")[0].message,
83
+ at("no-scorecard")[0]?.message ?? "",
73
84
  ),
74
85
  );
75
86
  check(
76
87
  "and the consequence names THIS directory by name",
77
- /no-scorecard/.test(at("no-scorecard")[0].message),
88
+ /no-scorecard/.test(at("no-scorecard")[0]?.message ?? ""),
78
89
  );
79
90
  check(
80
91
  "a directory with no source — a finding, and BOTH accepted forms are listed",
81
92
  at("no-source").length === 1 &&
82
- /`paper\.tex`/.test(at("no-source")[0].message) &&
83
- /`paper\.md`/.test(at("no-source")[0].message),
93
+ /`paper\.tex`/.test(at("no-source")[0]?.message ?? "") &&
94
+ /`paper\.md`/.test(at("no-source")[0]?.message ?? ""),
84
95
  );
85
96
 
86
97
  // 🔴 THE PAIRED HALF: detection is GENEROUS. Without this the check would scream about every
@@ -136,7 +147,7 @@ try {
136
147
  );
137
148
 
138
149
  // ── one schema for both halves ────────────────────────────────────────────────────────
139
- const asResults = asEslintResults(f);
150
+ const asResults = EslintResults.parse(asEslintResults(f));
140
151
  check(
141
152
  "findings come back shaped like an ESLint result — `--json` stays one array",
142
153
  asResults.length === 2 &&
@@ -144,7 +155,7 @@ try {
144
155
  (r) =>
145
156
  r.errorCount === 1 &&
146
157
  r.warningCount === 0 &&
147
- r.messages[0].ruleId === "structure/required-file" &&
158
+ r.messages[0]?.ruleId === "structure/required-file" &&
148
159
  r.messages[0].severity === 2,
149
160
  ),
150
161
  );
@@ -156,7 +167,7 @@ try {
156
167
  { require: ["PIPELINE-STATUS.md", "refs.bib"] },
157
168
  { cwd: root },
158
169
  ).filter((x) => x.file.endsWith("no-source"));
159
- return asEslintResults(two)[0].errorCount === 2;
170
+ return EslintResults.parse(asEslintResults(two))[0]?.errorCount === 2;
160
171
  })(),
161
172
  );
162
173
  check(
package/src/structure.ts CHANGED
@@ -25,7 +25,7 @@
25
25
  import { readdirSync, existsSync } from "node:fs";
26
26
  import { join, relative, basename, isAbsolute, sep } from "node:path";
27
27
  import type { StructureConfig, StructureFinding } from "./types.ts";
28
- import { CONFIG_FILE } from "../lib/paper-config.mjs";
28
+ import { CONFIG_FILE } from "#lib/paper-config";
29
29
 
30
30
  /** The requirements after the consumer's config is laid over the defaults. */
31
31
  type Rules = Required<StructureConfig>;
@@ -13,12 +13,14 @@ import {
13
13
  readFileSync,
14
14
  realpathSync,
15
15
  rmSync,
16
+ writeFileSync,
16
17
  } from "node:fs";
17
18
  import { tmpdir } from "node:os";
18
19
  import { dirname, join } from "node:path";
19
20
  import { fileURLToPath } from "node:url";
20
21
  import ts from "typescript";
21
- import { createChecker } from "../lib/check.mjs";
22
+ import { z } from "zod";
23
+ import { createChecker } from "../lib/check.ts";
22
24
 
23
25
  const HERE = dirname(fileURLToPath(import.meta.url));
24
26
  const VENUES = join(
@@ -29,18 +31,18 @@ const VENUES = join(
29
31
  "references",
30
32
  "venues",
31
33
  );
32
- const R = await import(join(HERE, "tex-requirements.ts"));
34
+ const R = await import("./tex-requirements.ts");
33
35
 
34
36
  const check = createChecker();
35
- const throws = (fn) => {
37
+ const throws = (fn: () => unknown): string => {
36
38
  try {
37
39
  fn();
38
40
  return "";
39
41
  } catch (e) {
40
- return e.message;
42
+ return e instanceof Error ? e.message : String(e);
41
43
  }
42
44
  };
43
- const read = (f) =>
45
+ const read = (f: string) =>
44
46
  R.parseProfile(readFileSync(join(VENUES, f), "utf8"), f, VENUES);
45
47
 
46
48
  // ── 1. the shipped data ─────────────────────────────────────────────────────────────────
@@ -69,11 +71,14 @@ check(
69
71
  );
70
72
 
71
73
  // The whole profile, parsed the way the other harnesses read these files — never a line of text.
72
- const templateOf = (v) =>
73
- ts.parseConfigFileTextToJson(
74
- v,
75
- readFileSync(join(VENUES, `${v}.jsonc`), "utf8"),
76
- ).config.template;
74
+ const TemplateField = z.object({ template: z.unknown().optional() });
75
+ const templateOf = (v: string): unknown =>
76
+ TemplateField.parse(
77
+ ts.parseConfigFileTextToJson(
78
+ v,
79
+ readFileSync(join(VENUES, `${v}.jsonc`), "utf8"),
80
+ ).config,
81
+ ).template;
77
82
  const acm = venues.filter((v) => templateOf(v) === "acmart");
78
83
  check(
79
84
  "at least one acmart venue is shipped (else the next checks see nothing)",
@@ -92,7 +97,7 @@ for (const v of acm) {
92
97
  // minimal one.
93
98
  ["fancyhdr", "fancyhdr.sty"],
94
99
  ["acmart", "acmart.cls"],
95
- ])
100
+ ] satisfies [string, string][])
96
101
  check(
97
102
  `🔴 ${v}: declares ${pkg}, proved by ${file}`,
98
103
  tex.packages[pkg]?.includes(file),
@@ -102,22 +107,39 @@ for (const v of acm) {
102
107
  // dictionaries diverged. Since the ACM venues extend the `acm-sigconf` family, only the family
103
108
  // declares the template's packages, so this holds by construction; the check stays for any two
104
109
  // standalone presets that name the same template.
105
- const byTemplate = new Map();
110
+ const byTemplate = new Map<unknown, string[]>();
106
111
  for (const v of venues) {
107
112
  const t = templateOf(v);
108
113
  byTemplate.set(t, [...(byTemplate.get(t) ?? []), v]);
109
114
  }
110
115
  for (const [t, vs] of byTemplate)
111
116
  check(
112
- `venues on template ${t} declare the SAME packages (${vs.join(", ")})`,
117
+ `venues on template ${String(t)} declare the SAME packages (${vs.join(", ")})`,
113
118
  new Set(vs.map((v) => JSON.stringify(read(`${v}.jsonc`)))).size === 1,
114
119
  );
115
120
 
121
+ // A venues directory whose schema file is JSON but not a schema: refused where it is read, with
122
+ // the file's name — not deep inside Ajv as "schema should be object or boolean".
123
+ {
124
+ const odd = realpathSync(
125
+ mkdtempSync(join(tmpdir(), "paperlint-texreq-schema-")),
126
+ );
127
+ writeFileSync(join(odd, R.SCHEMA_FILE), "3\n");
128
+ check(
129
+ "a schema file that is not a JSON object is refused, naming the file",
130
+ new RegExp(
131
+ `${R.SCHEMA_FILE.replace(/\./g, "\\.")}: not a JSON Schema object`,
132
+ ).test(throws(() => R.parsePreset("{}", "odd.jsonc", odd))),
133
+ throws(() => R.parsePreset("{}", "odd.jsonc", odd)),
134
+ );
135
+ }
136
+
116
137
  // ── 2. the schema rejects ───────────────────────────────────────────────────────────────
117
138
  const tmp = realpathSync(mkdtempSync(join(tmpdir(), "paperlint-texreq-h-")));
118
139
  copyFileSync(join(VENUES, R.SCHEMA_FILE), join(tmp, R.SCHEMA_FILE));
119
- const bad = (text) => throws(() => R.parseProfile(text, "bad.jsonc", tmp));
120
- const BAD = [
140
+ const bad = (text: string) =>
141
+ throws(() => R.parseProfile(text, "bad.jsonc", tmp));
142
+ const BAD: [string, string, string][] = [
121
143
  ["no `tex` block at all", `{ "template": "acmart" }`, "tex"],
122
144
  // Guards: typo detection — `templat` would be accepted and the field silently unread.
123
145
  [
@@ -181,13 +203,12 @@ rmSync(tmp, { recursive: true, force: true });
181
203
  // Which preset a paper resolves to — a typo, the base file, a missing one — is src/presets.ts's
182
204
  // question now, tested in src/presets.test.ts; this function only adds a resolved preset's packages.
183
205
  {
184
- const r = R.requirementsFor(
185
- { label: acm[0], tex: read(`${acm[0]}.jsonc`) },
186
- VENUES,
187
- );
206
+ // `acm` is non-empty: the check above it throws otherwise.
207
+ const [label = ""] = acm;
208
+ const r = R.requirementsFor({ label, tex: read(`${label}.jsonc`) }, VENUES);
188
209
  check(
189
210
  "a resolved preset → its packages ON TOP OF the base set, and the source names its label",
190
- r.source === `venue ${acm[0]}` &&
211
+ r.source === `venue ${label}` &&
191
212
  "acmart" in r.tex.packages &&
192
213
  "hyperref" in r.tex.packages &&
193
214
  "texcount" in r.tex.tools,
@@ -251,9 +272,16 @@ rmSync(tmp, { recursive: true, force: true });
251
272
  "violations: Ajv's null `errors` is no violation; a message Ajv left out is an empty one",
252
273
  R.violations("f.jsonc", { errors: null }).length === 0 &&
253
274
  R.violations("f.jsonc", {
254
- errors: [{ dataPath: "", message: undefined }],
275
+ errors: [
276
+ {
277
+ keyword: "required",
278
+ dataPath: "",
279
+ schemaPath: "#/required",
280
+ params: {},
281
+ },
282
+ ],
255
283
  })[0] === "f.jsonc: (top level) ",
256
284
  );
257
285
  }
258
286
 
259
- console.log(`tex-requirements: ${check.count} checks passed`);
287
+ console.log(`tex-requirements: ${String(check.count)} checks passed`);
@@ -26,7 +26,7 @@ import { join } from "node:path";
26
26
  // eslint-disable-next-line boundaries/dependencies -- legacy layer, moves behind a port in #76
27
27
  import Ajv from "ajv";
28
28
  import { packageVenuesDir } from "../skills/paper-pipeline/scripts/consumer.mjs";
29
- import { CONFIG_FILE } from "../lib/paper-config.mjs";
29
+ import { CONFIG_FILE } from "#lib/paper-config";
30
30
 
31
31
  /** CTAN package name → the names that prove it is installed. */
32
32
  export type PackageProofs = Readonly<Record<string, readonly string[]>>;
@@ -64,9 +64,10 @@ const validators = new Map<string, Validate>();
64
64
  function validatorFor(dir: string): Validate {
65
65
  const known = validators.get(dir);
66
66
  if (known) return known;
67
- const schema: object = JSON.parse(
68
- readFileSync(join(dir, SCHEMA_FILE), "utf8"),
69
- );
67
+ const path = join(dir, SCHEMA_FILE);
68
+ const schema: unknown = JSON.parse(readFileSync(path, "utf8"));
69
+ if (typeof schema !== "object" || schema === null)
70
+ throw new Error(`${path}: not a JSON Schema object`);
70
71
  const v = new Ajv({ allErrors: true }).compile(schema);
71
72
  validators.set(dir, v);
72
73
  return v;
@@ -214,7 +215,9 @@ export function parsePreset(
214
215
  file: string,
215
216
  dir: string = packageVenuesDir(),
216
217
  ): PresetFile {
217
- const { config, error } = typescript().parseConfigFileTextToJson(file, text);
218
+ const parsed = typescript().parseConfigFileTextToJson(file, text);
219
+ const config: unknown = parsed.config;
220
+ const error = parsed.error;
218
221
  if (error)
219
222
  throw new Error(
220
223
  `${file}: not valid JSONC — ${typescript().flattenDiagnosticMessageText(error.messageText, " ")}`,
@@ -6,7 +6,7 @@
6
6
  * inside is `fixtures/toolchain-mirror/install-tl`, which lays out a tree whose `kpsewhich` and
7
7
  * `tlmgr` answer from two text files. So what runs is paperlint's own download, fallback, unpack,
8
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`.
9
+ * The real TeX Live half is `test/e2e/toolchain.ts`.
10
10
  *
11
11
  * Tables, in order:
12
12
  * 1. the pure pieces (year, profile, tlmgr's unknown names, cache location);
@@ -32,7 +32,7 @@ import {
32
32
  import { tmpdir } from "node:os";
33
33
  import { dirname, join } from "node:path";
34
34
  import { fileURLToPath, pathToFileURL } from "node:url";
35
- import { createChecker } from "../lib/check.mjs";
35
+ import { createChecker } from "../lib/check.ts";
36
36
 
37
37
  const HERE = dirname(fileURLToPath(import.meta.url));
38
38
  const FIXTURE = join(HERE, "..", "fixtures", "toolchain-mirror");
@@ -9,7 +9,7 @@ import { chmodSync, mkdirSync, writeFileSync } from "node:fs";
9
9
  import { dirname } from "node:path";
10
10
  import { join } from "node:path";
11
11
  import { test } from "vitest";
12
- import { useTempDir, writeTree } from "../test/support.mjs";
12
+ import { useTempDir, writeTree } from "../test/support.ts";
13
13
  import {
14
14
  cachedTrees,
15
15
  CACHE_ENV,
package/src/toolchain.ts CHANGED
@@ -56,6 +56,10 @@ import {
56
56
  packageNames,
57
57
  type TexRequirements,
58
58
  } from "./tex-requirements.ts";
59
+ import { printed } from "./domain/text.ts";
60
+
61
+ /** stdin closed, stdout and stderr captured — the tuple type spawnSync's `stdio` wants. */
62
+ const PIPED: ["ignore", "pipe", "pipe"] = ["ignore", "pipe", "pipe"];
59
63
 
60
64
  export const CACHE_ENV = "PAPERLINT_TEXLIVE_DIR";
61
65
  export const MIRROR_ENV = "PAPERLINT_CTAN_MIRROR";
@@ -274,23 +278,29 @@ export function describeGaps(g: Gaps, tex: TexRequirements): string {
274
278
  export function formatDuration(ms: number): string {
275
279
  const s = Math.round(ms / 1000);
276
280
  return s < 60
277
- ? `${s}s`
278
- : `${Math.floor(s / 60)}m${String(s % 60).padStart(2, "0")}s`;
281
+ ? `${String(s)}s`
282
+ : `${String(Math.floor(s / 60))}m${String(s % 60).padStart(2, "0")}s`;
279
283
  }
280
284
 
281
285
  // ── processes ────────────────────────────────────────────────────────────────────────
282
286
 
283
287
  const quiet = (io: ToolchainIO, timeout?: number) => ({
284
288
  encoding: "utf8" as const,
285
- stdio: ["ignore", "pipe", "pipe"] as ["ignore", "pipe", "pipe"],
289
+ stdio: PIPED,
286
290
  env: io.env,
287
291
  maxBuffer: 64 * 1024 * 1024,
288
292
  ...(timeout ? { timeout } : {}),
289
293
  });
290
294
 
291
295
  /** The last lines a process printed — what a failure shows. */
292
- function tail(r: ReturnType<Runner>, n = 12): string[] {
293
- const text = `${String(r.stdout ?? "")}\n${String(r.stderr ?? "")}`;
296
+ function tail(
297
+ r: {
298
+ readonly stdout: string | undefined;
299
+ readonly stderr: string | undefined;
300
+ },
301
+ n = 12,
302
+ ): string[] {
303
+ const text = `${printed(r.stdout)}\n${printed(r.stderr)}`;
294
304
  return text
295
305
  .split("\n")
296
306
  .filter((l) => l.trim())
@@ -475,13 +485,15 @@ export function installPackages(
475
485
  if (noGaps(gaps)) break;
476
486
  const want = [...gaps.packages, ...gaps.tools, ...gaps.dependencies];
477
487
  const repairsBase = gaps.dependencies.length > 0;
478
- io.log(` tlmgr: installing ${want.length} package(s) from ${mirror}…`);
488
+ io.log(
489
+ ` tlmgr: installing ${String(want.length)} package(s) from ${mirror}…`,
490
+ );
479
491
  const r = io.run(
480
492
  join(tree.bin, "tlmgr"),
481
493
  ["--repository", mirror, "install", ...want],
482
494
  quiet(io, TLMGR_MS),
483
495
  );
484
- const output = `${String(r.stdout ?? "")}\n${String(r.stderr ?? "")}`;
496
+ const output = `${printed(r.stdout)}\n${printed(r.stderr)}`;
485
497
  // A new release: every mirror serves it, so trying the next one only repeats the refusal.
486
498
  const newer = releaseGap(output);
487
499
  if (newer) return { ok: false, newer };
@@ -567,7 +579,7 @@ export function ensureTexLive(
567
579
  if (!verified.ok) return fail(o.err, verified.lines);
568
580
  o.log(
569
581
  `✓ TeX Live ${used.year} is ready in ${used.dir}: ${verified.value} ` +
570
- `${sizeMB(used.dir)} MB, ${formatDuration(o.now() - started)}`,
582
+ `${String(sizeMB(used.dir))} MB, ${formatDuration(o.now() - started)}`,
571
583
  );
572
584
  o.log(binLine(used));
573
585
  if (used !== tree.value) o.log(leftInPlace(tree.value));
@@ -638,14 +650,14 @@ function verify(
638
650
  return {
639
651
  ok: true,
640
652
  value:
641
- `${packageNames(tex).length} packages verified ` +
642
- `(${files} files found by kpsewhich, ${Object.keys(tex.tools).length} tools),`,
653
+ `${String(packageNames(tex).length)} packages verified ` +
654
+ `(${String(files)} files found by kpsewhich, ${String(Object.keys(tex.tools).length)} tools),`,
643
655
  };
644
656
  }
645
657
 
646
658
  /** The superseded tree is named, with the space it holds — and left alone. */
647
659
  export const leftInPlace = (old: CachedTree): string =>
648
- ` TeX Live ${old.year} in ${old.dir} is left in place and no longer used (${sizeMB(old.dir)} MB); ` +
660
+ ` TeX Live ${old.year} in ${old.dir} is left in place and no longer used (${String(sizeMB(old.dir))} MB); ` +
649
661
  `delete that directory to free the space`;
650
662
 
651
663
  /**
@@ -693,14 +705,14 @@ function report(o: Resolved, tree: CachedTree | null): number {
693
705
  const n = packageNames(o.tex).length;
694
706
  if (!tree) {
695
707
  o.log(
696
- `✗ no TeX Live in ${cacheRoot(o.env, o.home)} — \`npx paperlint toolchain\` installs ${n} packages (~230 MB, ~2 min)`,
708
+ `✗ no TeX Live in ${cacheRoot(o.env, o.home)} — \`npx paperlint toolchain\` installs ${String(n)} packages (~230 MB, ~2 min)`,
697
709
  );
698
710
  return 1;
699
711
  }
700
712
  const gaps = gapsOf(tree, o.tex, o.run);
701
713
  if (noGaps(gaps)) {
702
714
  o.log(
703
- `✓ TeX Live ${tree.year} in ${tree.dir} has all ${n} declared packages`,
715
+ `✓ TeX Live ${tree.year} in ${tree.dir} has all ${String(n)} declared packages`,
704
716
  );
705
717
  o.log(binLine(tree));
706
718
  return 0;
@@ -709,9 +721,11 @@ function report(o: Resolved, tree: CachedTree | null): number {
709
721
  // A dependency the install left out is not one of the venue's declared packages: counted apart,
710
722
  // so the line never reads "lacks 0 of 51" followed by a name.
711
723
  const what = [
712
- ...(declared ? [`${declared} of ${n} declared packages`] : []),
724
+ ...(declared
725
+ ? [`${String(declared)} of ${String(n)} declared packages`]
726
+ : []),
713
727
  ...(gaps.dependencies.length
714
- ? [`${gaps.dependencies.length} TeX Live base package(s)`]
728
+ ? [`${String(gaps.dependencies.length)} TeX Live base package(s)`]
715
729
  : []),
716
730
  ].join(" and ");
717
731
  o.log(
@@ -759,13 +773,13 @@ function texPart(o: Resolved): number {
759
773
  // reason to install only when tlmgr refuses to add a newly declared package to the old tree.
760
774
  if (tree && complete(tree)) {
761
775
  o.log(
762
- `✓ TeX Live ${tree.year} in ${tree.dir} already has all ${n} declared packages — nothing to do`,
776
+ `✓ TeX Live ${tree.year} in ${tree.dir} already has all ${String(n)} declared packages — nothing to do`,
763
777
  );
764
778
  o.log(binLine(tree));
765
779
  return 0;
766
780
  }
767
781
  o.log(
768
- `paperlint toolchain: TeX Live with ${n} packages into ${cacheRoot(o.env, o.home)}`,
782
+ `paperlint toolchain: TeX Live with ${String(n)} packages into ${cacheRoot(o.env, o.home)}`,
769
783
  );
770
784
  return ensureTexLive(o.tex, o).ok ? 0 : 1;
771
785
  }