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
@@ -10,7 +10,6 @@ import {
10
10
  EMPTY_LOOKUP_CACHE,
11
11
  type LookupCache,
12
12
  } from "../../domain/lookup-cache.ts";
13
- // @ts-expect-error — a skill script in .mjs, it has no types
14
13
  import * as cites from "../../../skills/verify-citations/scripts/verify-cites.mjs";
15
14
 
16
15
  /** The verdicts of a cold run — an empty cache, the adapter the CLI wires. */
@@ -46,29 +45,29 @@ const dblp = (title: string, authors: string[]) =>
46
45
  /** Route by URL prefix; the longest prefix wins. */
47
46
  function fakeFetch(routes: Record<string, () => Response>): string[] {
48
47
  const calls: string[] = [];
49
- vi.stubGlobal("fetch", async (url: string) => {
48
+ vi.stubGlobal("fetch", (url: string) => {
50
49
  calls.push(url);
51
- const hit = Object.keys(routes)
52
- .filter((p) => url.startsWith(p))
53
- .sort((a, b) => b.length - a.length)[0];
54
- if (hit === undefined) return json(404);
55
- return routes[hit]!();
50
+ const hit = Object.entries(routes)
51
+ .filter(([p]) => url.startsWith(p))
52
+ .sort(([a], [b]) => b.length - a.length)[0];
53
+ return Promise.resolve(hit === undefined ? json(404) : hit[1]());
56
54
  });
57
55
  return calls;
58
56
  }
59
57
 
60
58
  /** Await `p` while fake time runs, so DBLP's pauses between requests elapse at once. */
61
59
  async function settle<T>(p: Promise<T>): Promise<T> {
62
- let done = false;
63
- const out = p.finally(() => (done = true));
64
- while (!done) await vi.advanceTimersByTimeAsync(1000);
60
+ // A field, not a `let`: the flag is set in a callback, which flow analysis cannot see.
61
+ const state = { done: false };
62
+ const out = p.finally(() => (state.done = true));
63
+ while (!state.done) await vi.advanceTimersByTimeAsync(1000);
65
64
  return out;
66
65
  }
67
66
 
68
67
  test("no service answers: not-checked, with the reason", async () => {
69
- vi.stubGlobal("fetch", async () => {
70
- throw new Error("getaddrinfo ENOTFOUND api.crossref.org");
71
- });
68
+ vi.stubGlobal("fetch", () =>
69
+ Promise.reject(new Error("getaddrinfo ENOTFOUND api.crossref.org")),
70
+ );
72
71
  assert.deepEqual(await checkOnly("@misc{k, title={T}}"), {
73
72
  kind: "not-checked",
74
73
  why: "the citation services cannot be reached (getaddrinfo ENOTFOUND api.crossref.org)",
@@ -104,19 +103,19 @@ test("each entry gets its existence and its authors: confirmed, fabricated, mism
104
103
  const r = await settle(checkOnly(bib));
105
104
  assert.equal(r.kind, "checked");
106
105
  const byKey = Object.fromEntries(
107
- (r.kind === "checked" ? r.entries : []).map((e) => [
108
- e.key,
109
- [e.exists, e.authors, e.why ?? ""],
110
- ]),
106
+ r.entries.map((e) => [e.key, [e.exists, e.authors, e.why ?? ""]]),
111
107
  );
112
108
  assert.deepEqual(
113
109
  {
114
- good: byKey["good"]!.slice(0, 2),
115
- fake: byKey["fake"]!.slice(0, 2),
116
- drift: byKey["drift"]!.slice(0, 2),
117
- driftWhy: String(byKey["drift"]![2]).split("; ").pop(),
118
- flaky: [byKey["flaky"]![1], String(byKey["flaky"]![2]).split("; ").pop()],
119
- pre: byKey["pre"]!.slice(0, 2),
110
+ good: byKey["good"]?.slice(0, 2),
111
+ fake: byKey["fake"]?.slice(0, 2),
112
+ drift: byKey["drift"]?.slice(0, 2),
113
+ driftWhy: String(byKey["drift"]?.[2]).split("; ").pop(),
114
+ flaky: [
115
+ byKey["flaky"]?.[1],
116
+ String(byKey["flaky"]?.[2]).split("; ").pop(),
117
+ ],
118
+ pre: byKey["pre"]?.slice(0, 2),
120
119
  },
121
120
  {
122
121
  good: ["true", "match"],
@@ -209,20 +208,14 @@ test(
209
208
  slowFetch(40);
210
209
  const bib = manyBib(20);
211
210
  const serial: string[] = [];
212
- for (const c of cites.parseBib(bib) as { id: string }[])
213
- serial.push(
214
- (
215
- (await cites.verifyCitationLive(c, { cache: {} })) as {
216
- verdict: string;
217
- }
218
- ).verdict,
219
- );
211
+ for (const c of cites.parseBib(bib))
212
+ serial.push((await cites.verifyCitationLive(c, { cache: {} })).verdict);
220
213
  const t0 = Date.now();
221
214
  const r = await checkOnly(bib);
222
215
  const ms = Date.now() - t0;
223
216
  assert.equal(r.kind, "checked");
224
217
  assert.deepEqual(
225
- r.kind === "checked" ? r.entries.map((e) => e.exists) : [],
218
+ r.entries.map((e) => e.exists),
226
219
  serial,
227
220
  );
228
221
  assert.ok(ms < (20 * 200) / 2, `${String(ms)} ms for 20 lookups`);
@@ -249,7 +242,7 @@ const PAPERS = [
249
242
  "@inproceedings{good, author={Ada Lovelace}, title={Good Paper}, booktitle={ICSE}, doi={10.1/good}}",
250
243
  "@inproceedings{other, author={Ada Lovelace}, title={Other Paper}, booktitle={ICSE}}",
251
244
  "@misc{pre, author={Ada Lovelace}, title={A Preprint}, journal={arXiv preprint}}",
252
- ];
245
+ ] as const;
253
246
  /** Every service answers; DBLP knows "Good Paper" and nothing else. */
254
247
  const answering = () =>
255
248
  fakeFetch({
@@ -414,7 +407,7 @@ test("a failed lookup is not cached: the next run asks it again", async () => {
414
407
  "https://api.crossref.org/": () => json(200, { message: { items: [] } }),
415
408
  "https://dblp.org/": () => json(500),
416
409
  });
417
- const bib = PAPERS[1]!;
410
+ const bib = PAPERS[1];
418
411
  const r = await settle(cached(bib, EMPTY_LOOKUP_CACHE));
419
412
  const failed: LookupCache = r.cache;
420
413
  assert.deepEqual(
@@ -427,10 +420,10 @@ test("a failed lookup is not cached: the next run asks it again", async () => {
427
420
  });
428
421
 
429
422
  test("offline with an incomplete cache: not-checked, and the cache comes back unchanged", async () => {
430
- vi.stubGlobal("fetch", async () => {
431
- throw new Error("getaddrinfo ENOTFOUND api.crossref.org");
432
- });
433
- const r = await cached(PAPERS[0]!, EMPTY_LOOKUP_CACHE);
423
+ vi.stubGlobal("fetch", () =>
424
+ Promise.reject(new Error("getaddrinfo ENOTFOUND api.crossref.org")),
425
+ );
426
+ const r = await cached(PAPERS[0], EMPTY_LOOKUP_CACHE);
434
427
  assert.deepEqual(r, {
435
428
  check: {
436
429
  kind: "not-checked",
@@ -35,29 +35,15 @@ import { sleep, todayUtc } from "./clock.io.ts";
35
35
 
36
36
  /** Citations looked up at once (#107). */
37
37
  const LOOKUPS_IN_FLIGHT = 6;
38
- // @ts-expect-error — a skill script in .mjs, it has no types
39
38
  import * as cites from "../../../skills/verify-citations/scripts/verify-cites.mjs";
40
- // @ts-expect-error — a skill script in .mjs, it has no types
39
+ import type { CiteVerdict } from "../../../skills/verify-citations/scripts/verify-cites.mjs";
41
40
  import * as authors from "../../../skills/verify-citations/scripts/bib-authors.mjs";
42
-
43
- interface CiteResult {
44
- readonly id: string;
45
- readonly verdict: "true" | "false" | "unresolvable";
46
- readonly reason?: string;
47
- /** "<service>: <reason>" for each question a refusing service did not answer (#120). */
48
- readonly refused?: readonly string[];
49
- }
50
- interface AuthorFinding {
51
- readonly key: string;
52
- readonly venue: string;
53
- readonly missing: readonly string[];
54
- readonly extra: readonly string[];
55
- readonly orderDiffers: boolean;
56
- }
57
- interface KeyWhy {
58
- readonly key: string;
59
- readonly why: string;
60
- }
41
+ import type {
42
+ AuthorBuckets,
43
+ AuthorFinding,
44
+ BibEntry,
45
+ } from "../../../skills/verify-citations/scripts/bib-authors.mjs";
46
+ import { messageOf } from "../../domain/text.ts";
61
47
 
62
48
  const describeAuthors = (f: AuthorFinding): string =>
63
49
  [
@@ -68,13 +54,6 @@ const describeAuthors = (f: AuthorFinding): string =>
68
54
  .filter(Boolean)
69
55
  .join("; ") + ` (DBLP: ${f.venue})`;
70
56
 
71
- interface AuthorBuckets {
72
- readonly findings: readonly AuthorFinding[];
73
- readonly skipped: readonly KeyWhy[];
74
- readonly unchecked: readonly KeyWhy[];
75
- readonly matched: readonly string[];
76
- }
77
-
78
57
  const authorsOf = (key: string, a: AuthorBuckets): EntryVerdict["authors"] =>
79
58
  a.findings.some((f) => f.key === key)
80
59
  ? "mismatch"
@@ -90,7 +69,7 @@ const authorsOf = (key: string, a: AuthorBuckets): EntryVerdict["authors"] =>
90
69
  * confirmed work: a refusal after a confirmation changed nothing.
91
70
  */
92
71
  const unconfirmed = (
93
- c: CiteResult | undefined,
72
+ c: CiteVerdict | undefined,
94
73
  ): readonly (string | undefined)[] =>
95
74
  c && c.verdict !== "true"
96
75
  ? [c.reason, ...(c.refused ?? []).map((r) => `not asked: ${r}`)]
@@ -99,7 +78,7 @@ const unconfirmed = (
99
78
  /** One entry's verdict from the two checkers' answers. */
100
79
  function entryVerdict(
101
80
  key: string,
102
- found: readonly CiteResult[],
81
+ found: readonly CiteVerdict[],
103
82
  a: AuthorBuckets,
104
83
  ): EntryVerdict {
105
84
  const c = found.find((x) => x.id === key);
@@ -119,27 +98,21 @@ function entryVerdict(
119
98
  };
120
99
  }
121
100
 
122
- interface BibAuthorsEntry {
123
- readonly key: string;
124
- readonly title: string;
125
- readonly author: string;
126
- }
127
-
128
101
  /** Whether bib-authors will ask DBLP about this entry — the filter its `checkAuthors` applies. */
129
- const asksDblp = (e: BibAuthorsEntry): boolean =>
102
+ const asksDblp = (e: BibEntry): boolean =>
130
103
  Boolean(e.title && e.author) &&
131
- (authors.claimsPublished(e) as boolean) &&
132
- !(authors.truncated(e.author) as boolean);
104
+ authors.claimsPublished(e) &&
105
+ !authors.truncated(e.author);
133
106
 
134
107
  /** Is every question this bibliography would ask already answered in `cache`? */
135
108
  async function fullyCached(
136
- citations: readonly object[],
137
- entries: readonly BibAuthorsEntry[],
109
+ citations: readonly cites.Citation[],
110
+ entries: readonly BibEntry[],
138
111
  cache: LookupCache,
139
112
  store: Readonly<Record<string, unknown>>,
140
113
  ): Promise<boolean> {
141
114
  const asks = await Promise.all(
142
- citations.map((c) => cites.wouldAsk(c, store) as Promise<boolean>),
115
+ citations.map((c) => cites.wouldAsk(c, store)),
143
116
  );
144
117
  return (
145
118
  !asks.includes(true) &&
@@ -185,11 +158,11 @@ function cachedDblp(cache: LookupCache, today: () => string) {
185
158
  if (refused !== null)
186
159
  throw new Error(`DBLP refused earlier in this run (${refused})`);
187
160
  try {
188
- const hits = (await authors.dblpHits(title)) as DblpHit[];
161
+ const hits = await authors.dblpHits(title);
189
162
  dblp.set(dblpTitleKey(title), { fetched: today(), title, hits });
190
163
  return hits;
191
164
  } catch (e) {
192
- refused = (e as Error).message;
165
+ refused = messageOf(e);
193
166
  throw new Error(`DBLP refused earlier in this run (${refused})`);
194
167
  }
195
168
  },
@@ -234,10 +207,8 @@ export const referencesChecker =
234
207
  async (bib, cache) => {
235
208
  // Answers past MAX_AGE_DAYS are left out, so the run asks them again (and `grown` re-dates them).
236
209
  const usable = freshPart(cache, today());
237
- const citations = (cites.parseBib(bib) as { id?: string }[]).filter(
238
- (c) => c.id,
239
- );
240
- const parsed = authors.parseBib(bib) as BibAuthorsEntry[];
210
+ const citations = cites.parseBib(bib);
211
+ const parsed = authors.parseBib(bib);
241
212
  const store: Record<string, unknown> = Object.fromEntries(
242
213
  [...usable.citations].map(([k, v]) => [k, v.response]),
243
214
  );
@@ -252,22 +223,19 @@ export const referencesChecker =
252
223
  // answer.
253
224
  // One breaker for the run: a service that refuses is not asked again, not once per citation
254
225
  // (#120). Requests already in flight when it starts refusing still land — at most six.
255
- const breaker = cites.createBreaker() as unknown;
226
+ const breaker = cites.createBreaker();
256
227
  const d = cachedDblp(usable, today);
257
228
  const [found, a] = await Promise.all([
258
- mapLimit(
259
- citations,
260
- LOOKUPS_IN_FLIGHT,
261
- (c) =>
262
- cites.verifyCitationLive(c, {
263
- cache: store,
264
- breaker,
265
- }) as Promise<CiteResult>,
229
+ mapLimit(citations, LOOKUPS_IN_FLIGHT, (c) =>
230
+ cites.verifyCitationLive(c, {
231
+ cache: store,
232
+ breaker,
233
+ }),
266
234
  ),
267
235
  authors.checkAuthors(parsed, {
268
236
  lookup: d.lookup,
269
237
  pause: d.pause,
270
- }) as Promise<AuthorBuckets>,
238
+ }),
271
239
  ]);
272
240
  const keys = new Set([
273
241
  ...found.map((c) => c.id),
@@ -10,12 +10,10 @@ export async function mapLimit<T, R>(
10
10
  fn: (item: T) => Promise<R>,
11
11
  ): Promise<R[]> {
12
12
  const out: R[] = new Array<R>(items.length);
13
- let next = 0;
13
+ // ONE iterator shared by every worker: each `next()` hands out the next index exactly once.
14
+ const queue = items.entries();
14
15
  const worker = async (): Promise<void> => {
15
- while (next < items.length) {
16
- const i = next++;
17
- out[i] = await fn(items[i] as T);
18
- }
16
+ for (const [i, item] of queue) out[i] = await fn(item);
19
17
  };
20
18
  await Promise.all(
21
19
  Array.from({ length: Math.min(limit, items.length) }, worker),
@@ -1,3 +1,4 @@
1
+ import { messageOf } from "../../domain/text.ts";
1
2
  /** Whether the citation services answer at all — the one request that decides "not checked". */
2
3
  export async function unreachable(): Promise<string | null> {
3
4
  try {
@@ -7,6 +8,6 @@ export async function unreachable(): Promise<string | null> {
7
8
  });
8
9
  return null;
9
10
  } catch (e) {
10
- return `the citation services cannot be reached (${(e as Error).message})`;
11
+ return `the citation services cannot be reached (${messageOf(e)})`;
11
12
  }
12
13
  }
@@ -19,7 +19,7 @@ import {
19
19
  import { tmpdir } from "node:os";
20
20
  import { basename, delimiter, dirname, join } from "node:path";
21
21
  import { fileURLToPath } from "node:url";
22
- import { createChecker } from "../lib/check.mjs";
22
+ import { createChecker } from "../lib/check.ts";
23
23
 
24
24
  const HERE = dirname(fileURLToPath(import.meta.url));
25
25
  const E = await import(join(HERE, "build-engine.ts"));
@@ -7,7 +7,7 @@ import assert from "node:assert/strict";
7
7
  import { chmodSync, mkdirSync, writeFileSync } from "node:fs";
8
8
  import { dirname, join } from "node:path";
9
9
  import { test } from "vitest";
10
- import { useTempDir } from "../test/support.mjs";
10
+ import { useTempDir } from "../test/support.ts";
11
11
  import { prepareEngine, refusal } from "./build-engine.ts";
12
12
 
13
13
  const work = useTempDir("paperlint-build-engine-test-");
@@ -34,7 +34,7 @@ async function go(o: Parameters<typeof prepareEngine>[0]) {
34
34
  err: (l) => err.push(l),
35
35
  platform: "linux",
36
36
  interactive: true,
37
- ask: async () => "y",
37
+ ask: () => Promise.resolve("y"),
38
38
  ...o,
39
39
  });
40
40
  return { r, out, err };
@@ -71,7 +71,7 @@ function withDefaults(o: EngineOptions): Resolved {
71
71
  const r = {
72
72
  dryRun: false,
73
73
  interactive: false,
74
- ask: async () => "",
74
+ ask: () => Promise.resolve(""),
75
75
  log: console.log,
76
76
  err: console.error,
77
77
  // eslint-disable-next-line no-restricted-globals -- legacy I/O, moves behind a port in #76
@@ -130,7 +130,7 @@ export function gatherFacts(o: Resolved): EngineFacts {
130
130
  }
131
131
 
132
132
  const names = (missing: readonly string[]): string =>
133
- `${missing.length} package(s): ${missing.join(", ")}`;
133
+ `${String(missing.length)} package(s): ${missing.join(", ")}`;
134
134
 
135
135
  /** The one line a run without a human gets. It names the command and what is missing. */
136
136
  export function refusal(d: EngineDecision, facts: EngineFacts): string {
@@ -4,7 +4,7 @@
4
4
  * The process runner is replaced by a fake that writes what pdflatex and bibtex would write, so
5
5
  * these assertions check the SHELL's decisions: what is run, with which environment, in which
6
6
  * order, and what is left on disk. Whether a real pdflatex produces a real PDF is the other half,
7
- * `test/e2e/build.mjs`.
7
+ * `test/e2e/build.ts`.
8
8
  *
9
9
  * 🔴 THE TWO THINGS PINNED DOWN HERE THAT A RETURNED OBJECT CANNOT SHOW: a paper-supplied
10
10
  * `build.sh` is never executed (checked by the trace it would leave on disk and by the list of
@@ -22,7 +22,7 @@ import {
22
22
  import { tmpdir } from "node:os";
23
23
  import { join, dirname, delimiter } from "node:path";
24
24
  import { fileURLToPath } from "node:url";
25
- import { createChecker } from "../lib/check.mjs";
25
+ import { createChecker } from "../lib/check.ts";
26
26
 
27
27
  const HERE = dirname(fileURLToPath(import.meta.url));
28
28
  const {
package/src/build.test.ts CHANGED
@@ -8,7 +8,7 @@ import assert from "node:assert/strict";
8
8
  import { existsSync, writeFileSync } from "node:fs";
9
9
  import { join } from "node:path";
10
10
  import { test } from "vitest";
11
- import { useTempDir, writeTree } from "../test/support.mjs";
11
+ import { useTempDir, writeTree } from "../test/support.ts";
12
12
  import {
13
13
  bibInput,
14
14
  buildPaper,
@@ -62,7 +62,7 @@ function bibtexRun(f: Fake, cwd: string) {
62
62
  ? { status: f.bibtex }
63
63
  : { status: f.bibtex, stdout: f.bibtexOut };
64
64
  }
65
- function fakeRun(f: Fake): BuildOptions["run"] {
65
+ function fakeRun(f: Fake): NonNullable<BuildOptions["run"]> {
66
66
  let pass = 0;
67
67
  return ((bin: string, _args: string[], opts: { cwd: string }) => {
68
68
  if (bin === f.missing)
@@ -103,18 +103,19 @@ const page = (words: number, numbered = 0) => ({
103
103
  });
104
104
  const reader =
105
105
  (last = page(80)): PdfReader =>
106
- async () => ({
107
- ok: true,
108
- facts: {
109
- pages: 1,
110
- fonts: {
111
- kind: "drawn",
112
- list: [{ kind: "embedded", name: "CMR10", program: "Type1" }],
106
+ () =>
107
+ Promise.resolve({
108
+ ok: true,
109
+ facts: {
110
+ pages: 1,
111
+ fonts: {
112
+ kind: "drawn",
113
+ list: [{ kind: "embedded", name: "CMR10", program: "Type1" }],
114
+ },
115
+ last,
116
+ layout: [],
113
117
  },
114
- last,
115
- layout: [],
116
- },
117
- });
118
+ });
118
119
  const quiet = () => {};
119
120
  /** buildPaper with fakes; the verdict line is what the command would print for it. */
120
121
  async function build(dir: string, f: Fake = {}, over: BuildOptions = {}) {
@@ -332,7 +333,7 @@ test("paths: a paper that IS the working directory is named by its full path", a
332
333
  const run = await buildPapers([dir], {
333
334
  cwd: dir,
334
335
  log: (l) => lines.push(l),
335
- engine: async () => null,
336
+ engine: () => Promise.resolve(null),
336
337
  });
337
338
  assert.deepEqual(
338
339
  [run, lines],
package/src/build.ts CHANGED
@@ -45,7 +45,7 @@ import { delimiter, join, relative } from "node:path";
45
45
  // eslint-disable-next-line boundaries/dependencies -- legacy layer, moves behind a port in #76
46
46
  import { getParser } from "@unified-latex/unified-latex-util-parse";
47
47
  import { packageVenuesDir } from "../skills/paper-pipeline/scripts/consumer.mjs";
48
- import { CONFIG_FILE } from "../lib/paper-config.mjs";
48
+ import { CONFIG_FILE } from "#lib/paper-config";
49
49
  import {
50
50
  declaredVenue,
51
51
  factsPath,
@@ -83,6 +83,10 @@ import {
83
83
  type Step,
84
84
  } from "./latex-loop.ts";
85
85
  import type { BuildResult, PlanLine } from "./types.ts";
86
+ import { messageOf, printed } from "./domain/text.ts";
87
+
88
+ /** stdin closed, stdout and stderr captured — the tuple type spawnSync's `stdio` wants. */
89
+ const PIPED: ["ignore", "pipe", "pipe"] = ["ignore", "pipe", "pipe"];
86
90
 
87
91
  /** A directory counts as a paper by the same markers as `structure.ts` — one shared dictionary. */
88
92
  export const PAPER_MARKERS = [
@@ -337,7 +341,7 @@ function spawnOptions(ctx: BuildContext) {
337
341
  return {
338
342
  cwd: ctx.paperDir,
339
343
  env: ctx.env,
340
- stdio: ["ignore", "pipe", "pipe"] as ["ignore", "pipe", "pipe"],
344
+ stdio: PIPED,
341
345
  encoding: "latin1" as const,
342
346
  maxBuffer: 64 * 1024 * 1024,
343
347
  };
@@ -376,19 +380,23 @@ function latexPass(
376
380
  exitCode === 0
377
381
  ? []
378
382
  : written === null
379
- ? printed(r)
383
+ ? lastPrinted(r)
380
384
  : errorExcerpt(log).map(fromLatin1),
381
385
  };
382
386
  }
383
387
 
388
+ /** The two streams of a finished run, as they really arrive (see `printed`). */
389
+ interface Streams {
390
+ readonly stdout: string | undefined;
391
+ readonly stderr: string | undefined;
392
+ }
393
+
384
394
  /**
385
395
  * The last lines pdflatex printed, for the run that wrote no log — it died before opening one (a
386
396
  * format it cannot load, a missing TeX Live file). Marked by NO_LOG, which the report keys on.
387
397
  */
388
- function printed(r: { stdout?: unknown; stderr?: unknown }): string[] {
389
- const lines = fromLatin1(
390
- `${String(r.stdout ?? "")}\n${String(r.stderr ?? "")}`,
391
- )
398
+ function lastPrinted(r: Streams): string[] {
399
+ const lines = fromLatin1(`${printed(r.stdout)}\n${printed(r.stderr)}`)
392
400
  .split("\n")
393
401
  .filter((l) => l.trim());
394
402
  return [NO_LOG, ...lines.slice(-8).map((l) => ` ${l}`)];
@@ -410,7 +418,7 @@ function bibtexPass(ctx: BuildContext, opts: SpawnOptions): Observation | null {
410
418
  after: hashes(ctx.paperDir),
411
419
  bib,
412
420
  errorLines:
413
- exitCode === 0 ? [] : bibtexExcerpt(fromLatin1(String(r.stdout ?? ""))),
421
+ exitCode === 0 ? [] : bibtexExcerpt(fromLatin1(printed(r.stdout))),
414
422
  };
415
423
  }
416
424
 
@@ -443,7 +451,7 @@ export function compile(ctx: BuildContext): {
443
451
  const NO_PDF = `pdflatex exited 0 but wrote no ${JOB}.pdf — does the document have any pages?`;
444
452
 
445
453
  const plural = (n: number, one: string, many: string): string =>
446
- `${n} ${n === 1 ? one : many}`;
454
+ `${String(n)} ${n === 1 ? one : many}`;
447
455
 
448
456
  export const compileStep: BuildStep = {
449
457
  name: "compile",
@@ -467,7 +475,7 @@ export const compileStep: BuildStep = {
467
475
  const bin = end.step === "latex" ? "pdflatex" : "bibtex";
468
476
  const where =
469
477
  end.cause.kind === "exit"
470
- ? `${bin} exited with ${end.cause.code}`
478
+ ? `${bin} exited with ${String(end.cause.code)}`
471
479
  : `${bin} did not converge`;
472
480
  const log = join(
473
481
  ctx.paperDir,
@@ -669,13 +677,14 @@ function baseDefaults({
669
677
  }
670
678
 
671
679
  /** Without a checker the record says so — never a pass. The CLI wires the real one. */
672
- const notWired: CheckReferences = async (_bib, cache) => ({
673
- check: {
674
- kind: "not-checked",
675
- why: "no reference checker was wired into this build",
676
- },
677
- cache,
678
- });
680
+ const notWired: CheckReferences = (_bib, cache) =>
681
+ Promise.resolve({
682
+ check: {
683
+ kind: "not-checked",
684
+ why: "no reference checker was wired into this build",
685
+ },
686
+ cache,
687
+ });
679
688
 
680
689
  /** banal as the measurer, wired from the build's environment: the one piece of root work left here (#76). */
681
690
  function defaultMeasurer(
@@ -762,7 +771,7 @@ export async function buildPaper(
762
771
  dir,
763
772
  status: "failed",
764
773
  plan: [],
765
- failure: { step: "facts", lines: [(e as Error).message] },
774
+ failure: { step: "facts", lines: [messageOf(e)] },
766
775
  };
767
776
  }
768
777
  for (const s of facts.ignoredScripts)
@@ -853,7 +862,7 @@ export function formatResult(r: BuildResult): string {
853
862
  function formatFailure(r: BuildResult): string {
854
863
  const [head, ...rest] = r.failure?.lines ?? ["failed"];
855
864
  return [
856
- ` ✗ ${r.failure?.step ?? "build"}: ${head}`,
865
+ ` ✗ ${r.failure?.step ?? "build"}: ${String(head)}`,
857
866
  ...rest.map((l) => ` ${l}`),
858
867
  ` ${PDF_REMOVED}`,
859
868
  ].join("\n");
@@ -28,8 +28,8 @@ import { spawnSync } from "node:child_process";
28
28
  import { tmpdir } from "node:os";
29
29
  import { join, dirname } from "node:path";
30
30
  import { fileURLToPath } from "node:url";
31
- import { PAPERS_DIR_FIELD, findProjectRoot } from "../lib/paper-config.mjs";
32
- import { createChecker } from "../lib/check.mjs";
31
+ import { PAPERS_DIR_FIELD, findProjectRoot } from "../lib/paper-config.ts";
32
+ import { createChecker } from "../lib/check.ts";
33
33
 
34
34
  const HERE = dirname(fileURLToPath(import.meta.url));
35
35
  const { run, parseArgs, buildConfig, nextSteps, toPaths, runHook } =
@@ -635,7 +635,7 @@ check(
635
635
 
636
636
  // ── 7-bis. THE SKILLS: WHAT WAS LINKED, AND WHAT WAS LEFT ALONE, BY NAME ────────────
637
637
  //
638
- // The links themselves are `link-skills.harness.mjs`'s subject. Here: init REPORTS them, and
638
+ // The links themselves are `link-skills.harness.ts`'s subject. Here: init REPORTS them, and
639
639
  // a name it refused to take does not turn a working install red. The state is handed in so
640
640
  // this block does not depend on a package being installed next to the temp project.
641
641
  {
@@ -967,7 +967,7 @@ check(
967
967
 
968
968
  // ── STRUCTURE GETS THROUGH TO THE COMMAND ──────────────────────────────────────────────
969
969
  //
970
- // `structure.mjs` is checked separately and in full (`structure.harness.mjs`). Here — exactly
970
+ // `structure.mjs` is checked separately and in full (`structure.harness.ts`). Here — exactly
971
971
  // one fact that harness cannot know: that the module is WIRED IN. A correct module forgotten in
972
972
  // `run()` gives zero findings and looks like a clean corpus.
973
973
  {
@@ -1148,7 +1148,7 @@ check(
1148
1148
  // pnpm: node_modules/vigiles/dist/cli.js DOES NOT
1149
1149
  // The old wiring addressed the runtime from the project root and did not resolve under pnpm,
1150
1150
  // and `|| exit 2` on PreToolUse(Bash) turned that into a block on ANY command. The end-to-end
1151
- // half (both installs, real processes) lives in `test/e2e/install.mjs`; here — the verdicts.
1151
+ // half (both installs, real processes) lives in `test/e2e/install.ts`; here — the verdicts.
1152
1152
  {
1153
1153
  const calls = [];
1154
1154
  const fake = (code) => (bin, args, opts) => {
@@ -1258,7 +1258,7 @@ console.log(
1258
1258
 
1259
1259
  // ─────────────────────────────────────────────────────────────────────────────
1260
1260
  // What is left after init is typed in THE SAME TERMINAL. The two `/plugin` lines are gone: the
1261
- // hooks are written into `.claude/settings.json` by init itself (hooks-settings.harness.mjs).
1261
+ // hooks are written into `.claude/settings.json` by init itself (hooks-settings.harness.ts).
1262
1262
 
1263
1263
  {
1264
1264
  const steps = nextSteps("papers");