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
package/src/cli.test.ts CHANGED
@@ -9,7 +9,7 @@ import { spawn } from "node:child_process";
9
9
  import { existsSync } from "node:fs";
10
10
  import { dirname, join } from "node:path";
11
11
  import { test } from "vitest";
12
- import { runNode, useTempDir, writeTree } from "../test/support.mjs";
12
+ import { runNode, useTempDir, writeTree } from "../test/support.ts";
13
13
  import {
14
14
  chooseVenue,
15
15
  initTexLive,
@@ -169,7 +169,12 @@ test("chooseVenue: a preset by path (inside and outside the paper), by prefixed
169
169
  const choose = (venue: string) =>
170
170
  chooseVenue(
171
171
  { venue, kind: null },
172
- { paperDir, cwd: dir, interactive: false, ask: async () => "" },
172
+ {
173
+ paperDir,
174
+ cwd: dir,
175
+ interactive: false,
176
+ ask: () => Promise.resolve(""),
177
+ },
173
178
  );
174
179
  const results = await Promise.all([
175
180
  choose("./papers/p/mine.jsonc"),
@@ -354,11 +359,11 @@ test("a paper's paperlint.json that does not parse stops the lint with its path"
354
359
  });
355
360
  const r = paperRuleBlocks([join(dir, "p")]);
356
361
  assert.equal(r.ok, false);
357
- assert.match(!r.ok ? r.error : "", /^.*rules-broken\/p\/paperlint\.json: /);
362
+ assert.match(r.error, /^.*rules-broken\/p\/paperlint\.json: /);
358
363
  });
359
364
 
360
365
  test("silentOptionalRules: a config with no rules turns nothing on", async () => {
361
- assert.deepEqual(await silentOptionalRules({} as never, [], {} as never), []);
366
+ assert.deepEqual(await silentOptionalRules({} as never, [], {}), []);
362
367
  });
363
368
 
364
369
  test("init with no path and --format md sets up the current directory", async () => {
@@ -493,7 +498,9 @@ function newAtTerminal(cwd: string, answers: Record<string, string>) {
493
498
  child.stdin.write(answer);
494
499
  }
495
500
  });
496
- child.on("close", (status) => done({ status, out }));
501
+ child.on("close", (status) => {
502
+ done({ status, out });
503
+ });
497
504
  });
498
505
  }
499
506
 
@@ -563,7 +570,7 @@ test("lint with its LaTeX language missing from the install still lints the mark
563
570
  "papers/p/paper.md": "# P\n\nSome prose.\n",
564
571
  "no-latex.mjs":
565
572
  "import { register } from 'node:module';\n" +
566
- "register('data:text/javascript,' + encodeURIComponent(\"export async function resolve(s, c, next) { if (s.endsWith('latex-language.mjs')) throw Object.assign(new Error('gone'), { code: 'ERR_MODULE_NOT_FOUND' }); return next(s, c); }\"));\n",
573
+ "register('data:text/javascript,' + encodeURIComponent(\"export async function resolve(s, c, next) { if (s === '#eslint-rules/latex-language') throw Object.assign(new Error('gone'), { code: 'ERR_MODULE_NOT_FOUND' }); return next(s, c); }\"));\n",
567
574
  });
568
575
  const without = runNode(BIN, ["lint", "papers"], {
569
576
  cwd: dir,
package/src/cli.ts CHANGED
@@ -30,7 +30,7 @@ import { spawnSync } from "node:child_process";
30
30
  import { fileURLToPath } from "node:url";
31
31
  import { join, dirname, resolve, relative, basename, sep } from "node:path";
32
32
  import markdown from "@eslint/markdown";
33
- // Types come from consumer.d.mts beside it, the same arrangement as lib/paper-config.d.mts.
33
+ // Types come from consumer.d.mts beside it.
34
34
  import {
35
35
  isMain,
36
36
  packageVenuesDir,
@@ -96,15 +96,16 @@ import {
96
96
  type PaperFormat,
97
97
  type VenueSetting,
98
98
  } from "./new-paper.ts";
99
- // The one source for the consumer's config key lives in the .mjs half of the package (the ESLint
100
- // rules and the skill scripts import it too); its types are in lib/paper-config.d.mts.
99
+ // The one source for the consumer's config key lives in lib/ (the ESLint rules and the skill scripts
100
+ // import it too). It is imported compiled, from dist/, the one path that resolves the same from src/
101
+ // and from dist/ — see CONTRIBUTING.md.
101
102
  import {
102
103
  CONFIG_FILE,
103
104
  DEFAULT_PAPERS_ROOT,
104
105
  PAPERS_DIR_FIELD,
105
106
  SETTINGS_KEYS,
106
107
  findProjectRoot,
107
- } from "../lib/paper-config.mjs";
108
+ } from "#lib/paper-config";
108
109
  import {
109
110
  parseRuleBlocks,
110
111
  parseRuleEntries,
@@ -117,22 +118,15 @@ import {
117
118
  export { init };
118
119
  export { nextSteps } from "./init.ts";
119
120
 
120
- // @ts-expect-error — an ESLint rule in .mjs, it has no types
121
- import paperStages from "../eslint-rules/paper-stages.mjs";
122
- // @ts-expect-error — an ESLint rule in .mjs, it has no types
123
- import researchQuestion from "../eslint-rules/paper-research-question.mjs";
124
- // @ts-expect-error — an ESLint rule in .mjs, it has no types
125
- import typography from "../eslint-rules/paper-typography.mjs";
126
- // @ts-expect-error — an ESLint rule in .mjs, it has no types
127
- import texBuild from "../eslint-rules/tex-build.mjs";
128
- // @ts-expect-error — an ESLint rule in .mjs, it has no types
129
- import bibReachable from "../eslint-rules/bib-reachable-entry.mjs";
130
- // @ts-expect-error — an ESLint rule in .mjs, it has no types
131
- import reviewFrontmatter from "../eslint-rules/review-frontmatter.mjs";
132
- // @ts-expect-error — an ESLint rule in .mjs, it has no types
133
- import siblingFrontmatter from "../eslint-rules/sibling-frontmatter.mjs";
134
- // @ts-expect-error — an ESLint rule in .mjs, it has no types
135
- import pdfRules from "../eslint-rules/pdf-last-page-balance.mjs";
121
+ import paperStages from "#eslint-rules/paper-stages";
122
+ import researchQuestion from "#eslint-rules/paper-research-question";
123
+ import typography from "#eslint-rules/paper-typography";
124
+ import texBuild from "#eslint-rules/tex-build";
125
+ import bibReachable from "#eslint-rules/bib-reachable-entry";
126
+ import reviewFrontmatter from "#eslint-rules/review-frontmatter";
127
+ import siblingFrontmatter from "#eslint-rules/sibling-frontmatter";
128
+ import pdfRules from "#eslint-rules/pdf-last-page-balance";
129
+ import { messageOf } from "./domain/text.ts";
136
130
 
137
131
  /** The shipped venue presets, read from the package's venues directory — the one list. */
138
132
  const SHIPPED_VENUES = (): string[] => shippedPresets(packageVenuesDir());
@@ -244,8 +238,7 @@ export function buildConfig(
244
238
  languageOptions: { frontmatter: "yaml" },
245
239
  };
246
240
 
247
- // eslint-disable-next-line @typescript-eslint/no-explicit-any -- #49: replace with a real type
248
- const cfg: any[] = [
241
+ const cfg: unknown[] = [
249
242
  // 🔴 THE PROJECT'S PAPER TEMPLATE IS NOT A PAPER. `paperlint new` reads `<papers>/.template/`, and
250
243
  // its files carry every marker a paper does. Flat config does NOT ignore dot-directories by
251
244
  // default (only `node_modules/` and `.git/`), so without this block `paperlint lint` would lint the
@@ -392,7 +385,9 @@ export function rulesOff(texLanguage?: unknown): Linter.Config[] {
392
385
  return [
393
386
  {
394
387
  name: "paperlint/rules-off",
395
- plugins: rulePlugins(texLanguage) as Linter.Config["plugins"],
388
+ plugins: rulePlugins(texLanguage) as NonNullable<
389
+ Linter.Config["plugins"]
390
+ >,
396
391
  rules: Object.fromEntries([...SHIPPED_RULES].map((id) => [id, "off"])),
397
392
  },
398
393
  {
@@ -405,7 +400,7 @@ export function rulesOff(texLanguage?: unknown): Linter.Config[] {
405
400
 
406
401
  /** Whether a rule entry (`"error"`, `2`, `["warn", {…}]`) turns the rule on. */
407
402
  const isOn = (entry: unknown): boolean => {
408
- const sev = Array.isArray(entry) ? entry[0] : entry;
403
+ const sev: unknown = Array.isArray(entry) ? entry[0] : entry;
409
404
  return sev !== undefined && sev !== "off" && sev !== 0;
410
405
  };
411
406
 
@@ -470,7 +465,10 @@ export function paperRuleBlocks(
470
465
  existsSync(p) && statSync(p).isFile() ? dirname(p) : p,
471
466
  );
472
467
  const papers = [...new Set(dirs.flatMap((p) => [p, ...papersIn(p)]))];
473
- const out = { preset: [] as RuleBlock[], own: [] as RuleBlock[] };
468
+ const out: { preset: RuleBlock[]; own: RuleBlock[] } = {
469
+ preset: [],
470
+ own: [],
471
+ };
474
472
  for (const dir of papers) {
475
473
  const p = paperPreset(dir, PRESET_DEPS);
476
474
  if (p.kind === "settings-problem")
@@ -701,8 +699,13 @@ export function ownVersion(
701
699
  readFileSync(new URL("../package.json", import.meta.url), "utf8"),
702
700
  ): string | undefined {
703
701
  try {
704
- const v = JSON.parse(readManifest())?.version;
705
- return typeof v === "string" ? v : undefined;
702
+ const manifest: unknown = JSON.parse(readManifest());
703
+ return typeof manifest === "object" &&
704
+ manifest !== null &&
705
+ "version" in manifest &&
706
+ typeof manifest.version === "string"
707
+ ? manifest.version
708
+ : undefined;
706
709
  } catch {
707
710
  return undefined;
708
711
  }
@@ -747,7 +750,7 @@ export function readConfig(
747
750
  try {
748
751
  parsed = JSON.parse(readFileSync(configPath, "utf8"));
749
752
  } catch (e) {
750
- err(`${configPath} is not valid JSON: ${(e as Error).message}`);
753
+ err(`${configPath} is not valid JSON: ${messageOf(e)}`);
751
754
  return { code: 2 };
752
755
  }
753
756
  // The discovered config is NAMED out loud. Otherwise a run from someone else's directory picks
@@ -787,7 +790,9 @@ export const papersRoots = (cfg: {
787
790
  export function toPaths(papers: unknown): string[] {
788
791
  if (typeof papers === "string") return papers.trim() ? [papers.trim()] : [];
789
792
  if (Array.isArray(papers))
790
- return papers.filter((x) => typeof x === "string" && x.trim());
793
+ return papers.filter(
794
+ (x): x is string => typeof x === "string" && x.trim() !== "",
795
+ );
791
796
  return [];
792
797
  }
793
798
 
@@ -1453,8 +1458,7 @@ export async function run(
1453
1458
  // failing outright — the LaTeX rules simply have no language to run in.
1454
1459
  let texLanguage: unknown = null;
1455
1460
  try {
1456
- // @ts-expect-error — the module is .mjs and has no types
1457
- ({ texLanguage } = await import("../eslint-rules/latex-language.mjs"));
1461
+ ({ texLanguage } = await import("#eslint-rules/latex-language"));
1458
1462
  } catch {
1459
1463
  texLanguage = null;
1460
1464
  }
@@ -1478,8 +1482,7 @@ export async function run(
1478
1482
  // reached, which is what the very first run over an empty directory showed: instead of a clear
1479
1483
  // message a stack from the depths of eslint-helpers.js flew out. A failure stays a failure, but
1480
1484
  // an explicable one.
1481
- // eslint-disable-next-line @typescript-eslint/no-explicit-any -- #49: replace with a real type
1482
- let results: any[];
1485
+ let results: ESLint.LintResult[];
1483
1486
  try {
1484
1487
  results = await eslint.lintFiles(paths);
1485
1488
  } catch (e) {
@@ -1555,8 +1558,7 @@ const isEmptySet = (e: unknown): boolean => {
1555
1558
  */
1556
1559
  async function reportLint(
1557
1560
  eslint: ESLint,
1558
- // eslint-disable-next-line @typescript-eslint/no-explicit-any -- #49: replace with a real type
1559
- results: any[],
1561
+ results: ESLint.LintResult[],
1560
1562
  structure: ReturnType<typeof checkStructure>,
1561
1563
  {
1562
1564
  a,
@@ -1598,7 +1600,7 @@ async function reportLint(
1598
1600
  out.trim() ||
1599
1601
  (structure.length > 0
1600
1602
  ? ""
1601
- : `✓ ${results.length} file(s) checked, no findings`),
1603
+ : `✓ ${String(results.length)} file(s) checked, no findings`),
1602
1604
  );
1603
1605
  }
1604
1606
  if (structure.length > 0 || results.some((r) => r.errorCount > 0)) return 1;
@@ -1608,7 +1610,7 @@ async function reportLint(
1608
1610
  const warnings = results.reduce((n, r) => n + r.warningCount, 0);
1609
1611
  if (warnings > a.maxWarnings) {
1610
1612
  err(
1611
- `${warnings} warning(s) exceed the --max-warnings limit of ${a.maxWarnings}`,
1613
+ `${String(warnings)} warning(s) exceed the --max-warnings limit of ${String(a.maxWarnings)}`,
1612
1614
  );
1613
1615
  return 1;
1614
1616
  }
@@ -10,7 +10,7 @@
10
10
  *
11
11
  * Run: npx vigiles test src/doctor.harness.mjs
12
12
  */
13
- import { PAPERS_DIR_FIELD } from "../lib/paper-config.mjs";
13
+ import { PAPERS_DIR_FIELD } from "../lib/paper-config.ts";
14
14
  import {
15
15
  mkdtempSync,
16
16
  mkdirSync,
@@ -21,7 +21,7 @@ import {
21
21
  import { tmpdir } from "node:os";
22
22
  import { dirname, join } from "node:path";
23
23
  import { fileURLToPath } from "node:url";
24
- import { createChecker } from "../lib/check.mjs";
24
+ import { createChecker } from "../lib/check.ts";
25
25
 
26
26
  const HERE = dirname(fileURLToPath(import.meta.url));
27
27
  const { doctor, detectPapers, PROGRAMS, found } = await import(
@@ -279,7 +279,7 @@ const runDoctor = (
279
279
 
280
280
  // ── VII-bis. SKILLS THAT ARE NOT LINKED ARE NAMED — AND DO NOT FAIL THE RUN ─────────────────
281
281
  // Before this section a consumer without `.claude/skills/` links had no `/paper-pipeline`, and
282
- // nothing anywhere said so. The link state itself is `link-skills.harness.mjs`'s subject; here
282
+ // nothing anywhere said so. The link state itself is `link-skills.harness.ts`'s subject; here
283
283
  // only doctor's REPORTING of it is judged, so the state is handed in rather than built on disk.
284
284
  {
285
285
  const dir = consumer({ papersDir: "papers", declared: "papers" });
package/src/doctor.ts CHANGED
@@ -36,6 +36,7 @@ import {
36
36
  import { PAPER_MARKERS } from "./build.ts";
37
37
  import { linkSkills, SKILLS_HOME, type LinkReport } from "./link-skills.ts";
38
38
  import { doctorHooks } from "./hooks-settings.ts";
39
+ import { messageOf } from "./domain/text.ts";
39
40
 
40
41
  export interface Program {
41
42
  readonly bin: string;
@@ -169,7 +170,7 @@ function declarationVerdict(raw: string | null): {
169
170
  } catch (e) {
170
171
  return {
171
172
  lines: [
172
- ` ✗ ${CONFIG_FILE} is not valid JSON: ${(e as Error).message}`,
173
+ ` ✗ ${CONFIG_FILE} is not valid JSON: ${messageOf(e)}`,
173
174
  ` the edit guard refuses every Bash command until it parses — fix it with Edit or Write`,
174
175
  ],
175
176
  bad: 1,
@@ -334,7 +335,7 @@ export function doctor({
334
335
  try {
335
336
  out.push(...doctorHooks(root));
336
337
  } catch (e) {
337
- out.push(`hooks`, ` ⚠ not checked — ${(e as Error).message}`);
338
+ out.push(`hooks`, ` ⚠ not checked — ${messageOf(e)}`);
338
339
  }
339
340
  out.push("");
340
341
  log(out.join("\n"));
@@ -31,6 +31,7 @@
31
31
  * never read as empty.
32
32
  */
33
33
  import { err, ok, type Result } from "./result.ts";
34
+ import { messageOf } from "./text.ts";
34
35
 
35
36
  export const LOOKUP_CACHE_SCHEMA = 1;
36
37
 
@@ -106,7 +107,7 @@ function sectionOf<T>(
106
107
  name: string,
107
108
  section: unknown,
108
109
  known: readonly string[],
109
- one: (o: Json, at: string) => Result<T, string>,
110
+ one: (o: Json, fetched: string, at: string) => Result<T, string>,
110
111
  ): Result<Map<string, T>, string> {
111
112
  if (!isObject(section)) return err(`\`${name}\`: expected an object`);
112
113
  const out = new Map<string, T>();
@@ -115,18 +116,23 @@ function sectionOf<T>(
115
116
  if (!isObject(v)) return err(`${at}\`: expected an object`);
116
117
  const extra = unknownField(v, known);
117
118
  if (extra !== undefined) return err(`${at}\`: unknown field \`${extra}\``);
118
- if (!isDate(v["fetched"]))
119
+ const fetched = v["fetched"];
120
+ if (!isDate(fetched))
119
121
  return err(`${at}.fetched\`: expected a date, YYYY-MM-DD`);
120
- const r = one(v, at);
122
+ const r = one(v, fetched, at);
121
123
  if (!r.ok) return r;
122
124
  out.set(key, r.value);
123
125
  }
124
126
  return ok(out);
125
127
  }
126
128
 
127
- const cachedResponse = (o: Json, at: string): Result<CachedResponse, string> =>
129
+ const cachedResponse = (
130
+ o: Json,
131
+ fetched: string,
132
+ at: string,
133
+ ): Result<CachedResponse, string> =>
128
134
  isObject(o["response"]) && o["response"]["transport"] === "ok"
129
- ? ok({ fetched: o["fetched"] as string, response: o["response"] })
135
+ ? ok({ fetched, response: o["response"] })
130
136
  : err(
131
137
  `${at}.response\`: expected a successful response ({"transport": "ok", …})`,
132
138
  );
@@ -137,7 +143,11 @@ const isHit = (h: unknown): h is DblpHit =>
137
143
  Array.isArray(h["authors"]) &&
138
144
  h["authors"].every((a) => typeof a === "string");
139
145
 
140
- function cachedDblp(o: Json, at: string): Result<CachedDblp, string> {
146
+ function cachedDblp(
147
+ o: Json,
148
+ fetched: string,
149
+ at: string,
150
+ ): Result<CachedDblp, string> {
141
151
  const { title, hits } = o;
142
152
  if (typeof title !== "string") return err(`${at}.title\`: expected a string`);
143
153
  if (!Array.isArray(hits)) return err(`${at}.hits\`: expected an array`);
@@ -147,9 +157,9 @@ function cachedDblp(o: Json, at: string): Result<CachedDblp, string> {
147
157
  `${at}.hits[${String(bad)}]\`: expected {venue, year, type, title: string; authors: string[]}`,
148
158
  );
149
159
  return ok({
150
- fetched: o["fetched"] as string,
160
+ fetched,
151
161
  title,
152
- hits: hits as DblpHit[],
162
+ hits: hits.filter(isHit),
153
163
  });
154
164
  }
155
165
 
@@ -159,7 +169,7 @@ export function parseLookupCache(text: string): Result<LookupCache, string> {
159
169
  try {
160
170
  json = JSON.parse(text);
161
171
  } catch (e) {
162
- return err(`not JSON: ${(e as Error).message}`);
172
+ return err(`not JSON: ${messageOf(e)}`);
163
173
  }
164
174
  if (!isObject(json))
165
175
  return err("expected an object with `schema`, `citations` and `dblp`");
@@ -186,8 +196,8 @@ export function parseLookupCache(text: string): Result<LookupCache, string> {
186
196
  return ok({ citations: citations.value, dblp: dblp.value });
187
197
  }
188
198
 
189
- const sorted = <T>(m: ReadonlyMap<string, T>): Record<string, T> =>
190
- Object.fromEntries([...m.keys()].sort().map((k) => [k, m.get(k) as T]));
199
+ const sorted = <T>(m: ReadonlyMap<string, T>): Record<string, T | undefined> =>
200
+ Object.fromEntries([...m.keys()].sort().map((k) => [k, m.get(k)]));
191
201
 
192
202
  /** The cache as the file holds it: keys sorted, so one new answer is a small diff. */
193
203
  export const serializeLookupCache = (c: LookupCache): string =>
@@ -3,7 +3,7 @@ import { test } from "vitest";
3
3
  import { andThen, err, map, match, ok, type Result } from "./result.ts";
4
4
 
5
5
  const half = (n: number): Result<number, string> =>
6
- n % 2 === 0 ? ok(n / 2) : err(`${n} is odd`);
6
+ n % 2 === 0 ? ok(n / 2) : err(`${String(n)} is odd`);
7
7
 
8
8
  test("map and andThen run on ok and pass an error through untouched", () => {
9
9
  assert.deepEqual(
@@ -24,7 +24,7 @@ test("map and andThen run on ok and pass an error through untouched", () => {
24
24
 
25
25
  test("match picks the branch by `ok`", () => {
26
26
  const say = (r: Result<number, string>) =>
27
- match(r, { ok: (n) => `got ${n}`, err: (e) => `no: ${e}` });
27
+ match(r, { ok: (n) => `got ${String(n)}`, err: (e) => `no: ${e}` });
28
28
  assert.equal(say(half(4)), "got 2");
29
29
  assert.equal(say(half(5)), "no: 5 is odd");
30
30
  });
@@ -0,0 +1,32 @@
1
+ import assert from "node:assert/strict";
2
+ import { test } from "vitest";
3
+ import { codeOf, firstLine, messageOf, printed } from "./text.ts";
4
+
5
+ test("messageOf: an Error's message, and anything else thrown as its text", () => {
6
+ assert.equal(messageOf(new Error("boom")), "boom");
7
+ // `throw "x"` and `throw 3` are legal JavaScript; `(e as Error).message` read them as undefined.
8
+ assert.equal(messageOf("plain string"), "plain string");
9
+ assert.equal(messageOf(3), "3");
10
+ });
11
+
12
+ test("codeOf: a string `code` only", () => {
13
+ assert.equal(
14
+ codeOf(Object.assign(new Error("x"), { code: "ENOENT" })),
15
+ "ENOENT",
16
+ );
17
+ assert.equal(codeOf(new Error("no code")), undefined);
18
+ assert.equal(codeOf({ code: 3 }), undefined);
19
+ assert.equal(codeOf(null), undefined);
20
+ assert.equal(codeOf("ENOENT"), undefined);
21
+ });
22
+
23
+ test("printed: a stream a failed spawn left undefined reads as empty text", () => {
24
+ assert.equal(printed(undefined), "");
25
+ assert.equal(printed(null), "");
26
+ assert.equal(printed("out"), "out");
27
+ });
28
+
29
+ test("firstLine: the first non-blank line, trimmed", () => {
30
+ assert.equal(firstLine("\n \n first \nsecond"), "first");
31
+ assert.equal(firstLine(""), "");
32
+ });
@@ -9,3 +9,26 @@ export const firstLine = (s: string): string =>
9
9
  .split("\n")
10
10
  .map((l) => l.trim())
11
11
  .find(Boolean) ?? "";
12
+
13
+ /**
14
+ * What a process printed on one stream, as text. Node types `spawnSync(…, { encoding })`'s
15
+ * `stdout`/`stderr` as `string`, but both are `undefined` when the spawn itself failed (measured:
16
+ * `spawnSync("no-such-program", [], { encoding: "utf8" }).stdout` is `undefined`). The parameter
17
+ * takes the stream as it really arrives, so the fallback guards a real case instead of reading
18
+ * as dead code against a type that lies.
19
+ */
20
+ export const printed = (stream: string | null | undefined): string =>
21
+ stream ?? "";
22
+
23
+ /** What a caught value says: an `Error`'s message, anything else as text (`catch` gets `unknown`). */
24
+ export const messageOf = (e: unknown): string =>
25
+ e instanceof Error ? e.message : String(e);
26
+
27
+ /** The `code` a Node error carries (`ENOENT`, `EACCES`, …), when it carries a string one. */
28
+ export const codeOf = (e: unknown): string | undefined =>
29
+ typeof e === "object" &&
30
+ e !== null &&
31
+ "code" in e &&
32
+ typeof e.code === "string"
33
+ ? e.code
34
+ : undefined;
@@ -5,7 +5,7 @@
5
5
  * 1. `resolveEngine` over `EngineFacts` — one row per branch of the order;
6
6
  * 2. the fact readers: `missingPackages` over kpsewhich output, `missingTools`, `whichOnPath`,
7
7
  * `probeTree` through a fake runner.
8
- * All pure or port-driven: no TeX needed. The real-TeX half is `test/e2e/build.mjs`.
8
+ * All pure or port-driven: no TeX needed. The real-TeX half is `test/e2e/build.ts`.
9
9
  */
10
10
  import {
11
11
  chmodSync,
@@ -18,7 +18,7 @@ import {
18
18
  import { tmpdir } from "node:os";
19
19
  import { dirname, join } from "node:path";
20
20
  import { fileURLToPath } from "node:url";
21
- import { createChecker } from "../lib/check.mjs";
21
+ import { createChecker } from "../lib/check.ts";
22
22
 
23
23
  const HERE = dirname(fileURLToPath(import.meta.url));
24
24
  const {
package/src/engine.ts CHANGED
@@ -24,6 +24,7 @@ import { spawnSync } from "node:child_process";
24
24
  import { accessSync, constants, statSync } from "node:fs";
25
25
  import { basename, delimiter, join } from "node:path";
26
26
  import type { TexRequirements } from "./tex-requirements.ts";
27
+ import { printed } from "./domain/text.ts";
27
28
 
28
29
  /** A process runner with `spawnSync`'s shape — the port the harness replaces. */
29
30
  export type Runner = typeof spawnSync;
@@ -161,7 +162,7 @@ export function probeTree(
161
162
  stdio: ["ignore", "pipe", "pipe"],
162
163
  });
163
164
  if (r.error) return Object.keys(packages).sort();
164
- return missingPackages(packages, String(r.stdout ?? ""));
165
+ return missingPackages(packages, printed(r.stdout));
165
166
  }
166
167
 
167
168
  /**
@@ -181,7 +182,7 @@ export function missingDependencies(
181
182
  stdio: ["ignore", "pipe", "pipe"],
182
183
  });
183
184
  if (r.error) return [];
184
- const lines = String(r.stdout ?? "").split("\n");
185
+ const lines = printed(r.stdout).split("\n");
185
186
  const start = lines.findIndex(
186
187
  (l) => l.trim() === "DEPENDS WITHOUT PACKAGES:",
187
188
  );
@@ -193,7 +194,7 @@ export function missingDependencies(
193
194
  return [
194
195
  ...new Set(
195
196
  (end < 0 ? section : section.slice(0, end)).map((l) =>
196
- l.split(" in: ")[0]!.trim(),
197
+ l.replace(/ in: [\s\S]*/, "").trim(),
197
198
  ),
198
199
  ),
199
200
  ].sort();
@@ -4,8 +4,8 @@
4
4
  * The document is assembled from hand-made measurements (pure), and `measurePaper` runs on
5
5
  * in-memory files with a FAKE pdf.js reader and a FAKE measurer, so each outcome — read failed, not
6
6
  * measured, measured — is chosen rather than hoped for. Reading real PDFs is
7
- * `pdf-facts.harness.mjs`'s job; banal as the measurer is `adapters/banal/index.test.ts`, and the
8
- * real banal is `test/e2e/banal.mjs`.
7
+ * `pdf-facts.harness.ts`'s job; banal as the measurer is `adapters/banal/index.test.ts`, and the
8
+ * real banal is `test/e2e/banal.ts`.
9
9
  */
10
10
  import assert from "node:assert/strict";
11
11
  import { readdirSync, readFileSync } from "node:fs";
@@ -147,10 +147,11 @@ test("🔴 geometry_source and the nine columns come from ONE branch", () => {
147
147
  const PAPER = "/r/papers/p";
148
148
  const PDF = `${PAPER}/paper.pdf`;
149
149
  const PDF_BYTES = "%PDF-pretend";
150
- const good: PdfReader = async () => ({
151
- ok: true,
152
- facts: { pages: 2, fonts: FONTS, last: lastPage(60, 60), layout: [] },
153
- });
150
+ const good: PdfReader = () =>
151
+ Promise.resolve({
152
+ ok: true,
153
+ facts: { pages: 2, fonts: FONTS, last: lastPage(60, 60), layout: [] },
154
+ });
154
155
  /** The shipped presets at their real paths: `extends` resolves through the same Files port. */
155
156
  const SHIPPED_PRESETS = Object.fromEntries(
156
157
  readdirSync(packageVenuesDir())
@@ -232,7 +233,8 @@ test("a failed read is the one error line, and nothing is written", async () =>
232
233
  const before = files.map.size;
233
234
  const m = await measurePaper(PAPER, PDF, {
234
235
  ...o,
235
- readPdf: async () => ({ ok: false, reason: "unreadable", detail: "boom" }),
236
+ readPdf: () =>
237
+ Promise.resolve({ ok: false, reason: "unreadable", detail: "boom" }),
236
238
  });
237
239
  assert.match(!m.ok ? m.error : "", /pdf\.js \(unreadable\): boom/);
238
240
  assert.equal(files.map.size, before);
package/src/facts-file.ts CHANGED
@@ -50,6 +50,7 @@ import type { Files } from "./ports/files.ts";
50
50
  import { err, ok, type Result } from "./domain/result.ts";
51
51
  import { paperPreset, paperPresetProblem } from "./presets.ts";
52
52
  import { packageVenuesDir } from "../skills/paper-pipeline/scripts/consumer.mjs";
53
+ import { messageOf } from "./domain/text.ts";
53
54
 
54
55
  export const FACTS_SCHEMA = 2;
55
56
  export const FACTS_DIR = "_build";
@@ -338,7 +339,7 @@ export function parseFactsText(text: string): Result<ReadFacts, FactsProblem> {
338
339
  try {
339
340
  d = JSON.parse(text);
340
341
  } catch (e) {
341
- return err({ kind: "broken", why: `not JSON (${(e as Error).message})` });
342
+ return err({ kind: "broken", why: `not JSON (${messageOf(e)})` });
342
343
  }
343
344
  if (!isRecord(d)) return err({ kind: "broken", why: "not a JSON object" });
344
345
  if (d["schema"] !== FACTS_SCHEMA)