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
@@ -38,23 +38,44 @@
38
38
  * author wrote correctly.
39
39
  */
40
40
  import Ajv from "ajv";
41
+ import type { ErrorObject } from "ajv";
41
42
  import { CORE_SCHEMA, load } from "js-yaml";
43
+ import assert from "node:assert/strict";
42
44
  import { readFileSync } from "node:fs";
45
+ import { z } from "zod";
46
+ import type { RuleContext } from "./rule-context.ts";
47
+
48
+ /** A compiled schema, as far as `errorsOf` uses one. Ajv's `ValidateFunction` is one. */
49
+ export interface Validate {
50
+ (data: unknown): unknown;
51
+ readonly errors?: readonly ErrorObject[] | null;
52
+ }
53
+
54
+ /**
55
+ * What a parser failure says about itself. js-yaml's `YAMLException` carries a `reason`; anything
56
+ * else thrown may carry a `message`, or be a bare value that says nothing.
57
+ */
58
+ const Thrown = z
59
+ .object({
60
+ reason: z.string().optional().catch(undefined),
61
+ message: z.string().optional().catch(undefined),
62
+ })
63
+ .catch({});
43
64
 
44
65
  /** One ajv error as a line a person can act on. `if` errors only repeat their `then`. */
45
- function describe(e) {
66
+ function describe(e: ErrorObject): string {
46
67
  const where = e.dataPath
47
68
  ? `\`${e.dataPath.replace(/^\./, "")}\``
48
69
  : "the frontmatter";
49
- if (e.keyword === "enum")
70
+ if (e.keyword === "enum" && "allowedValues" in e.params)
50
71
  return `${where} must be one of: ${e.params.allowedValues.join(", ")}`;
51
- if (e.keyword === "additionalProperties")
72
+ if (e.keyword === "additionalProperties" && "additionalProperty" in e.params)
52
73
  return `${where} has an unknown field \`${e.params.additionalProperty}\``;
53
- return `${where} ${e.message}`;
74
+ return `${where} ${String(e.message)}`;
54
75
  }
55
76
 
56
77
  /** The schema's verdict on `data` as messages. Ajv types `errors` as possibly null; that is tolerated. */
57
- export const errorsOf = (validate, data) =>
78
+ export const errorsOf = (validate: Validate, data: unknown): string[] =>
58
79
  validate(data)
59
80
  ? []
60
81
  : (validate.errors ?? []).filter((e) => e.keyword !== "if").map(describe);
@@ -63,10 +84,10 @@ export const errorsOf = (validate, data) =>
63
84
  * A rule that validates a markdown file's YAML frontmatter against the JSON Schema in `schemaUrl`
64
85
  * (a file shipped beside the rule). A file with no frontmatter is validated as `{}`.
65
86
  */
66
- export function frontmatterRule(schemaUrl, description) {
67
- const validate = new Ajv({ allErrors: true }).compile(
68
- JSON.parse(readFileSync(schemaUrl, "utf8")),
69
- );
87
+ export function frontmatterRule(schemaUrl: URL | string, description: string) {
88
+ const schema: unknown = JSON.parse(readFileSync(schemaUrl, "utf8"));
89
+ assert.ok(typeof schema === "object" && schema !== null, "not a schema");
90
+ const validate = new Ajv({ allErrors: true }).compile(schema);
70
91
  return {
71
92
  meta: {
72
93
  type: "problem",
@@ -78,24 +99,25 @@ export function frontmatterRule(schemaUrl, description) {
78
99
  invalid: "{{problem}}",
79
100
  },
80
101
  },
81
- create(context) {
82
- let frontmatter = null;
83
- let data = {};
102
+ create(context: RuleContext) {
103
+ let frontmatter: object | null = null;
104
+ let data: unknown = {};
84
105
  return {
85
- yaml(node) {
106
+ yaml(node: { readonly value?: string }) {
86
107
  frontmatter = node;
87
108
  try {
88
109
  data = load(node.value ?? "", { schema: CORE_SCHEMA }) ?? {};
89
110
  } catch (e) {
90
111
  data = null;
112
+ const { reason, message } = Thrown.parse(e);
91
113
  context.report({
92
114
  node,
93
115
  messageId: "malformed",
94
- data: { why: e.reason ?? e.message ?? "unparseable" },
116
+ data: { why: reason ?? message ?? "unparseable" },
95
117
  });
96
118
  }
97
119
  },
98
- "root:exit"(root) {
120
+ "root:exit"(root: object) {
99
121
  if (data === null) return;
100
122
  for (const problem of errorsOf(validate, data))
101
123
  context.report({
@@ -0,0 +1,62 @@
1
+ /**
2
+ * The slice of ESLint's rule API the rules in this directory use.
3
+ *
4
+ * These rules run on three languages (JavaScript, markdown and the `.tex` language next door),
5
+ * and ESLint's own `Rule.RuleContext` is the JavaScript one. So the rules are typed against what
6
+ * they actually touch, the way `src/venue-rules.ts` and `src/reference-rules.ts` already are.
7
+ */
8
+
9
+ /** A line and a 1-based column, as ESLint reports them. */
10
+ export interface Loc {
11
+ readonly line: number;
12
+ readonly column: number;
13
+ }
14
+
15
+ import type { Root } from "mdast";
16
+
17
+ /** The source code a rule reads. `raw` exists on the `.tex` language's source code only. */
18
+ export interface RuleSourceCode {
19
+ readonly text: string;
20
+ readonly raw?: string;
21
+ /** mdast: the markdown language's tree, or the `.tex` language's projection of one. */
22
+ readonly ast?: Root;
23
+ getLocFromIndex(index: number): Loc;
24
+ getText(node?: object): string;
25
+ }
26
+
27
+ /** What a fix produces: replace `range` of the file with `text`. */
28
+ export interface RuleFix {
29
+ readonly range: readonly [number, number];
30
+ readonly text: string;
31
+ }
32
+
33
+ export interface RuleFixer {
34
+ replaceTextRange(range: readonly [number, number], text: string): RuleFix;
35
+ insertTextBeforeRange(
36
+ range: readonly [number, number],
37
+ text: string,
38
+ ): RuleFix;
39
+ }
40
+
41
+ export interface RuleSuggestion {
42
+ readonly messageId: string;
43
+ readonly data?: Readonly<Record<string, string | number>>;
44
+ fix(fixer: RuleFixer): RuleFix;
45
+ }
46
+
47
+ export interface RuleReport {
48
+ readonly node?: object;
49
+ readonly loc?: { readonly start: Loc; readonly end: Loc };
50
+ readonly messageId: string;
51
+ readonly data?: Readonly<Record<string, string | number>>;
52
+ readonly fix?: (fixer: RuleFixer) => RuleFix;
53
+ readonly suggest?: readonly RuleSuggestion[];
54
+ }
55
+
56
+ export interface RuleContext {
57
+ readonly filename: string;
58
+ readonly options: readonly unknown[];
59
+ readonly settings: Readonly<Record<string, unknown>>;
60
+ readonly sourceCode: RuleSourceCode;
61
+ report(descriptor: RuleReport): void;
62
+ }
@@ -7,7 +7,7 @@
7
7
  * The package writes the cards, so it ships the schema: `sibling-frontmatter.schema.json`. Same
8
8
  * mechanism as `review/frontmatter`.
9
9
  */
10
- import { frontmatterRule } from "./review-frontmatter.mjs";
10
+ import { frontmatterRule } from "./review-frontmatter.ts";
11
11
 
12
12
  export default {
13
13
  rules: {
@@ -20,7 +20,7 @@
20
20
  import assert from "node:assert/strict";
21
21
  import { ESLint } from "eslint";
22
22
  import { recordCheck } from "vigiles";
23
- import localRules from "./temp-root-realpath.mjs";
23
+ import localRules from "./temp-root-realpath.ts";
24
24
 
25
25
  const eslint = new ESLint({
26
26
  overrideConfigFile: true,
@@ -45,8 +45,18 @@
45
45
  * assertion was about their sum.
46
46
  */
47
47
 
48
+ import type { CallExpression, Node } from "estree";
49
+ import type { RuleContext } from "./rule-context.ts";
50
+
51
+ /**
52
+ * Whether `value` is an ESTree node: an object whose `type` is a string. The walk below reaches
53
+ * every property of a node, and most of them (`regex`, `value`, arrays of flags) are not nodes.
54
+ */
55
+ const isNode = (value: object): value is Node =>
56
+ "type" in value && typeof value.type === "string";
57
+
48
58
  /** The called function's name: `f()` and `mod.f()` count as the same name for our purposes. */
49
- const calleeName = (node) => {
59
+ const calleeName = (node: Node | null | undefined): string => {
50
60
  if (node?.type !== "CallExpression") return "";
51
61
  const c = node.callee;
52
62
  if (c.type === "Identifier") return c.name;
@@ -65,10 +75,10 @@ const calleeName = (node) => {
65
75
  * text-based guard would be forced to exclude itself — exactly the class of check string-based
66
76
  * checks are banned for in this repository.
67
77
  */
68
- const mentionsTmpdir = (node) => {
78
+ const mentionsTmpdir = (node: unknown): boolean => {
69
79
  if (node === null || typeof node !== "object") return false;
70
80
  if (Array.isArray(node)) return node.some(mentionsTmpdir);
71
- if (typeof node.type !== "string") return false;
81
+ if (!isNode(node)) return false;
72
82
  if (calleeName(node) === "tmpdir") return true;
73
83
  for (const [key, value] of Object.entries(node)) {
74
84
  if (key === "parent" || key === "loc" || key === "range") continue;
@@ -79,7 +89,9 @@ const mentionsTmpdir = (node) => {
79
89
  };
80
90
 
81
91
  /** `realpathSync(x)` and `realpathSync.native(x)` — both resolve the path, both count. */
82
- const isRealpathCall = (node) => {
92
+ const isRealpathCall = (
93
+ node: Node | null | undefined,
94
+ ): node is CallExpression => {
83
95
  if (node?.type !== "CallExpression") return false;
84
96
  const name = calleeName(node);
85
97
  if (name === "realpathSync") return true;
@@ -91,6 +103,7 @@ const isRealpathCall = (node) => {
91
103
  return (
92
104
  obj?.type === "MemberExpression" &&
93
105
  !obj.computed &&
106
+ "name" in obj.property &&
94
107
  obj.property.name === "realpathSync"
95
108
  );
96
109
  };
@@ -113,9 +126,9 @@ export default {
113
126
  "directory under two names, and every path comparison built on it silently compares the two.",
114
127
  },
115
128
  },
116
- create(context) {
129
+ create(context: RuleContext) {
117
130
  return {
118
- CallExpression(node) {
131
+ CallExpression(node: CallExpression & { readonly parent?: Node }) {
119
132
  if (calleeName(node) !== "mkdtempSync") return;
120
133
  if (!mentionsTmpdir(node.arguments)) return;
121
134
  const parent = node.parent;
@@ -70,7 +70,7 @@ import { fileURLToPath } from "node:url";
70
70
 
71
71
  const HERE = dirname(fileURLToPath(import.meta.url));
72
72
  const ROOT = join(HERE, ".."); // eslint-rules → repository root
73
- const RULES = join(HERE, "tex-build.mjs");
73
+ const RULES = join(HERE, "tex-build.ts");
74
74
  assert.ok(existsSync(RULES), `no entry point for the test: ${RULES}`);
75
75
 
76
76
  const TMP = realpathSync(mkdtempSync(join(tmpdir(), "tex-build-")));
@@ -78,7 +78,7 @@ const TMP = realpathSync(mkdtempSync(join(tmpdir(), "tex-build-")));
78
78
  // `rmSync` at the bottom never runs in exactly the runs that are red.
79
79
  process.on("exit", () => rmSync(TMP, { recursive: true, force: true }));
80
80
 
81
- const { texLanguage } = await import(join(HERE, "latex-language.mjs"));
81
+ const { texLanguage } = await import(join(HERE, "latex-language.ts"));
82
82
  const texBuild = (await import(RULES)).default;
83
83
 
84
84
  // A new rule with no declared severity must not slip through unnoticed.
@@ -50,6 +50,7 @@
50
50
  * on `readdirSync` order and on how many drafts happen to lie next to the paper.
51
51
  */
52
52
  import { readFileSync } from "node:fs";
53
+ import type { RuleContext } from "./rule-context.ts";
53
54
 
54
55
  /**
55
56
  * A copy rather than an import, for the same reason the prose lexicons are copies: a rule may
@@ -152,7 +153,7 @@ const futurePromise = {
152
153
  "through paper.tex",
153
154
  },
154
155
  },
155
- create(context) {
156
+ create(context: RuleContext) {
156
157
  return {
157
158
  root() {
158
159
  // 🔴 ONE GUARD, NOT THREE. The first draft had `if (!file || !existsSync(file))`
@@ -177,7 +178,7 @@ const futurePromise = {
177
178
  .split("\n")
178
179
  .map((l) => l.replace(/(^|[^\\])%.*$/, "$1"));
179
180
  lines.forEach((line, i) => {
180
- const m = line.match(FUTURE_PROMISE_RE);
181
+ const m = FUTURE_PROMISE_RE.exec(line);
181
182
  if (!m) return;
182
183
  const column = m.index + 1;
183
184
  context.report({
@@ -259,7 +260,7 @@ const acmFrontmatterOverride = {
259
260
  "on its own",
260
261
  },
261
262
  },
262
- create(context) {
263
+ create(context: RuleContext) {
263
264
  return {
264
265
  root() {
265
266
  // Same single guard as the sibling rule, and for the same measured reason: a separate
package/eslint.config.mjs CHANGED
@@ -11,11 +11,11 @@
11
11
  * is why `scripts/rules-see-files.mjs` exists and why it runs as part of the test suite:
12
12
  * every rule declared below must be enabled for at least one file that is actually on disk.
13
13
  */
14
- import { texLanguage } from "./eslint-rules/latex-language.mjs";
15
- import texBuild from "./eslint-rules/tex-build.mjs";
14
+ import { texLanguage } from "#eslint-rules/latex-language";
15
+ import texBuild from "#eslint-rules/tex-build";
16
16
  import markdown from "@eslint/markdown";
17
- import localRules from "./eslint-rules/temp-root-realpath.mjs";
18
- import portRules from "./eslint-rules/install-path-literals.mjs";
17
+ import localRules from "#eslint-rules/temp-root-realpath";
18
+ import portRules from "#eslint-rules/install-path-literals";
19
19
  import n from "eslint-plugin-n";
20
20
  import tseslint from "typescript-eslint";
21
21
  import boundaries from "eslint-plugin-boundaries";
@@ -25,6 +25,35 @@ import { fileURLToPath } from "node:url";
25
25
  // The complexity set, shared by the TypeScript block and the ratchet below. Every function
26
26
  // measured over these limits on 2026-09-24 was either refactored under them (the #59 build code)
27
27
  // or pinned at its current maximum in RATCHET.
28
+ const TS_FILES = ["**/*.ts", "**/*.mts"];
29
+
30
+ /** `x as unknown as T` tells the checker to look away (#76). Shared: a block REPLACES a rule's options. */
31
+ const AS_UNKNOWN_AS = {
32
+ selector:
33
+ "TSAsExpression > TSAsExpression[typeAnnotation.type='TSUnknownKeyword']",
34
+ message:
35
+ "`as unknown as` switches the type checker off. Convert with a function, or fix the type.",
36
+ };
37
+
38
+ /**
39
+ * A RULE MAY NOT ASK GIT. `paper/source` once bound a paper's stage to a commit sha and checked it
40
+ * with `git cat-file -e`; of four recorded shas, one still resolved ninety minutes later, because
41
+ * squash-merge and `gc` delete commits as routine. And `actions/checkout` fetches one commit, so
42
+ * under CI even a live sha does not resolve: the rule is red where it must hold and green locally.
43
+ * A rule reads the files ESLint gives it. Matched on the AST: a spawner called with the program
44
+ * `git` (or a shell line starting with `git `). Two things pass, deliberately: opening `.git/` with
45
+ * `fs`, and a program name assembled at runtime.
46
+ */
47
+ const SPAWNERS = "exec|execSync|execFile|execFileSync|spawn|spawnSync|fork";
48
+ const GIT_IN_A_RULE = [
49
+ `CallExpression[callee.name=/^(${SPAWNERS})$/][arguments.0.value=/^git( |$)/]`,
50
+ `CallExpression[callee.property.name=/^(${SPAWNERS})$/][arguments.0.value=/^git( |$)/]`,
51
+ ].map((selector) => ({
52
+ selector,
53
+ message:
54
+ "A rule may not run git: a sha is deleted by routine maintenance and absent from a shallow CI checkout. Read the file content ESLint passes the rule.",
55
+ }));
56
+
28
57
  const MAX_LINES = { max: 60, skipComments: true, skipBlankLines: true };
29
58
 
30
59
  // 🔴 A CEILING, NOT A PERMISSION: each number is the file's measured maximum on 2026-09-24, so a
@@ -38,6 +67,20 @@ const RATCHET = {
38
67
  "src/hooks-settings.ts": { complexity: 12, "max-depth": 4 },
39
68
  "src/structure.ts": { complexity: 12, "max-depth": 4 },
40
69
  "src/new-paper.ts": { complexity: 11 },
70
+ // Moved from JavaScript on 2026-09-27 (#78) and measured that day: the move changed their
71
+ // language, not their shape, so their debt is pinned here like the rest rather than paid in it.
72
+ "eslint-rules/latex-language.ts": {
73
+ complexity: 68,
74
+ "max-lines-per-function": 314,
75
+ "max-depth": 4,
76
+ },
77
+ "eslint-rules/paper-typography.ts": { complexity: 29, "max-depth": 5 },
78
+ "eslint-rules/paper-stages.ts": {
79
+ complexity: 15,
80
+ "max-lines-per-function": 105,
81
+ },
82
+ "eslint-rules/temp-root-realpath.ts": { complexity: 13 },
83
+ "lib/markdown.ts": { complexity: 12 },
41
84
  };
42
85
 
43
86
  // ── Hexagonal layers (src/CLAUDE.md, issue #76) ─────────────────────────────────────────
@@ -121,6 +164,15 @@ export const layerBoundaries = (root) => ({
121
164
  plugins: { boundaries },
122
165
  settings: {
123
166
  "boundaries/root-path": root,
167
+ // `#lib/*` and `#eslint-rules/*` are package.json subpath imports; the Node resolver the plugin
168
+ // defaults to does not read that map and leaves them unresolved, i.e. unknown. The TypeScript
169
+ // resolver reads it, and with the repository's source condition lands on the `.ts` file, which
170
+ // is classified as the js-module element it is.
171
+ "import/resolver": {
172
+ typescript: {
173
+ conditionNames: ["paperlint-source", "types", "import", "node"],
174
+ },
175
+ },
124
176
  "boundaries/elements": [
125
177
  { type: "port", pattern: "src/ports", partialMatch: false },
126
178
  { type: "domain", pattern: "src/domain", partialMatch: false },
@@ -138,7 +190,7 @@ export const layerBoundaries = (root) => ({
138
190
  partialMatch: false,
139
191
  capture: ["kind"],
140
192
  },
141
- // test/support.mjs — shared helpers for tests. Only the `test` category may import it: the
193
+ // test/support.ts — shared helpers for tests. Only the `test` category may import it: the
142
194
  // policies below allow tests any element, and allow no other file this one.
143
195
  { type: "test-support", pattern: "test", partialMatch: false },
144
196
  ],
@@ -147,7 +199,8 @@ export const layerBoundaries = (root) => ({
147
199
  { category: "root", pattern: "src/cli.ts", exclusive: true },
148
200
  // Every other file at the top of src/ is app. A new one gets the full app rules.
149
201
  { category: "app", pattern: "src/*.ts" },
150
- { category: "test", pattern: "src/**/*.test.ts" },
202
+ // A harness is a test too: it drives the code through vigiles instead of vitest.
203
+ { category: "test", pattern: "src/**/*.{test,harness}.ts" },
151
204
  // AXIS B: purity is a category, orthogonal to the element.
152
205
  { category: "io", pattern: "src/adapters/*/*.io.ts" },
153
206
  ],
@@ -243,12 +296,17 @@ export const layerBoundaries = (root) => ({
243
296
  * Files that did I/O before the layers existed carry an `eslint-disable-next-line` naming #76 above
244
297
  * each such import or use; `reportUnusedDisableDirectives: "error"` turns a disable that suppresses
245
298
  * nothing into a finding, so an exemption leaves the moment its I/O does, and
246
- * `scripts/layer-legacy-frozen.mjs` freezes how many each file may carry. A NEW module that needs
299
+ * `scripts/layer-legacy-frozen.ts` freezes how many each file may carry. A NEW module that needs
247
300
  * the outside world is a new `*.io.ts` in the adapter of the program it talks to — not a disable.
248
301
  */
249
302
  export const IO_GLOBALS = {
250
303
  files: ["src/**/*.ts"],
251
- ignores: ["src/cli.ts", "src/adapters/*/*.io.ts", "src/**/*.test.ts"],
304
+ ignores: [
305
+ "src/cli.ts",
306
+ "src/adapters/*/*.io.ts",
307
+ "src/**/*.test.ts",
308
+ "src/**/*.harness.ts",
309
+ ],
252
310
  linterOptions: { reportUnusedDisableDirectives: "error" },
253
311
  rules: {
254
312
  "no-restricted-globals": [
@@ -271,6 +329,27 @@ const ceiling = (rule, n) =>
271
329
  ? ["error", { ...MAX_LINES, max: n }]
272
330
  : ["error", n];
273
331
 
332
+ /**
333
+ * New code is TypeScript (#78). A JavaScript file (`.js`, `.mjs`, `.cjs`) under any of these
334
+ * directories is an error, whatever its name — the check is on the file's AST root, so a name with
335
+ * any number of dots and an empty file are both caught. The directories are the ones already moved
336
+ * to TypeScript; each later step of #78 adds the directories it converts, and the last one replaces
337
+ * the list with every JavaScript file in the repository.
338
+ */
339
+ const TYPESCRIPT_ONLY = [
340
+ "src/adapters/**/*.{js,mjs,cjs}",
341
+ "src/domain/**/*.{js,mjs,cjs}",
342
+ "src/ports/**/*.{js,mjs,cjs}",
343
+ // test/ itself and test/e2e — not test/fixtures/layers/eslint-rules, whose `rule.mjs` stands for
344
+ // the JavaScript module it is linted as.
345
+ "test/*.{js,mjs,cjs}",
346
+ "test/e2e/**/*.{js,mjs,cjs}",
347
+ "test/fixtures/layers/src/**/*.{js,mjs,cjs}",
348
+ "fixtures/real-markdown-paper/**/*.{js,mjs,cjs}",
349
+ "skills/paper-pipeline/scripts/fixtures/**/*.{js,mjs,cjs}",
350
+ "skills/plan-paper-timeline/fixtures/**/*.{js,mjs,cjs}",
351
+ ];
352
+
274
353
  export default [
275
354
  // 🔴 TRANSIENT DIRECTORIES ARE NOT THE CORPUS, and leaving them in is a RACE, not sloppiness.
276
355
  // `paper-stages.harness.mjs` creates a temp tree under fixtures and removes it when done,
@@ -297,37 +376,51 @@ export default [
297
376
  // 🔴 THE PACKAGE'S TYPESCRIPT, and before 2026-09-24 no block matched it — the same silent
298
377
  // ignore the `.mjs` block below was written against, recurring for `.ts` (#49). `src/` is the
299
378
  // CLI and the build; the skill specs compile into the SKILL.md files the package ships; the
300
- // three hand-written `.d.mts` type the `.mjs` modules `src/` imports. All tracked TypeScript.
379
+ // hand-written `.d.mts` files type the `.mjs` modules `src/` imports. All tracked TypeScript.
301
380
  //
302
381
  // Measured on the first run (41727cd): 44 findings in 9 of 39 files, all in `src/`, plus one
303
382
  // `no-useless-escape` in a spec that had dropped a backslash from its compiled SKILL.md. The
304
383
  // #59 build files were refactored clean; the older CLI files sit in RATCHET, one block below.
305
384
  //
306
- // NOT TYPE-AWARE, deliberately: no rule here needs type information, and `npm run build`
307
- // (tsc, strict) already type-checks `src/` as the first gate of `npm run check`.
385
+ // TYPE-AWARE since 2026-09-27: typescript-eslint's `strictTypeChecked` preset, fed by
386
+ // `tsconfig.test.json`, which now includes EVERY tracked `.ts`/`.mts` (the 24 skill specs and
387
+ // `vitest.config.ts` had been in no tsconfig, so neither tsc nor this block type-checked them).
388
+ // `parserOptions.project` rather than `projectService`: the service looks for the nearest
389
+ // `tsconfig.json`, and the root one covers only the shipped `src/`, so every test, script and
390
+ // spec would fall back to a default project. First run: 374 findings in 64 files, all fixed in
391
+ // the same PR, none suppressed.
308
392
  //
309
393
  // NOT HERE: `port/js-install-path` (43 findings, 41 of them the specs' `.claude/skills/`
310
394
  // literals — issue #19's debt, `warn` for `.mjs` for the same reason; 2 in hooks-settings.ts,
311
395
  // which writes the consumer's settings and must name the install) and `n/no-missing-import`
312
- // (tsc already fails on an unresolved import in `src/`).
396
+ // (tsc already fails on an unresolved import).
397
+ ...tseslint.configs.strictTypeChecked.map((config) => ({
398
+ ...config,
399
+ files: TS_FILES,
400
+ })),
313
401
  {
314
- files: ["**/*.ts", "**/*.mts"],
315
- languageOptions: { parser: tseslint.parser },
402
+ files: TS_FILES,
403
+ languageOptions: {
404
+ parser: tseslint.parser,
405
+ parserOptions: {
406
+ project: "./tsconfig.test.json",
407
+ tsconfigRootDir: dirname(fileURLToPath(import.meta.url)),
408
+ },
409
+ },
316
410
  plugins: { "@typescript-eslint": tseslint.plugin, local: localRules },
317
411
  rules: {
318
412
  // The same macOS-only defect as in the `.mjs` block; clean here, so it opens at `error`.
319
413
  "local/temp-root-realpath": "error",
320
- // A dead import is a sign of an incomplete edit. The TypeScript variant, because the core
321
- // rule does not understand type-only positions.
322
- "@typescript-eslint/no-unused-vars": "error",
323
- // `any` switches the checker off for everything it flows into — the gate that makes `src/`
324
- // TypeScript at all. Five older sites are pinned inline and named in #49.
414
+ // `any` switches the checker off for everything it flows into. The preset has it too; named
415
+ // here so that dropping the preset cannot drop it.
325
416
  "@typescript-eslint/no-explicit-any": "error",
326
417
  // The behaviour rules of the `.mjs` block, for the same reasons: each changes what the code
327
418
  // DOES, not how it looks, and a regex that escapes the wrong thing searches for the wrong thing.
328
419
  "no-empty": "error",
329
420
  "no-constant-condition": "error",
330
- "no-dupe-keys": "error",
421
+ // `no-unreachable` is switched off by the preset's `eslint-recommended` layer on the claim
422
+ // that tsc reports it; tsc only greys unreachable code unless `allowUnreachableCode` is
423
+ // false, so it is switched back on. (`no-dupe-keys` is dropped: tsc does fail on it.)
331
424
  "no-unreachable": "error",
332
425
  "no-fallthrough": "error",
333
426
  "no-useless-escape": "error",
@@ -339,15 +432,7 @@ export default [
339
432
  complexity: ["error", 10],
340
433
  // `x as unknown as T` tells the checker to look away: two spellings of one port were once
341
434
  // reconciled that way (#76). A real conversion is a function; a real subset needs no cast.
342
- "no-restricted-syntax": [
343
- "error",
344
- {
345
- selector:
346
- "TSAsExpression > TSAsExpression[typeAnnotation.type='TSUnknownKeyword']",
347
- message:
348
- "`as unknown as` switches the type checker off. Convert with a function, or fix the type.",
349
- },
350
- ],
435
+ "no-restricted-syntax": ["error", AS_UNKNOWN_AS],
351
436
  // Nesting beyond three blocks is where a step belongs in its own named function.
352
437
  "max-depth": ["error", 3],
353
438
  // Five positional parameters are a record without field names; pass an object instead.
@@ -423,7 +508,9 @@ export default [
423
508
  // unreachable code, fallthrough in `case`.
424
509
  "no-empty": "error",
425
510
  "no-constant-condition": "error",
426
- "no-dupe-keys": "error",
511
+ // `no-unreachable` is switched off by the preset's `eslint-recommended` layer on the claim
512
+ // that tsc reports it; tsc only greys unreachable code unless `allowUnreachableCode` is
513
+ // false, so it is switched back on. (`no-dupe-keys` is dropped: tsc does fail on it.)
427
514
  "no-unreachable": "error",
428
515
  "no-fallthrough": "error",
429
516
  // Regexes: unnecessary escaping slash and control character in class — both findings about
@@ -507,4 +594,26 @@ export default [
507
594
  // one whose firing path nothing exercises, which is rule 4 wearing a different hat. The
508
595
  // pass/fail signal lives in `npm test`, not in the warning count of `npm run lint`.
509
596
  },
597
+ // Rule sources, whatever their extension; tests and harnesses plant git calls on purpose.
598
+ {
599
+ files: ["eslint-rules/*.ts", "eslint-rules/*.mjs"],
600
+ ignores: ["eslint-rules/*.test.*", "eslint-rules/*.harness.*"],
601
+ rules: {
602
+ "no-restricted-syntax": ["error", AS_UNKNOWN_AS, ...GIT_IN_A_RULE],
603
+ },
604
+ },
605
+ // LAST, so no block above can replace its `no-restricted-syntax` options for these files (none
606
+ // sets that rule for JavaScript today; being last keeps it that way).
607
+ {
608
+ files: TYPESCRIPT_ONLY,
609
+ rules: {
610
+ "no-restricted-syntax": [
611
+ "error",
612
+ {
613
+ selector: "Program",
614
+ message: "New code is TypeScript (#78): write this file as .ts.",
615
+ },
616
+ ],
617
+ },
618
+ },
510
619
  ];
@@ -1,6 +1,6 @@
1
1
  # PDF fixtures for the pdf.js reader
2
2
 
3
- Real PDFs that `src/pdf-facts.harness.mjs` and `skills/render-paper/extract-pdf-facts.harness.mjs`
3
+ Real PDFs that `src/pdf-facts.harness.ts` and `skills/render-paper/extract-pdf-facts.harness.ts`
4
4
  read. They were measured with poppler and with pdf.js on 2026-09-24 (issue #61); the harnesses pin
5
5
  poppler's numbers, so a pass means agreement with the tool pdf.js replaced.
6
6
 
@@ -13,7 +13,7 @@ poppler's numbers, so a pass means agreement with the tool pdf.js replaced.
13
13
  | `encrypted.pdf` | `t3-all.pdf` saved with an AES-256 user password (PyMuPDF) | pdf.js refuses it with `PasswordException`; the reader reports `encrypted` |
14
14
  | `hidden-text.pdf` | `hidden-text.tex`, pdflatex | two-column body text beside near-white, rotated and render-mode-3 text; page 2 holds only hidden text, so banal calls it blank |
15
15
 
16
- `test/e2e/banal.mjs` also reads these files, with the real banal: it pins what banal 1.2 printed for
16
+ `test/e2e/banal.ts` also reads these files, with the real banal: it pins what banal 1.2 printed for
17
17
  each one on poppler's `pdftohtml` 24.02.0 (2026-09-25), so a pass there means paperlint's pdf.js-written
18
18
  XML measures the same as poppler's. `hidden-text.pdf` exists for that test: each kind of hidden text
19
19
  changes banal's answer if it is counted.
@@ -1,18 +1,31 @@
1
1
  /**
2
2
  * ONE reader of `baseline.json`, shared by the two runs that compare against it: the harness
3
- * beside this file (the repository's own `bin/paperlint.mjs`) and `test/e2e/install.mjs` (the binary
3
+ * beside this file (the repository's own `bin/paperlint.mjs`) and `test/e2e/install.ts` (the binary
4
4
  * a consumer actually got). Two copies of "growth fails, a drop never does" would drift the first
5
5
  * time one of them is tightened, and the two runs would then disagree about the same article.
6
6
  */
7
7
  import { readFileSync } from "node:fs";
8
8
  import { dirname, join } from "node:path";
9
9
  import { fileURLToPath } from "node:url";
10
+ import { z } from "zod";
11
+
12
+ /** Findings per rule id. */
13
+ export type Counts = Record<string, number>;
14
+
15
+ const Baseline = z.object({ findings: z.record(z.string(), z.number()) });
16
+ const LintOutput = z.array(
17
+ z.object({
18
+ messages: z.array(z.object({ ruleId: z.string().nullable() })).nullish(),
19
+ }),
20
+ );
10
21
 
11
22
  const HERE = dirname(fileURLToPath(import.meta.url));
12
23
 
13
24
  /** The recorded counts, `{ruleId: n}`. */
14
- export function recordedFindings() {
15
- return JSON.parse(readFileSync(join(HERE, "baseline.json"), "utf8")).findings;
25
+ export function recordedFindings(): Counts {
26
+ return Baseline.parse(
27
+ JSON.parse(readFileSync(join(HERE, "baseline.json"), "utf8")),
28
+ ).findings;
16
29
  }
17
30
 
18
31
  /**
@@ -20,11 +33,13 @@ export function recordedFindings() {
20
33
  * 🔴 Output that does not parse THROWS. An empty set read as "clean" is how this repository's
21
34
  * checks have gone hollow before.
22
35
  */
23
- export function countByRule(stdout) {
24
- const out = {};
25
- for (const file of JSON.parse(stdout))
26
- for (const m of file.messages ?? [])
27
- out[m.ruleId] = (out[m.ruleId] ?? 0) + 1;
36
+ export function countByRule(stdout: string): Counts {
37
+ const out: Counts = {};
38
+ for (const file of LintOutput.parse(JSON.parse(stdout)))
39
+ for (const m of file.messages ?? []) {
40
+ const rule = String(m.ruleId);
41
+ out[rule] = (out[rule] ?? 0) + 1;
42
+ }
28
43
  return out;
29
44
  }
30
45
 
@@ -37,12 +52,18 @@ export function countByRule(stdout) {
37
52
  * A partial drop is neither: it is what a fix looks like (paperlint#44 is expected to take typography
38
53
  * to zero, and then the recording is updated in the same change).
39
54
  */
40
- export function compareToBaseline(found, recorded = recordedFindings()) {
55
+ export function compareToBaseline(
56
+ found: Readonly<Counts>,
57
+ recorded: Readonly<Counts> = recordedFindings(),
58
+ ): {
59
+ grew: { rule: string; now: number; recorded: number }[];
60
+ vanished: string[];
61
+ } {
41
62
  const grew = Object.entries(found)
42
63
  .filter(([rule, n]) => n > (recorded[rule] ?? 0))
43
64
  .map(([rule, n]) => ({ rule, now: n, recorded: recorded[rule] ?? 0 }));
44
65
  const vanished = Object.keys(recorded).filter(
45
- (r) => recorded[r] > 0 && !(r in found),
66
+ (r) => (recorded[r] ?? 0) > 0 && !(r in found),
46
67
  );
47
68
  return { grew, vanished };
48
69
  }