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
@@ -7,7 +7,7 @@
7
7
  * and a suite-breaking change was pushed. Writing the subset down in a doc would have produced
8
8
  * the same outcome one release later — the list would simply have been wrong instead of absent.
9
9
  *
10
- * So the assertions below do not check that `check.mjs` contains the right strings. They pull
10
+ * So the assertions below do not check that `check.ts` contains the right strings. They pull
11
11
  * the job names OUT OF `.github/workflows/ci.yml` and require every one to be accounted for.
12
12
  * Add a job to CI and this goes red the same day, naming the job nobody covered.
13
13
  *
@@ -18,13 +18,50 @@ import { existsSync, readdirSync, readFileSync } from "node:fs";
18
18
  import { dirname, join } from "node:path";
19
19
  import { fileURLToPath } from "node:url";
20
20
  import { load } from "js-yaml";
21
- import { createChecker } from "../lib/check.mjs";
21
+ import { z } from "zod";
22
+ import { createChecker } from "../lib/check.ts";
23
+ import type { Gate } from "./check.ts";
24
+
25
+ /** A workflow file, as far as these checks read it. `on` is a plain key: js-yaml reads YAML 1.2. */
26
+ const Step = z.looseObject({
27
+ name: z.string().optional(),
28
+ run: z.string().optional(),
29
+ uses: z.string().optional(),
30
+ });
31
+ const Workflow = z.looseObject({
32
+ on: z
33
+ .looseObject({
34
+ pull_request: z
35
+ .looseObject({ types: z.array(z.string()).optional() })
36
+ .nullish(),
37
+ })
38
+ .optional(),
39
+ jobs: z
40
+ .record(
41
+ z.string(),
42
+ z.looseObject({
43
+ steps: z.array(Step).optional(),
44
+ strategy: z
45
+ .looseObject({
46
+ matrix: z
47
+ .looseObject({ os: z.array(z.string()).optional() })
48
+ .optional(),
49
+ })
50
+ .optional(),
51
+ }),
52
+ )
53
+ .optional(),
54
+ });
55
+ type Workflow = z.infer<typeof Workflow>;
56
+ const PackageJson = z.looseObject({
57
+ scripts: z.record(z.string(), z.string()).optional(),
58
+ });
22
59
 
23
60
  const HERE = dirname(fileURLToPath(import.meta.url));
24
61
  const ROOT = dirname(HERE);
25
62
 
26
63
  const { GATES, NOT_COVERED, outcome, SKIP_EXIT, commandOf } =
27
- await import("./check.mjs");
64
+ await import("./check.ts");
28
65
 
29
66
  const check = createChecker();
30
67
 
@@ -34,7 +71,7 @@ const check = createChecker();
34
71
  // command red for anyone without pnpm or TeX, and it would be ignored.
35
72
  check("exit 0 is a pass", outcome(0) === "pass");
36
73
  check(
37
- `exit ${SKIP_EXIT} is a skip — neither a pass nor a failure`,
74
+ `exit ${String(SKIP_EXIT)} is a skip — neither a pass nor a failure`,
38
75
  outcome(SKIP_EXIT) === "skip",
39
76
  );
40
77
  check("the skip code is the one vigiles' runner uses (77)", SKIP_EXIT === 77);
@@ -54,7 +91,10 @@ const WF_DIR = join(ROOT, ".github", "workflows");
54
91
  const workflows = Object.fromEntries(
55
92
  readdirSync(WF_DIR)
56
93
  .filter((f) => f.endsWith(".yml"))
57
- .map((f) => [f, load(readFileSync(join(WF_DIR, f), "utf-8"))]),
94
+ .map((f) => [
95
+ f,
96
+ Workflow.parse(load(readFileSync(join(WF_DIR, f), "utf-8"))),
97
+ ]),
58
98
  );
59
99
  const ciJobs = Object.values(workflows).flatMap((wf) =>
60
100
  Object.keys(wf.jobs ?? {}),
@@ -67,7 +107,8 @@ check(
67
107
  // ── macOS: a cell of the TeX job, on every push ─────────────────────────────────────────
68
108
  // install-tl-unx must work there, and issue #9 appears there alone. The trigger used to be a
69
109
  // separate once-per-PR workflow; the repository is public, so the minutes are free.
70
- const prTypes = (wf) => wf.on?.pull_request?.types ?? [];
110
+ const prTypes = (wf: Workflow | undefined): string[] =>
111
+ wf?.on?.pull_request?.types ?? [];
71
112
  const buildOs =
72
113
  workflows["ci.yml"]?.jobs?.["build-e2e"]?.strategy?.matrix?.os ?? [];
73
114
  check(
@@ -114,7 +155,9 @@ for (const job of Object.keys(NOT_COVERED)) {
114
155
  // A gate whose script or file was renamed fails at the moment someone runs it — which is
115
156
  // exactly the moment they are trusting it. Catch it here instead.
116
157
  const scripts =
117
- JSON.parse(readFileSync(join(ROOT, "package.json"), "utf-8")).scripts ?? {};
158
+ PackageJson.parse(
159
+ JSON.parse(readFileSync(join(ROOT, "package.json"), "utf-8")),
160
+ ).scripts ?? {};
118
161
  for (const g of GATES) {
119
162
  check(
120
163
  `gate «${g.name}» says how to run it in exactly one way (script or run)`,
@@ -130,7 +173,7 @@ for (const g of GATES) {
130
173
  // A `run` gate names files and programs directly. Every file it names must be on disk, and
131
174
  // every program other than `node` must be installed in node_modules/.bin.
132
175
  const argv = commandOf(g);
133
- const files = argv.filter((a) => /\.m?js$/.test(a));
176
+ const files = argv.filter((a) => /\.[cm]?[jt]s$/.test(a));
134
177
  const programs = [argv[0]];
135
178
  // Guards: a gate runs SOMETHING — a script of ours, or an installed program such as `tsc`.
136
179
  check(
@@ -159,6 +202,64 @@ for (const g of GATES.filter((g) => g.job === null)) {
159
202
  );
160
203
  }
161
204
 
205
+ // ── HALF FIVE: the gate's command RUNS in the job it names ─────────────────────────────────
206
+ // Half two proves the job exists; it does not prove the job runs the gate. `the tests type-check`
207
+ // claimed `gates` for weeks while no step of that job ran tsc — the claim was the only coverage.
208
+ // So each step's `run` is split into commands (lines, `&&`, `||`, `;`) and each command into
209
+ // tokens, and the gate's own command has to be one of them: `npm run <script>` (flags and extra
210
+ // arguments allowed) for a script gate, its argv (an `npx` in front and extra arguments allowed)
211
+ // for a run gate. Tokens, not a substring: `npm run lint` must not be found inside
212
+ // `npm run lint:skills`, nor a path inside a comment.
213
+ const commandsIn = (run: string | undefined): string[][] =>
214
+ (run ?? "")
215
+ .split(/\n|&&|\|\||;/)
216
+ .map((c) => c.trim().split(/\s+/).filter(Boolean))
217
+ .filter((t) => t.length > 0);
218
+ const stepsOf = (job: string) =>
219
+ Object.values(workflows).flatMap((wf) => wf.jobs?.[job]?.steps ?? []);
220
+ const startsWith = (tokens: readonly string[], prefix: readonly string[]) =>
221
+ prefix.every((p, i) => tokens[i] === p);
222
+ const runsGate = (tokens: readonly string[], g: Gate): boolean => {
223
+ if (g.script)
224
+ return (
225
+ tokens[0] === "npm" &&
226
+ tokens[1] === "run" &&
227
+ tokens.slice(2).find((t) => !t.startsWith("-")) === g.script
228
+ );
229
+ const argv = commandOf(g);
230
+ return (
231
+ startsWith(tokens, argv) ||
232
+ (tokens[0] === "npx" && startsWith(tokens.slice(1), argv))
233
+ );
234
+ };
235
+ for (const g of GATES) {
236
+ if (g.job === null) continue;
237
+ const steps = stepsOf(g.job);
238
+ const { inCi } = g;
239
+ if (inCi) {
240
+ // A gate CI runs as a side effect of another step, through an npm lifecycle script: the step
241
+ // it names must exist in the job and install the package (npm runs lifecycle scripts there),
242
+ // and the lifecycle script must be the gate's own command, so the two cannot drift apart.
243
+ const step = steps.find((s) => s.name === inCi.step);
244
+ const tokens = commandsIn(step?.run)[0] ?? [];
245
+ check(
246
+ `gate «${g.name}» runs in CI job «${g.job}» through step «${inCi.step}», whose ${inCi.lifecycle} script is the gate's command`,
247
+ step !== undefined &&
248
+ tokens[0] === "npm" &&
249
+ ["ci", "install"].includes(tokens[1] ?? "") &&
250
+ typeof scripts[g.script] === "string" &&
251
+ scripts[inCi.lifecycle] === scripts[g.script],
252
+ JSON.stringify({ run: step?.run, lifecycle: scripts[inCi.lifecycle] }),
253
+ );
254
+ continue;
255
+ }
256
+ check(
257
+ `gate «${g.name}» runs its command (${commandOf(g).join(" ")}) in a step of CI job «${g.job}»`,
258
+ steps.some((s) => commandsIn(s.run).some((t) => runsGate(t, g))),
259
+ steps.map((s) => s.run ?? `(uses ${String(s.uses)})`).join(" | "),
260
+ );
261
+ }
262
+
162
263
  // ── and the tail is not optional ───────────────────────────────────────────────────────────
163
264
  check(
164
265
  "NOT_COVERED is non-empty — if it ever is, either CI shrank or someone silenced the tail",
@@ -166,6 +267,6 @@ check(
166
267
  );
167
268
 
168
269
  console.log(
169
- `✓ ${String(check.count)} assertions passed — npm run check: ${GATES.length} gates, ` +
170
- `${ciJobs.length} CI job(s) all accounted for`,
270
+ `✓ ${String(check.count)} assertions passed — npm run check: ${String(GATES.length)} gates, ` +
271
+ `${String(ciJobs.length)} CI job(s) all accounted for`,
171
272
  );
@@ -1,21 +1,24 @@
1
1
  /**
2
- * check.mjs's runGates — the one command run before a push — on every outcome, with an injected
2
+ * check.ts's runGates — the one command run before a push — on every outcome, with an injected
3
3
  * spawn: a pass, a declared skip (77) and a failure, and THE TAIL that names what it does not cover.
4
4
  */
5
5
  import assert from "node:assert/strict";
6
6
  import { spawnSync } from "node:child_process";
7
7
  import { fileURLToPath } from "node:url";
8
8
  import { test } from "vitest";
9
- import { GATES, NOT_COVERED, runGates } from "./check.mjs";
9
+ import { GATES, NOT_COVERED, runGates, type Gate } from "./check.ts";
10
10
 
11
11
  const capture = () => {
12
- const out = [];
13
- const io = { log: (s) => out.push(s), err: (s) => out.push(`E ${s}`) };
12
+ const out: string[] = [];
13
+ const io = {
14
+ log: (s: string) => out.push(s),
15
+ err: (s: string) => out.push(`E ${s}`),
16
+ };
14
17
  return { out, io };
15
18
  };
16
19
 
17
20
  test("runGates: a pass, a declared skip (77) and a failure, then the tail naming what is not covered", () => {
18
- const gates = [
21
+ const gates: Gate[] = [
19
22
  { name: "passes", job: "gates", run: ["true"] },
20
23
  { name: "skips", job: "gates", run: ["skipper", "--x"] },
21
24
  {
@@ -25,12 +28,12 @@ test("runGates: a pass, a declared skip (77) and a failure, then the tail naming
25
28
  run: ["false"],
26
29
  },
27
30
  ];
28
- const status = { true: 0, skipper: 77, false: 3 };
29
- const written = [];
31
+ const status: Record<string, number> = { true: 0, skipper: 77, false: 3 };
32
+ const written: string[] = [];
30
33
  const { out, io } = capture();
31
34
  const code = runGates({
32
35
  gates,
33
- spawn: (cmd) => ({ status: status[cmd] }),
36
+ spawn: (cmd) => ({ status: status[cmd] ?? null }),
34
37
  write: (s) => written.push(s),
35
38
  ...io,
36
39
  });
@@ -79,7 +82,7 @@ test("runGates: all passing, with a skip, says how many passed and that the skip
79
82
  });
80
83
 
81
84
  test("run as a program with every gate passing: the plain verdict, exit 0", () => {
82
- // spawnSync is replaced before check.mjs loads, so the real file runs end to end — every gate
85
+ // spawnSync is replaced before check.ts loads, so the real file runs end to end — every gate
83
86
  // "passes" in microseconds instead of the eleven real ones running.
84
87
  const allPass = [
85
88
  'import cp from "node:child_process";',
@@ -92,7 +95,7 @@ test("run as a program with every gate passing: the plain verdict, exit 0", () =
92
95
  [
93
96
  "--import",
94
97
  `data:text/javascript,${encodeURIComponent(allPass)}`,
95
- fileURLToPath(new URL("./check.mjs", import.meta.url)),
98
+ fileURLToPath(new URL("./check.ts", import.meta.url)),
96
99
  ],
97
100
  { encoding: "utf8" },
98
101
  );
@@ -19,7 +19,7 @@
19
19
  * boundary reads as complete, which is the same class as a counter that counts what it ignores.
20
20
  *
21
21
  * ── WHAT KEEPS THE LIST FROM GOING STALE ───────────────────────────────────────────────────
22
- * `check.harness.mjs` sits beside it and pulls the job names OUT OF THE WORKFLOW ITSELF,
22
+ * `check.harness.ts` sits beside it and pulls the job names OUT OF THE WORKFLOW ITSELF,
23
23
  * requiring every one to be either covered here or named in `NOT_COVERED` with a reason. Add a
24
24
  * job to CI and the harness goes red the same day — the list cannot quietly fall behind.
25
25
  *
@@ -53,11 +53,63 @@ const ROOT = dirname(dirname(fileURLToPath(import.meta.url)));
53
53
  * Programs installed by npm (`vigiles`, `eslint`) are found because `node_modules/.bin` is put
54
54
  * first on PATH below, the same way `npm run` does it.
55
55
  */
56
- export const GATES = [
56
+ /**
57
+ * How CI runs a gate that has no step of its own: the step that runs it as a side effect, and the
58
+ * npm lifecycle script through which it does. `check.harness.ts` requires that step in the job
59
+ * and that lifecycle script to be the gate's own command, so the two cannot drift apart.
60
+ */
61
+ export interface RunInCi {
62
+ readonly step: string;
63
+ readonly lifecycle: "prepare";
64
+ }
65
+
66
+ /**
67
+ * A gate runs either an npm script or a command of its own — never both. Only an npm script can be
68
+ * run by CI through a lifecycle script (`inCi`); every other gate has a step that runs its command.
69
+ */
70
+ type GateCommand =
71
+ | {
72
+ readonly script: string;
73
+ readonly run?: undefined;
74
+ readonly inCi?: RunInCi;
75
+ }
76
+ | {
77
+ readonly run: readonly [string, ...string[]];
78
+ readonly script?: undefined;
79
+ readonly inCi?: undefined;
80
+ };
81
+
82
+ /**
83
+ * A gate reproduces a CI job, or runs only here and says why (`job: null` needs a `reason`, or it
84
+ * becomes the quiet way to drop something out of CI).
85
+ */
86
+ export type Gate = { readonly name: string } & GateCommand &
87
+ (
88
+ | { readonly job: string; readonly reason?: undefined }
89
+ | { readonly job: null; readonly reason: string }
90
+ );
91
+
92
+ /** What `runGates` needs from a finished process: its exit status, `null` when a signal ended it. */
93
+ export type Spawn = (
94
+ cmd: string,
95
+ args: readonly string[],
96
+ options: {
97
+ cwd: string;
98
+ stdio: "inherit";
99
+ encoding: "utf8";
100
+ env: NodeJS.ProcessEnv;
101
+ },
102
+ ) => { readonly status: number | null };
103
+
104
+ export type Outcome = "pass" | "skip" | "fail";
105
+
106
+ export const GATES: readonly Gate[] = [
57
107
  {
58
108
  name: "the package compiles",
59
109
  job: "gates",
60
110
  script: "build",
111
+ // CI has no `npm run build` step: `npm ci` runs `prepare`, which is `tsc` as well.
112
+ inCi: { step: "npm ci", lifecycle: "prepare" },
61
113
  // First because everything below imports from `dist/`. A stale build makes every later
62
114
  // failure a lie about its own cause.
63
115
  },
@@ -86,17 +138,6 @@ export const GATES = [
86
138
  job: "gates",
87
139
  run: ["node", "scripts/rules-see-files.mjs"],
88
140
  },
89
- {
90
- name: "rules read content, not the filesystem",
91
- job: null,
92
- // 🔴 Deliberately not in CI, and this is the one asymmetry worth stating rather than
93
- // hiding: the check is about a property of the rules' source, which cannot change between
94
- // a developer's tree and the runner's. Running it twice buys nothing; NOT running it
95
- // locally buys a defect that reaches review.
96
- reason:
97
- "source-only property — identical in every environment, so CI adds nothing",
98
- run: ["node", "scripts/rules-are-content-only.mjs"],
99
- },
100
141
  {
101
142
  name: "every test, under coverage — vitest, then the vigiles harnesses, with c8's thresholds (npm run coverage)",
102
143
  job: "gates",
@@ -114,40 +155,42 @@ export const GATES = [
114
155
  {
115
156
  name: "legacy layer exemptions are frozen — none new, none grown (#76)",
116
157
  job: "gates",
117
- run: ["node", "scripts/layer-legacy-frozen.mjs"],
158
+ run: ["node", "scripts/layer-legacy-frozen.ts"],
118
159
  },
119
160
  {
120
161
  name: "install e2e — pack, install under npm and pnpm, run the binary",
121
162
  job: "gates",
122
- run: ["node", "test/e2e/install.mjs"],
163
+ run: ["node", "test/e2e/install.ts"],
123
164
  },
124
165
  {
125
166
  name: "build e2e — a real pdflatex, and the PDF's fonts are measured",
126
167
  job: "build-e2e",
127
- run: ["node", "test/e2e/build.mjs"],
168
+ run: ["node", "test/e2e/build.ts"],
128
169
  },
129
170
  {
130
171
  name: "banal e2e — the real banal on pdf.js-written XML gives banal-on-pdftohtml's numbers",
131
172
  job: "build-e2e",
132
- run: ["node", "test/e2e/banal.mjs"],
173
+ run: ["node", "test/e2e/banal.ts"],
133
174
  },
134
175
  {
135
176
  name: "toolchain e2e — real TeX Live into $PAPERLINT_TEXLIVE_DIR, then a build with only it on PATH",
136
177
  job: "build-e2e",
137
- run: ["node", "test/e2e/toolchain.mjs"],
178
+ run: ["node", "test/e2e/toolchain.ts"],
138
179
  },
139
180
  ];
140
181
 
141
182
  /** The command line a gate runs, as an argument list. */
142
- export function commandOf(gate) {
143
- return gate.script ? ["npm", "run", "-s", gate.script] : gate.run;
183
+ export function commandOf(gate: Gate): readonly [string, ...string[]] {
184
+ return gate.script !== undefined
185
+ ? ["npm", "run", "-s", gate.script]
186
+ : gate.run;
144
187
  }
145
188
 
146
189
  /**
147
190
  * CI jobs this command does NOT reproduce. Each needs a reason a reader can check, because an
148
191
  * unexplained gap is indistinguishable from an oversight — which is what the harness enforces.
149
192
  */
150
- export const NOT_COVERED = {
193
+ export const NOT_COVERED: Readonly<Record<string, string>> = {
151
194
  // build-e2e's macOS cell is the one part of a covered job no local command can stand in for:
152
195
  // it exists because issue #9 appeared on macOS alone (/var is a symlink to /private/var).
153
196
  merge:
@@ -169,31 +212,30 @@ export const NOT_COVERED = {
169
212
  * this loop counted that as a pass, and `skipped` below was declared and never filled: a machine
170
213
  * without pnpm or TeX printed "all gates passed" for runs that never happened.
171
214
  */
172
- export const SKIP_EXIT = 77;
173
- export function outcome(status) {
215
+ export const SKIP_EXIT: number = 77;
216
+ export function outcome(status: number | null): Outcome {
174
217
  if (status === 0) return "pass";
175
218
  if (status === SKIP_EXIT) return "skip";
176
219
  return "fail";
177
220
  }
178
221
 
179
- /**
180
- * Run every gate in order and print the verdict and THE TAIL. `spawn`, `log`, `err` and `write`
181
- * are injected so a test can drive every outcome without running eleven real gates.
182
- *
183
- * @returns the exit code: 1 when a gate failed, else 0 (a skip is not a failure, and says so).
184
- */
185
- export function runGates({
186
- gates = GATES,
187
- spawn = spawnSync,
188
- log = console.log,
189
- err = console.error,
190
- write = (s) => process.stdout.write(s),
191
- } = {}) {
222
+ /** The gates that did not pass, each as the line the verdict prints, split by outcome. */
223
+ interface NotPassed {
224
+ failed: string[];
225
+ skipped: string[];
226
+ }
227
+
228
+ /** Run each gate in order; `write` announces it before it starts. */
229
+ function runEach(
230
+ gates: readonly Gate[],
231
+ spawn: Spawn,
232
+ write: (text: string) => unknown,
233
+ ): NotPassed {
192
234
  const BIN_FIRST_PATH = [join(ROOT, "node_modules", ".bin"), process.env.PATH]
193
235
  .filter(Boolean)
194
236
  .join(delimiter);
195
- const failed = [];
196
- const skipped = [];
237
+ const failed: string[] = [];
238
+ const skipped: string[] = [];
197
239
 
198
240
  for (const g of gates) {
199
241
  write(`── ${g.name}\n`);
@@ -210,10 +252,19 @@ export function runGates({
210
252
  const o = outcome(r.status);
211
253
  if (o === "pass") continue;
212
254
  const shown = commandOf(g).join(" ");
213
- if (o === "skip") skipped.push(`${g.name} (${shown} → ${SKIP_EXIT})`);
214
- else failed.push(`${g.name} (${shown} → ${r.status})`);
255
+ if (o === "skip")
256
+ skipped.push(`${g.name} (${shown} → ${String(SKIP_EXIT)})`);
257
+ else failed.push(`${g.name} (${shown} → ${String(r.status)})`);
215
258
  }
259
+ return { failed, skipped };
260
+ }
216
261
 
262
+ /** The verdict, then THE TAIL. */
263
+ function report(
264
+ gates: readonly Gate[],
265
+ { failed, skipped }: NotPassed,
266
+ { log, err }: { log: (line: string) => void; err: (line: string) => void },
267
+ ): void {
217
268
  log("");
218
269
  if (failed.length) {
219
270
  err("🔴 gates failed:");
@@ -221,7 +272,7 @@ export function runGates({
221
272
  } else {
222
273
  const passed = gates.length - skipped.length;
223
274
  log(
224
- `✓ ${passed} gate(s) passed${skipped.length ? `, ${skipped.length} SKIPPED — not run, not passed` : ""}`,
275
+ `✓ ${String(passed)} gate(s) passed${skipped.length ? `, ${String(skipped.length)} SKIPPED — not run, not passed` : ""}`,
225
276
  );
226
277
  }
227
278
  for (const s of skipped) log(`⏳ skipped: ${s}`);
@@ -234,7 +285,30 @@ export function runGates({
234
285
  for (const g of gates.filter((x) => x.job === null)) {
235
286
  log(` (and «${g.name}» runs ONLY here — ${g.reason})`);
236
287
  }
237
- return failed.length ? 1 : 0;
288
+ }
289
+
290
+ /**
291
+ * Run every gate in order and print the verdict and THE TAIL. `spawn`, `log`, `err` and `write`
292
+ * are injected so a test can drive every outcome without running eleven real gates.
293
+ *
294
+ * @returns the exit code: 1 when a gate failed, else 0 (a skip is not a failure, and says so).
295
+ */
296
+ export function runGates({
297
+ gates = GATES,
298
+ spawn = spawnSync,
299
+ log = console.log,
300
+ err = console.error,
301
+ write = (s: string) => process.stdout.write(s),
302
+ }: {
303
+ gates?: readonly Gate[];
304
+ spawn?: Spawn;
305
+ log?: (line: string) => void;
306
+ err?: (line: string) => void;
307
+ write?: (text: string) => unknown;
308
+ } = {}): number {
309
+ const notPassed = runEach(gates, spawn, write);
310
+ report(gates, notPassed, { log, err });
311
+ return notPassed.failed.length ? 1 : 0;
238
312
  }
239
313
 
240
314
  if (isMain(import.meta.url)) process.exit(runGates());