paperlint 3.1.3 → 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 (371) 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/src/CLAUDE.md +1 -1
  280. package/src/adapters/banal/index.test.ts +3 -6
  281. package/src/adapters/banal/index.ts +7 -4
  282. package/src/adapters/banal/invocation.test.ts +3 -1
  283. package/src/adapters/banal/invocation.ts +1 -1
  284. package/src/adapters/banal/output.test.ts +1 -1
  285. package/src/adapters/banal/run.test.ts +3 -3
  286. package/src/adapters/banal/xml.test.ts +1 -1
  287. package/src/adapters/curl/download.test.ts +4 -2
  288. package/src/adapters/node/files.io.ts +2 -2
  289. package/src/adapters/node/files.test.ts +3 -1
  290. package/src/adapters/node/process.io.ts +4 -3
  291. package/src/adapters/node/process.test.ts +1 -1
  292. package/src/adapters/node/workspace.test.ts +3 -1
  293. package/src/adapters/pdfjs/fill.test.ts +2 -2
  294. package/src/adapters/pdfjs/fill.ts +14 -7
  295. package/src/adapters/references/index.test.ts +31 -38
  296. package/src/adapters/references/index.ts +26 -58
  297. package/src/adapters/references/pool.ts +3 -5
  298. package/src/adapters/references/reach.io.ts +2 -1
  299. package/src/build-engine.harness.mjs +1 -1
  300. package/src/build-engine.test.ts +2 -2
  301. package/src/build-engine.ts +2 -2
  302. package/src/build.harness.mjs +2 -2
  303. package/src/build.test.ts +15 -14
  304. package/src/build.ts +28 -19
  305. package/src/cli.harness.mjs +6 -6
  306. package/src/cli.test.ts +13 -6
  307. package/src/cli.ts +39 -37
  308. package/src/doctor.harness.mjs +3 -3
  309. package/src/doctor.ts +3 -2
  310. package/src/domain/lookup-cache.ts +21 -11
  311. package/src/domain/result.test.ts +2 -2
  312. package/src/domain/text.test.ts +32 -0
  313. package/src/domain/text.ts +23 -0
  314. package/src/engine.harness.mjs +2 -2
  315. package/src/engine.ts +4 -3
  316. package/src/facts-file.test.ts +9 -7
  317. package/src/facts-file.ts +2 -1
  318. package/src/{hooks-settings.harness.mjs → hooks-settings.harness.ts} +39 -20
  319. package/src/hooks-settings.ts +20 -9
  320. package/src/init.test.ts +12 -9
  321. package/src/init.ts +22 -12
  322. package/src/{latex-log.harness.mjs → latex-log.harness.ts} +14 -14
  323. package/src/{latex-loop.harness.mjs → latex-loop.harness.ts} +64 -36
  324. package/src/latex-loop.ts +3 -3
  325. package/src/{link-skills.harness.mjs → link-skills.harness.ts} +19 -19
  326. package/src/link-skills.test.ts +3 -3
  327. package/src/link-skills.ts +5 -4
  328. package/src/lint-fix.test.ts +4 -6
  329. package/src/linted-set.test.ts +2 -2
  330. package/src/new-paper.harness.mjs +2 -2
  331. package/src/new-paper.test.ts +8 -10
  332. package/src/new-paper.ts +2 -2
  333. package/src/paper-files.test.ts +3 -5
  334. package/src/paper-settings-commands.test.ts +5 -4
  335. package/src/paper-settings.test.ts +5 -5
  336. package/src/paper-settings.ts +8 -9
  337. package/src/{pdf-facts.harness.mjs → pdf-facts.harness.ts} +9 -8
  338. package/src/pdf-facts.test.ts +10 -11
  339. package/src/pdf-facts.ts +2 -1
  340. package/src/{pdf-geometry.harness.mjs → pdf-geometry.harness.ts} +19 -12
  341. package/src/pdf-geometry.ts +3 -1
  342. package/src/presets.ts +3 -2
  343. package/src/references.test.ts +51 -43
  344. package/src/references.ts +14 -7
  345. package/src/{rules-config.harness.mjs → rules-config.harness.ts} +7 -13
  346. package/src/rules-config.ts +7 -4
  347. package/src/rules-off.test.ts +6 -4
  348. package/src/{structure.harness.mjs → structure.harness.ts} +26 -15
  349. package/src/structure.ts +1 -1
  350. package/src/{tex-requirements.harness.mjs → tex-requirements.harness.ts} +50 -22
  351. package/src/tex-requirements.ts +8 -5
  352. package/src/toolchain.harness.mjs +2 -2
  353. package/src/toolchain.test.ts +1 -1
  354. package/src/toolchain.ts +31 -17
  355. package/src/venue-rules.test.ts +62 -64
  356. package/src/venue-rules.ts +1 -1
  357. package/tsconfig.json +6 -3
  358. package/tsconfig.pkg.json +10 -0
  359. package/lib/check.d.mts +0 -17
  360. package/lib/paper-config.d.mts +0 -31
  361. package/scripts/layer-legacy-frozen.d.mts +0 -36
  362. package/scripts/rules-are-content-only.harness.mjs +0 -105
  363. package/scripts/rules-are-content-only.mjs +0 -148
  364. package/scripts/rules-are-content-only.test.mjs +0 -61
  365. package/scripts/test.mjs +0 -44
  366. package/scripts/test.test.mjs +0 -52
  367. /package/bin/{paperlint.test.mjs → paperlint.test.ts} +0 -0
  368. /package/lib/{skill-eval-fixture.mjs → skill-eval-fixture.ts} +0 -0
  369. /package/skills/grade-paper-writing/__snapshots__/{prose-lint.test.mjs.snap → prose-lint.test.ts.snap} +0 -0
  370. /package/skills/osf-artifact-upload/{osf-artifact-upload.harness.mjs → osf-artifact-upload.harness.ts} +0 -0
  371. /package/skills/tighten-paper/__snapshots__/{structure.test.mjs.snap → structure.test.ts.snap} +0 -0
@@ -6,30 +6,43 @@
6
6
  * A defect in `nextStep` fails in table 1, one in `summarize` in table 2, before table 3 runs.
7
7
  *
8
8
  * The decision is pure, so none of this needs TeX. The real-pdflatex half lives in
9
- * `test/e2e/build.mjs`.
9
+ * `test/e2e/build.ts`.
10
10
  */
11
- import { dirname, join } from "node:path";
12
- import { fileURLToPath } from "node:url";
13
- import { createChecker } from "../lib/check.mjs";
11
+ import { createChecker } from "../lib/check.ts";
12
+ import type {
13
+ BibInput,
14
+ Hashes,
15
+ Observation,
16
+ State,
17
+ Step,
18
+ } from "./latex-loop.ts";
14
19
 
15
- const HERE = dirname(fileURLToPath(import.meta.url));
16
- const { nextStep, summarize, changedFiles, MAX_PASSES } = await import(
17
- join(HERE, "latex-loop.ts")
18
- );
20
+ const { nextStep, summarize, changedFiles, MAX_PASSES } =
21
+ await import("./latex-loop.ts");
19
22
 
20
23
  const check = createChecker();
21
24
 
22
25
  // ── builders ────────────────────────────────────────────────────────────────────────────
23
- const H = (aux, toc = null, out = null, bbl = null) => ({ aux, toc, out, bbl });
24
- const NO_BIB = { kind: "none" };
25
- const bib = (citations, bibHash = "b1") => ({
26
+ type LatexPass = Extract<Observation, { step: "latex" }>;
27
+ type BibtexPass = Extract<Observation, { step: "bibtex" }>;
28
+ const H = (
29
+ aux: string | null,
30
+ toc: string | null = null,
31
+ out: string | null = null,
32
+ bbl: string | null = null,
33
+ ): Hashes => ({ aux, toc, out, bbl });
34
+ const NO_BIB: BibInput = { kind: "none" };
35
+ const bib = (
36
+ citations: readonly string[],
37
+ bibHash: string | null = "b1",
38
+ ): BibInput => ({
26
39
  kind: "needed",
27
40
  citations,
28
41
  databases: ["refs"],
29
42
  style: "plain",
30
43
  bibHash,
31
44
  });
32
- const latex = (o = {}) => ({
45
+ const latex = (o: Partial<LatexPass> = {}): LatexPass => ({
33
46
  step: "latex",
34
47
  final: false,
35
48
  exitCode: 0,
@@ -40,7 +53,7 @@ const latex = (o = {}) => ({
40
53
  errorLines: [],
41
54
  ...o,
42
55
  });
43
- const bibtex = (o = {}) => ({
56
+ const bibtex = (o: Partial<BibtexPass> = {}): BibtexPass => ({
44
57
  step: "bibtex",
45
58
  exitCode: 0,
46
59
  before: H("a1"),
@@ -49,11 +62,11 @@ const bibtex = (o = {}) => ({
49
62
  errorLines: [],
50
63
  ...o,
51
64
  });
52
- const kind = (s) =>
65
+ const kind = (s: Step) =>
53
66
  s.kind === "latex" ? `latex${s.final ? "-final" : ""}` : s.kind;
54
67
 
55
68
  // ── 1. nextStep over a State ─────────────────────────────────────────────────────────────
56
- const S = (o = {}) => ({
69
+ const S = (o: Partial<State> = {}): State => ({
57
70
  failed: null,
58
71
  bibOutdated: false,
59
72
  unsettled: [],
@@ -62,12 +75,12 @@ const S = (o = {}) => ({
62
75
  warnings: [],
63
76
  ...o,
64
77
  });
65
- const exitFail = {
78
+ const exitFail: NonNullable<State["failed"]> = {
66
79
  step: "bibtex",
67
80
  cause: { kind: "exit", code: 2 },
68
81
  lines: ["I couldn't open database file nope.bib"],
69
82
  };
70
- const STATES = [
83
+ const STATES: [string, State, string][] = [
71
84
  // Guards: the only moment paper-guards can speak — its error comes on the final pass, so testing
72
85
  // 'final' before 'exit code' would ship the undefined reference green.
73
86
  [
@@ -96,7 +109,7 @@ const STATES = [
96
109
  // Guards: the stop on a document that never settles — without it the loop runs forever; with a
97
110
  // silent cap it ships a PDF with stale cross-references.
98
111
  [
99
- `Q3 cap: ${MAX_PASSES} passes and still unsettled — fail`,
112
+ `Q3 cap: ${String(MAX_PASSES)} passes and still unsettled — fail`,
100
113
  S({ unsettled: ["paper.aux"], latexPasses: MAX_PASSES }),
101
114
  "fail",
102
115
  ],
@@ -115,20 +128,29 @@ for (const [label, state, expected] of STATES) {
115
128
  const q1 = nextStep(S({ failed: exitFail }));
116
129
  check(
117
130
  "Q1 passes the failure through: the step, the cause and the lines verbatim",
118
- q1.step === "bibtex" && q1.cause.code === 2 && q1.lines === exitFail.lines,
131
+ q1.kind === "fail" &&
132
+ q1.step === "bibtex" &&
133
+ q1.cause.kind === "exit" &&
134
+ q1.cause.code === 2 &&
135
+ q1.lines === exitFail.lines,
119
136
  );
120
137
  const q3cap = nextStep(
121
138
  S({ unsettled: ["paper.aux", "labels-changed"], latexPasses: MAX_PASSES }),
122
139
  );
123
140
  check(
124
141
  "the cap's failure is no-convergence and NAMES what stayed unsettled",
125
- q3cap.cause.kind === "no-convergence" &&
126
- q3cap.lines[0].includes("paper.aux, labels-changed"),
142
+ q3cap.kind === "fail" &&
143
+ q3cap.cause.kind === "no-convergence" &&
144
+ (q3cap.lines[0] ?? "").includes("paper.aux, labels-changed"),
127
145
  );
128
146
  check(
129
147
  "done carries the state's warnings",
130
- nextStep(S({ finalDone: true, warnings: ["undefined-citations"] }))
131
- .warnings[0] === "undefined-citations",
148
+ (() => {
149
+ const done = nextStep(
150
+ S({ finalDone: true, warnings: ["undefined-citations"] }),
151
+ );
152
+ return done.kind === "done" && done.warnings[0] === "undefined-citations";
153
+ })(),
132
154
  );
133
155
 
134
156
  // ── 2. summarize over histories ─────────────────────────────────────────────────────────
@@ -152,7 +174,10 @@ check(
152
174
  (() => {
153
175
  const f = sum([latex({ exitCode: 1, errorLines: ["l.4 \\foo"] })]).failed;
154
176
  return (
155
- f?.step === "latex" && f.cause.code === 1 && f.lines[0] === "l.4 \\foo"
177
+ f?.step === "latex" &&
178
+ f.cause.kind === "exit" &&
179
+ f.cause.code === 1 &&
180
+ f.lines[0] === "l.4 \\foo"
156
181
  );
157
182
  })(),
158
183
  );
@@ -162,22 +187,22 @@ check(
162
187
  );
163
188
  check(
164
189
  "summarize: bibOutdated when the aux names a bibliography bibtex has not run on",
165
- sum([latex({ bib: bib(["k"]) })]).bibOutdated === true,
190
+ sum([latex({ bib: bib(["k"]) })]).bibOutdated,
166
191
  );
167
192
  check(
168
193
  "summarize: bibOutdated is false once bibtex ran on exactly that input",
169
- sum([latex({ bib: bib(["k"]) }), bibtex()]).bibOutdated === false,
194
+ !sum([latex({ bib: bib(["k"]) }), bibtex()]).bibOutdated,
170
195
  );
171
196
  // Guards: the .bib hash as an input — a corrected author or year in refs.bib would keep the old
172
197
  // .bbl, and the PDF would print the entry as it was before the fix.
173
198
  check(
174
199
  "summarize: the .bib content changed since bibtex ran — bibOutdated",
175
200
  sum([latex({ bib: bib(["k"]) }), bibtex(), latex({ bib: bib(["k"], "b2") })])
176
- .bibOutdated === true,
201
+ .bibOutdated,
177
202
  );
178
203
  check(
179
204
  "summarize: no bibliography at all — never bibOutdated",
180
- sum([latex()]).bibOutdated === false,
205
+ !sum([latex()]).bibOutdated,
181
206
  );
182
207
  check(
183
208
  "summarize: unsettled names every tracked file the last pass changed",
@@ -232,10 +257,10 @@ check(
232
257
  );
233
258
 
234
259
  // ── 3. composed: nextStep(summarize(history)) ───────────────────────────────────────────
235
- const step = (history) => nextStep(summarize(history));
260
+ const step = (history: readonly Observation[]) => nextStep(summarize(history));
236
261
 
237
262
  // ── the histories the loop sees ───────────────────────────────────────────────────────────────────────────
238
- const TABLE = [
263
+ const TABLE: [string, Observation[], string][] = [
239
264
  ["nothing ran yet — the first step is a plain pass", [], "latex"],
240
265
  [
241
266
  "a pass that created the aux — rerun",
@@ -364,7 +389,8 @@ const latexFail = step([
364
389
  ]);
365
390
  check(
366
391
  "a latex fail names the step, the exit code and the log excerpt verbatim",
367
- latexFail.step === "latex" &&
392
+ latexFail.kind === "fail" &&
393
+ latexFail.step === "latex" &&
368
394
  latexFail.cause.kind === "exit" &&
369
395
  latexFail.cause.code === 1 &&
370
396
  latexFail.lines[1] === "l.4 \\foo",
@@ -380,14 +406,16 @@ check(
380
406
  "a bibtex fail names BIBTEX, not latex",
381
407
  bibFail.kind === "fail" &&
382
408
  bibFail.step === "bibtex" &&
409
+ bibFail.cause.kind === "exit" &&
383
410
  bibFail.cause.code === 2,
384
411
  );
385
412
 
386
413
  // ── the cap ─────────────────────────────────────────────────────────────────────────────
387
- const moving = (i) => latex({ before: H(`a${i}`), after: H(`a${i + 1}`) });
414
+ const moving = (i: number) =>
415
+ latex({ before: H(`a${String(i)}`), after: H(`a${String(i + 1)}`) });
388
416
  const underCap = Array.from({ length: MAX_PASSES - 1 }, (_, i) => moving(i));
389
417
  check(
390
- `the cap is ${MAX_PASSES} and it is not hit early: after ${MAX_PASSES - 1} moving passes, one more`,
418
+ `the cap is ${String(MAX_PASSES)} and it is not hit early: after ${String(MAX_PASSES - 1)} moving passes, one more`,
391
419
  MAX_PASSES === 5 && kind(step(underCap)) === "latex",
392
420
  );
393
421
  const atCap = Array.from({ length: MAX_PASSES }, (_, i) => moving(i));
@@ -398,7 +426,7 @@ check(
398
426
  );
399
427
  check(
400
428
  "the non-convergence failure NAMES the file that kept changing",
401
- capped.kind === "fail" && capped.lines[0].includes("paper.aux"),
429
+ capped.kind === "fail" && (capped.lines[0] ?? "").includes("paper.aux"),
402
430
  );
403
431
  const cappedMarker = step(
404
432
  Array.from({ length: MAX_PASSES }, () =>
@@ -408,7 +436,7 @@ const cappedMarker = step(
408
436
  check(
409
437
  "a log that keeps asking for a rerun hits the same cap, and names the marker",
410
438
  cappedMarker.kind === "fail" &&
411
- cappedMarker.lines[0].includes("labels-changed"),
439
+ (cappedMarker.lines[0] ?? "").includes("labels-changed"),
412
440
  );
413
441
  const convergedAtCap = [...atCap.slice(0, -1), latex()];
414
442
  check(
@@ -446,5 +474,5 @@ check(
446
474
  );
447
475
 
448
476
  console.log(
449
- `✓ ${String(check.count)} assertions passed — latex-loop: nextStep over State, summarize over histories, and the two composed, including the ${MAX_PASSES}-pass cap`,
477
+ `✓ ${String(check.count)} assertions passed — latex-loop: nextStep over State, summarize over histories, and the two composed, including the ${String(MAX_PASSES)}-pass cap`,
450
478
  );
package/src/latex-loop.ts CHANGED
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * PURE. No disk, no process, no clock. The shell in `build.ts` runs a step, records what it saw
5
5
  * as an `Observation`, appends it to the history and asks `nextStep(summarize(history))` again.
6
- * Both halves are table tests (`latex-loop.harness.mjs`), not scenarios that need TeX.
6
+ * Both halves are table tests (`latex-loop.harness.ts`), not scenarios that need TeX.
7
7
  *
8
8
  * ── THE LOOP: `summarize` reduces the history to a `State`, and `nextStep` asks four questions ──
9
9
  * 1. did the last program fail? → fail, with its error lines
@@ -118,7 +118,7 @@ export type Step =
118
118
  };
119
119
 
120
120
  /** Non-final pdflatex passes allowed before the build is declared non-converging. */
121
- export const MAX_PASSES = 5;
121
+ export const MAX_PASSES: number = 5;
122
122
 
123
123
  /** The history, reduced to the four questions `nextStep` asks. */
124
124
  export type State = {
@@ -152,7 +152,7 @@ const noConvergence = (unsettled: readonly string[]): Step => ({
152
152
  step: "latex",
153
153
  cause: { kind: "no-convergence", unsettled },
154
154
  lines: [
155
- `the build does not converge: after ${MAX_PASSES} pdflatex passes, the last one still changed or asked to rerun: ${unsettled.join(", ")}.`,
155
+ `the build does not converge: after ${String(MAX_PASSES)} pdflatex passes, the last one still changed or asked to rerun: ${unsettled.join(", ")}.`,
156
156
  `A document whose aux keeps changing on every pass never settles; stopping here instead of`,
157
157
  `shipping a PDF with stale cross-references.`,
158
158
  ],
@@ -12,7 +12,7 @@
12
12
  * The fixture package keeps its skills where the real one does, under `SHIPPED_SKILLS_DIR` from
13
13
  * consumer.mjs — the same constant the linker and the install e2e read.
14
14
  *
15
- * Run: node src/link-skills.harness.mjs
15
+ * Run: node src/link-skills.harness.ts
16
16
  */
17
17
  import {
18
18
  existsSync,
@@ -26,17 +26,14 @@ import {
26
26
  writeFileSync,
27
27
  } from "node:fs";
28
28
  import { tmpdir } from "node:os";
29
- import { dirname, isAbsolute, join } from "node:path";
30
- import { fileURLToPath } from "node:url";
31
- import { createChecker } from "../lib/check.mjs";
29
+ import { isAbsolute, join } from "node:path";
30
+ import { createChecker } from "../lib/check.ts";
31
+ import type { LinkReport } from "./link-skills.ts";
32
32
 
33
- const HERE = dirname(fileURLToPath(import.meta.url));
34
- const { linkSkills, locatePackage, shippedSkills } = await import(
35
- join(HERE, "link-skills.ts")
36
- );
37
- const { SHIPPED_SKILLS_DIR: SHIPS, PACKAGE_NAME: PKG } = await import(
38
- join(HERE, "..", "skills", "paper-pipeline", "scripts", "consumer.mjs")
39
- );
33
+ const { linkSkills, locatePackage, shippedSkills } =
34
+ await import("./link-skills.ts");
35
+ const { SHIPPED_SKILLS_DIR: SHIPS, PACKAGE_NAME: PKG } =
36
+ await import("../skills/paper-pipeline/scripts/consumer.mjs");
40
37
 
41
38
  const check = createChecker();
42
39
 
@@ -46,7 +43,7 @@ const work = realpathSync(
46
43
  );
47
44
 
48
45
  /** A package as a manager would unpack it: manifest, skills, one non-skill. */
49
- function writePackage(dir) {
46
+ function writePackage(dir: string): void {
50
47
  mkdirSync(dir, { recursive: true });
51
48
  writeFileSync(
52
49
  join(dir, "package.json"),
@@ -63,7 +60,7 @@ function writePackage(dir) {
63
60
  }
64
61
 
65
62
  /** A consumer project with the package installed the npm way or the pnpm way. */
66
- function consumer(name, manager = "npm") {
63
+ function consumer(name: string, manager = "npm"): string {
67
64
  const dir = join(work, name);
68
65
  mkdirSync(join(dir, "node_modules"), { recursive: true });
69
66
  writeFileSync(
@@ -89,8 +86,11 @@ function consumer(name, manager = "npm") {
89
86
  return dir;
90
87
  }
91
88
 
92
- const home = (dir) => join(dir, ".claude", "skills");
93
- const status = (report, name) => report.links.find((l) => l.name === name);
89
+ const home = (dir: string) => join(dir, ".claude", "skills");
90
+ /** The report's links; a refusal has none. */
91
+ const linksOf = (report: LinkReport) => (report.ok ? report.links : []);
92
+ const status = (report: LinkReport, name: string) =>
93
+ linksOf(report).find((l) => l.name === name);
94
94
 
95
95
  try {
96
96
  // ── I. A FRESH npm CONSUMER: EVERY DECLARED SKILL IS LINKED, AND ONLY SKILLS ───────────
@@ -99,11 +99,11 @@ try {
99
99
  const r = linkSkills(dir);
100
100
  check(
101
101
  "🔴 it links every skill the package ships",
102
- r.ok === true && SKILLS.every((s) => status(r, s)?.status === "created"),
102
+ r.ok && SKILLS.every((s) => status(r, s)?.status === "created"),
103
103
  );
104
104
  check(
105
105
  "and nothing that is not a skill — a directory without SKILL.md is not one",
106
- r.links.length === SKILLS.length &&
106
+ linksOf(r).length === SKILLS.length &&
107
107
  !existsSync(join(home(dir), "shared")),
108
108
  );
109
109
  check(
@@ -195,7 +195,7 @@ try {
195
195
  const r = linkSkills(dir);
196
196
  check(
197
197
  "a project that does not have the package gets an error, not links into somebody's cache",
198
- r.ok === false && /does not resolve from/.test(r.error),
198
+ !r.ok && /does not resolve from/.test(r.error),
199
199
  );
200
200
  check("and nothing is created", !existsSync(join(dir, ".claude")));
201
201
  }
@@ -229,5 +229,5 @@ try {
229
229
  }
230
230
 
231
231
  console.log(
232
- `✓ ${check.count} assertions passed — link-skills: the skills are where Claude Code looks`,
232
+ `✓ ${String(check.count)} assertions passed — link-skills: the skills are where Claude Code looks`,
233
233
  );
@@ -14,7 +14,7 @@ import {
14
14
  } from "node:fs";
15
15
  import { join } from "node:path";
16
16
  import { test } from "vitest";
17
- import { useTempDir, writeTree } from "../test/support.mjs";
17
+ import { useTempDir, writeTree } from "../test/support.ts";
18
18
  import { linkSkills, locatePackage, type LinkFs } from "./link-skills.ts";
19
19
 
20
20
  const root = useTempDir("paperlint-link-skills-");
@@ -62,7 +62,7 @@ test("a package without a skills directory, or one holding a dangling link, is n
62
62
  symlinkSync(join(root, "gone"), join(dangling, "skills", "b"));
63
63
  const r = linkSkills(join(root, "p2"), { locate: at(dangling) });
64
64
  assert.equal(r.ok, false);
65
- assert.match(!r.ok ? r.error : "", /^1 skill link\(s\) in .* lead nowhere:/);
65
+ assert.match(r.error, /^1 skill link\(s\) in .* lead nowhere:/);
66
66
  });
67
67
 
68
68
  test("a skills home that cannot be made is named", () => {
@@ -71,7 +71,7 @@ test("a skills home that cannot be made is named", () => {
71
71
  });
72
72
  const r = linkSkills(project, { locate: at(pkg("home", ["a"])) });
73
73
  assert.equal(r.ok, false);
74
- assert.match(!r.ok ? r.error : "", /^cannot create .*\.claude\/skills: /);
74
+ assert.match(r.error, /^cannot create .*\.claude\/skills: /);
75
75
  });
76
76
 
77
77
  test("entries that are someone else's: a plain file, one that cannot be resolved, a link that cannot be made", () => {
@@ -44,6 +44,7 @@ import {
44
44
  PACKAGE_NAME,
45
45
  SHIPPED_SKILLS_DIR,
46
46
  } from "../skills/paper-pipeline/scripts/consumer.mjs";
47
+ import { messageOf, codeOf } from "./domain/text.ts";
47
48
 
48
49
  export { PACKAGE_NAME };
49
50
  /** Where Claude Code looks for project skills, relative to the project root. */
@@ -74,7 +75,7 @@ export function locatePackage(
74
75
  manifest = resolveManifest(project);
75
76
  } catch (e) {
76
77
  return {
77
- error: `${(e as NodeJS.ErrnoException).code ?? "error"}: ${(e as Error).message.split("\n")[0]}`,
78
+ error: `${codeOf(e) ?? "error"}: ${String(messageOf(e).split("\n")[0])}`,
78
79
  };
79
80
  }
80
81
  const dir = realpathSync(dirname(manifest));
@@ -108,7 +109,7 @@ export function shippedSkills(
108
109
  try {
109
110
  return { skillsDir, names: installedSkills(skillsDir) };
110
111
  } catch (e) {
111
- return { error: (e as Error).message };
112
+ return { error: messageOf(e) };
112
113
  }
113
114
  }
114
115
 
@@ -207,7 +208,7 @@ function linkOne(
207
208
  return {
208
209
  name,
209
210
  status: "foreign",
210
- reason: `could not create the link: ${(e as Error).message}`,
211
+ reason: `could not create the link: ${messageOf(e)}`,
211
212
  };
212
213
  }
213
214
  }
@@ -218,7 +219,7 @@ function makeHome(home: string): string | null {
218
219
  mkdirSync(home, { recursive: true });
219
220
  return null;
220
221
  } catch (e) {
221
- return `cannot create ${home}: ${(e as Error).message}`;
222
+ return `cannot create ${home}: ${messageOf(e)}`;
222
223
  }
223
224
  }
224
225
 
@@ -13,6 +13,7 @@ import { tmpdir } from "node:os";
13
13
  import { join } from "node:path";
14
14
  import { afterEach, describe, expect, it } from "vitest";
15
15
  import { run } from "./cli.ts";
16
+ import { lintReport } from "../test/lint-report.ts";
16
17
 
17
18
  const dirs: string[] = [];
18
19
  afterEach(() => {
@@ -50,8 +51,8 @@ async function lint(root: string, args: string[] = []) {
50
51
  return { code, out: out.join("\n"), err: err.join("\n") };
51
52
  }
52
53
  const ruleIds = (stdout: string): string[] =>
53
- (JSON.parse(stdout) as { messages: { ruleId: string }[] }[]).flatMap((r) =>
54
- r.messages.map((m) => m.ruleId),
54
+ lintReport(stdout).flatMap((r) =>
55
+ r.messages.map((m) => m.ruleId ?? "(fatal)"),
55
56
  );
56
57
 
57
58
  describe("paperlint lint --fix", () => {
@@ -85,10 +86,7 @@ describe("sibling cards: `read:` is required, from the package's own schema", ()
85
86
  "papers/a/siblings/jones2024.md": "---\nread: abstract\n---\n# Jones\n",
86
87
  },
87
88
  );
88
- const results = JSON.parse((await lint(root)).out) as {
89
- filePath: string;
90
- messages: { ruleId: string; severity: number }[];
91
- }[];
89
+ const results = lintReport((await lint(root)).out);
92
90
  const of = (name: string) =>
93
91
  results.find((r) => r.filePath.endsWith(name))?.messages ?? null;
94
92
  expect(of("smith2025.md")?.map((m) => [m.ruleId, m.severity])).toEqual([
@@ -19,6 +19,7 @@ import { tmpdir } from "node:os";
19
19
  import { join, relative } from "node:path";
20
20
  import { afterEach, describe, expect, it } from "vitest";
21
21
  import { run } from "./cli.ts";
22
+ import { lintReport } from "../test/lint-report.ts";
22
23
 
23
24
  const dirs: string[] = [];
24
25
  afterEach(() => {
@@ -91,9 +92,8 @@ async function lintedSet(root: string, args: string[]): Promise<string[]> {
91
92
  log: (s: string) => out.push(s),
92
93
  err: () => {},
93
94
  });
94
- type Result = { filePath: string; messages: { ruleId: string | null }[] };
95
95
  return (
96
- (JSON.parse(out.join("\n")) as Result[])
96
+ lintReport(out.join("\n"))
97
97
  // A structure finding reports a MISSING file in the same shape; it was not linted.
98
98
  .filter(
99
99
  (r) => !r.messages.some((m) => m.ruleId === "structure/required-file"),
@@ -30,8 +30,8 @@ import { tmpdir } from "node:os";
30
30
  import { dirname, join } from "node:path";
31
31
  import { fileURLToPath } from "node:url";
32
32
  import { load } from "js-yaml";
33
- import { frontmatterBlock } from "../lib/markdown.mjs";
34
- import { createChecker } from "../lib/check.mjs";
33
+ import { frontmatterBlock } from "../lib/markdown.ts";
34
+ import { createChecker } from "../lib/check.ts";
35
35
 
36
36
  const HERE = dirname(fileURLToPath(import.meta.url));
37
37
  const ROOT = dirname(HERE);
@@ -20,6 +20,8 @@ import { parsePaperSettings } from "./paper-settings.ts";
20
20
  import { chooseVenue, run } from "./cli.ts";
21
21
  import { shippedPresets } from "./presets.ts";
22
22
  import { packageVenuesDir } from "../skills/paper-pipeline/scripts/consumer.mjs";
23
+ import { lintReport } from "../test/lint-report.ts";
24
+ import { z } from "zod";
23
25
 
24
26
  const dirs: string[] = [];
25
27
  afterEach(() => {
@@ -31,10 +33,9 @@ const tmp = () => {
31
33
  return d;
32
34
  };
33
35
  const settingsOf = (dir: string) =>
34
- JSON.parse(readFileSync(join(dir, "paperlint.json"), "utf8")) as Record<
35
- string,
36
- unknown
37
- >;
36
+ z
37
+ .record(z.string(), z.unknown())
38
+ .parse(JSON.parse(readFileSync(join(dir, "paperlint.json"), "utf8")));
38
39
 
39
40
  describe("paperlint new — paperlint.json", () => {
40
41
  it("is always written, from the package template: no venue chosen yet, and valid", () => {
@@ -107,11 +108,7 @@ describe("paperlint lint — a paper with no venue preset chosen", () => {
107
108
  log: (s: string) => out.push(s),
108
109
  err: () => {},
109
110
  });
110
- const messages = (
111
- JSON.parse(out.join("\n")) as {
112
- messages: { ruleId: string; severity: number; message: string }[];
113
- }[]
114
- ).flatMap((r) => r.messages);
111
+ const messages = lintReport(out.join("\n")).flatMap((r) => r.messages);
115
112
  return { code, messages };
116
113
  }
117
114
 
@@ -266,7 +263,8 @@ describe("chooseVenue — on a terminal", () => {
266
263
  paperDir: "/p/papers/demo",
267
264
  cwd: "/p",
268
265
  interactive: true,
269
- ask: async (q: string) => (asked.push(q), answers.shift() ?? ""),
266
+ ask: (q: string) =>
267
+ Promise.resolve((asked.push(q), answers.shift() ?? "")),
270
268
  },
271
269
  };
272
270
  };
package/src/new-paper.ts CHANGED
@@ -25,7 +25,7 @@ import {
25
25
  /* eslint-enable boundaries/dependencies */
26
26
  import { join } from "node:path";
27
27
  import { fileURLToPath } from "node:url";
28
- import { CONFIG_FILE } from "../lib/paper-config.mjs";
28
+ import { CONFIG_FILE } from "#lib/paper-config";
29
29
 
30
30
  export type PaperFormat = "tex" | "md";
31
31
  export const FORMATS: readonly PaperFormat[] = ["tex", "md"];
@@ -50,7 +50,7 @@ export const SOURCE_FILE: Readonly<Record<PaperFormat, string>> = {
50
50
  };
51
51
 
52
52
  export const isFormat = (v: unknown): v is PaperFormat =>
53
- FORMATS.includes(v as PaperFormat);
53
+ FORMATS.some((f) => f === v);
54
54
 
55
55
  /**
56
56
  * Why a name is refused, or null. `[a-z0-9._-]+` is the charset; a LEADING dot is refused on top
@@ -27,6 +27,7 @@ import {
27
27
  ruleOwners,
28
28
  scopeToOwned,
29
29
  } from "./paper-files.ts";
30
+ import { lintReport } from "../test/lint-report.ts";
30
31
 
31
32
  const dirs: string[] = [];
32
33
  afterEach(() => {
@@ -67,7 +68,7 @@ async function lint(
67
68
  }
68
69
 
69
70
  const linted = (stdout: string): string[] =>
70
- (JSON.parse(stdout) as { filePath: string }[]).map((r) => r.filePath);
71
+ lintReport(stdout).map((r) => r.filePath);
71
72
 
72
73
  /** Vendored code under a paper: a syntax error, an unknown rule in a directive, plain CommonJS. */
73
74
  const VENDORED = {
@@ -208,10 +209,7 @@ describe("🔴 a generated block never reaches a file paperlint does not lint (#
208
209
  async (_, settings) => {
209
210
  const root = project({ ...SIBLINGS, ...settings });
210
211
  const r = await lint(root);
211
- const results = JSON.parse(r.out) as {
212
- filePath: string;
213
- messages: { ruleId: string | null; message: string }[];
214
- }[];
212
+ const results = lintReport(r.out);
215
213
  // Guards: the class — no file reaches ESLint's default JavaScript parser.
216
214
  expect(
217
215
  results.flatMap((x) => x.messages).filter((m) => m.ruleId === null),
@@ -13,6 +13,7 @@ import { tmpdir } from "node:os";
13
13
  import { join } from "node:path";
14
14
  import { afterEach, describe, expect, it } from "vitest";
15
15
  import { run, toolchainTex } from "./cli.ts";
16
+ import { lintReport } from "../test/lint-report.ts";
16
17
 
17
18
  const dirs: string[] = [];
18
19
  afterEach(() => {
@@ -55,9 +56,9 @@ async function lint(
55
56
  }
56
57
 
57
58
  const rulesIn = (stdout: string, paper: string): string[] =>
58
- (JSON.parse(stdout) as { filePath: string; messages: { ruleId: string }[] }[])
59
+ lintReport(stdout)
59
60
  .filter((r) => r.filePath.endsWith(join(paper, "paper.tex")))
60
- .flatMap((r) => r.messages.map((m) => m.ruleId));
61
+ .flatMap((r) => r.messages.map((m) => m.ruleId ?? "(fatal)"));
61
62
 
62
63
  describe("🔴 an optional rule that reaches no paper — judged against the PROJECT, not the run (#103)", () => {
63
64
  const THIRD = {
@@ -151,7 +152,7 @@ describe("papersDir — optional, `papers` by default", () => {
151
152
  const r = await lint(project());
152
153
  expect(r.code).toBe(0);
153
154
  expect(
154
- (JSON.parse(r.out) as { filePath: string }[]).map((f) =>
155
+ lintReport(r.out).map((f) =>
155
156
  f.filePath.slice(f.filePath.indexOf("papers/")),
156
157
  ),
157
158
  ).toEqual(
@@ -178,7 +179,7 @@ describe("papersDir — optional, `papers` by default", () => {
178
179
  "docs/drafts/c/PIPELINE-STATUS.md": "---\nstages: []\n---\n",
179
180
  });
180
181
  const r = await lint(root);
181
- const linted = (JSON.parse(r.out) as { filePath: string }[]).map((f) =>
182
+ const linted = lintReport(r.out).map((f) =>
182
183
  f.filePath.slice(root.length + 1),
183
184
  );
184
185
  expect(linted).toContain("docs/drafts/c/paper.tex");
@@ -5,7 +5,7 @@
5
5
  import { describe, expect, it } from "vitest";
6
6
  import { memoryFiles } from "./adapters/memory/index.ts";
7
7
  import type { AbsolutePath } from "./domain/paths.ts";
8
- import { findProjectRoot } from "../lib/paper-config.mjs";
8
+ import { findProjectRoot } from "../lib/paper-config.ts";
9
9
  import {
10
10
  paperRules,
11
11
  parsePaperSettings,
@@ -128,12 +128,12 @@ describe("readPaperSettings — a root paperlint.json that cannot be used", () =
128
128
  readPaperSettings(withFiles({ [`${ROOT}/paperlint.json`]: root }), PAPER);
129
129
  const notJson = broken("{ nope");
130
130
  expect(!notJson.ok && notJson.error.kind).toBe("broken");
131
+ const why: unknown = expect.stringMatching(
132
+ /^the root paperlint\.json: .*kind/,
133
+ );
131
134
  expect(broken('{"kind":3}')).toEqual({
132
135
  ok: false,
133
- error: {
134
- kind: "broken",
135
- why: expect.stringMatching(/^the root paperlint\.json: .*kind/),
136
- },
136
+ error: { kind: "broken", why },
137
137
  });
138
138
  });
139
139
  });