paperlint 3.1.3 → 4.0.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 (527) hide show
  1. package/.github/workflows/ci.yml +12 -7
  2. package/CLAUDE.md +5 -5
  3. package/CONTRIBUTING.md +34 -5
  4. package/dist/adapters/banal/failure.d.ts +1 -7
  5. package/dist/adapters/banal/failure.d.ts.map +1 -1
  6. package/dist/adapters/banal/failure.js +5 -7
  7. package/dist/adapters/banal/failure.js.map +1 -1
  8. package/dist/adapters/banal/index.d.ts.map +1 -1
  9. package/dist/adapters/banal/index.js +4 -3
  10. package/dist/adapters/banal/index.js.map +1 -1
  11. package/dist/adapters/banal/install.d.ts.map +1 -1
  12. package/dist/adapters/banal/install.js +4 -3
  13. package/dist/adapters/banal/install.js.map +1 -1
  14. package/dist/adapters/banal/invocation.d.ts +10 -5
  15. package/dist/adapters/banal/invocation.d.ts.map +1 -1
  16. package/dist/adapters/banal/invocation.js +13 -1
  17. package/dist/adapters/banal/invocation.js.map +1 -1
  18. package/dist/adapters/banal/locate.d.ts +12 -9
  19. package/dist/adapters/banal/locate.d.ts.map +1 -1
  20. package/dist/adapters/banal/locate.js +19 -12
  21. package/dist/adapters/banal/locate.js.map +1 -1
  22. package/dist/adapters/banal/run.d.ts +10 -1
  23. package/dist/adapters/banal/run.d.ts.map +1 -1
  24. package/dist/adapters/banal/run.js +8 -4
  25. package/dist/adapters/banal/run.js.map +1 -1
  26. package/dist/adapters/banal/settings.d.ts +1 -1
  27. package/dist/adapters/banal/settings.d.ts.map +1 -1
  28. package/dist/adapters/banal/settings.js +2 -1
  29. package/dist/adapters/banal/settings.js.map +1 -1
  30. package/dist/adapters/memory/index.d.ts.map +1 -1
  31. package/dist/adapters/memory/index.js +8 -2
  32. package/dist/adapters/memory/index.js.map +1 -1
  33. package/dist/adapters/node/files.io.d.ts.map +1 -1
  34. package/dist/adapters/node/files.io.js +2 -1
  35. package/dist/adapters/node/files.io.js.map +1 -1
  36. package/dist/adapters/node/process.io.d.ts +5 -7
  37. package/dist/adapters/node/process.io.d.ts.map +1 -1
  38. package/dist/adapters/node/process.io.js +4 -3
  39. package/dist/adapters/node/process.io.js.map +1 -1
  40. package/dist/adapters/node/workspace.io.d.ts.map +1 -1
  41. package/dist/adapters/node/workspace.io.js +4 -2
  42. package/dist/adapters/node/workspace.io.js.map +1 -1
  43. package/dist/adapters/pdfjs/fill.d.ts.map +1 -1
  44. package/dist/adapters/pdfjs/fill.js +13 -7
  45. package/dist/adapters/pdfjs/fill.js.map +1 -1
  46. package/dist/adapters/references/index.d.ts.map +1 -1
  47. package/dist/adapters/references/index.js +8 -7
  48. package/dist/adapters/references/index.js.map +1 -1
  49. package/dist/adapters/references/pool.d.ts.map +1 -1
  50. package/dist/adapters/references/pool.js +4 -5
  51. package/dist/adapters/references/pool.js.map +1 -1
  52. package/dist/adapters/references/reach.io.d.ts.map +1 -1
  53. package/dist/adapters/references/reach.io.js +2 -1
  54. package/dist/adapters/references/reach.io.js.map +1 -1
  55. package/dist/build-engine.d.ts +1 -1
  56. package/dist/build-engine.d.ts.map +1 -1
  57. package/dist/build-engine.js +10 -3
  58. package/dist/build-engine.js.map +1 -1
  59. package/dist/build.d.ts +2 -3
  60. package/dist/build.d.ts.map +1 -1
  61. package/dist/build.js +19 -17
  62. package/dist/build.js.map +1 -1
  63. package/dist/caller-path.d.ts +3 -0
  64. package/dist/caller-path.d.ts.map +1 -0
  65. package/dist/caller-path.js +13 -0
  66. package/dist/caller-path.js.map +1 -0
  67. package/dist/cli.d.ts +10 -17
  68. package/dist/cli.d.ts.map +1 -1
  69. package/dist/cli.js +93 -46
  70. package/dist/cli.js.map +1 -1
  71. package/dist/doctor.d.ts +12 -3
  72. package/dist/doctor.d.ts.map +1 -1
  73. package/dist/doctor.js +5 -3
  74. package/dist/doctor.js.map +1 -1
  75. package/dist/domain/lookup-cache.d.ts.map +1 -1
  76. package/dist/domain/lookup-cache.js +10 -8
  77. package/dist/domain/lookup-cache.js.map +1 -1
  78. package/dist/domain/paths.d.ts +8 -2
  79. package/dist/domain/paths.d.ts.map +1 -1
  80. package/dist/domain/paths.js +12 -1
  81. package/dist/domain/paths.js.map +1 -1
  82. package/dist/domain/ready.d.ts +24 -0
  83. package/dist/domain/ready.d.ts.map +1 -0
  84. package/dist/domain/ready.js +11 -0
  85. package/dist/domain/ready.js.map +1 -0
  86. package/dist/domain/record.d.ts +8 -0
  87. package/dist/domain/record.d.ts.map +1 -0
  88. package/dist/domain/record.js +8 -0
  89. package/dist/domain/record.js.map +1 -0
  90. package/dist/domain/sha256.d.ts.map +1 -1
  91. package/dist/domain/sha256.js +4 -2
  92. package/dist/domain/sha256.js.map +1 -1
  93. package/dist/domain/text.d.ts +12 -0
  94. package/dist/domain/text.d.ts.map +1 -1
  95. package/dist/domain/text.js +17 -0
  96. package/dist/domain/text.js.map +1 -1
  97. package/dist/engine.d.ts +23 -22
  98. package/dist/engine.d.ts.map +1 -1
  99. package/dist/engine.js +4 -3
  100. package/dist/engine.js.map +1 -1
  101. package/dist/eslint-rules/bib-reachable-entry.d.ts +22 -0
  102. package/dist/eslint-rules/bib-reachable-entry.d.ts.map +1 -0
  103. package/dist/eslint-rules/bib-reachable-entry.js +75 -0
  104. package/dist/eslint-rules/bib-reachable-entry.js.map +1 -0
  105. package/dist/eslint-rules/install-path-literals.d.ts +80 -0
  106. package/dist/eslint-rules/install-path-literals.d.ts.map +1 -0
  107. package/dist/eslint-rules/install-path-literals.js +105 -0
  108. package/dist/eslint-rules/install-path-literals.js.map +1 -0
  109. package/dist/eslint-rules/latex-language.d.ts +136 -0
  110. package/dist/eslint-rules/latex-language.d.ts.map +1 -0
  111. package/dist/eslint-rules/latex-language.js +639 -0
  112. package/dist/eslint-rules/latex-language.js.map +1 -0
  113. package/dist/eslint-rules/paper-research-question.d.ts +31 -0
  114. package/dist/eslint-rules/paper-research-question.d.ts.map +1 -0
  115. package/dist/eslint-rules/paper-research-question.js +188 -0
  116. package/dist/eslint-rules/paper-research-question.js.map +1 -0
  117. package/dist/eslint-rules/paper-stages.d.ts +83 -0
  118. package/dist/eslint-rules/paper-stages.d.ts.map +1 -0
  119. package/dist/eslint-rules/paper-stages.js +358 -0
  120. package/dist/eslint-rules/paper-stages.js.map +1 -0
  121. package/dist/eslint-rules/paper-typography.d.ts +74 -0
  122. package/dist/eslint-rules/paper-typography.d.ts.map +1 -0
  123. package/dist/eslint-rules/paper-typography.js +404 -0
  124. package/dist/eslint-rules/paper-typography.js.map +1 -0
  125. package/dist/eslint-rules/papers.d.ts +42 -0
  126. package/dist/eslint-rules/papers.d.ts.map +1 -0
  127. package/dist/eslint-rules/papers.js +145 -0
  128. package/dist/eslint-rules/papers.js.map +1 -0
  129. package/dist/eslint-rules/pdf-last-page-balance.d.ts +72 -0
  130. package/dist/eslint-rules/pdf-last-page-balance.d.ts.map +1 -0
  131. package/dist/eslint-rules/pdf-last-page-balance.js +237 -0
  132. package/dist/eslint-rules/pdf-last-page-balance.js.map +1 -0
  133. package/dist/eslint-rules/review-frontmatter.d.ts +57 -0
  134. package/dist/eslint-rules/review-frontmatter.d.ts.map +1 -0
  135. package/dist/eslint-rules/review-frontmatter.js +126 -0
  136. package/dist/eslint-rules/review-frontmatter.js.map +1 -0
  137. package/dist/eslint-rules/review-frontmatter.schema.json +36 -0
  138. package/dist/eslint-rules/rule-context.d.ts +55 -0
  139. package/dist/eslint-rules/rule-context.d.ts.map +1 -0
  140. package/dist/eslint-rules/rule-context.js +9 -0
  141. package/dist/eslint-rules/rule-context.js.map +1 -0
  142. package/dist/eslint-rules/sibling-frontmatter.d.ts +25 -0
  143. package/dist/eslint-rules/sibling-frontmatter.d.ts.map +1 -0
  144. package/dist/eslint-rules/sibling-frontmatter.js +16 -0
  145. package/dist/eslint-rules/sibling-frontmatter.js.map +1 -0
  146. package/dist/eslint-rules/sibling-frontmatter.schema.json +14 -0
  147. package/dist/eslint-rules/temp-root-realpath.d.ts +71 -0
  148. package/dist/eslint-rules/temp-root-realpath.d.ts.map +1 -0
  149. package/dist/eslint-rules/temp-root-realpath.js +139 -0
  150. package/dist/eslint-rules/temp-root-realpath.js.map +1 -0
  151. package/dist/eslint-rules/tex-build.d.ts +35 -0
  152. package/dist/eslint-rules/tex-build.d.ts.map +1 -0
  153. package/dist/eslint-rules/tex-build.js +313 -0
  154. package/dist/eslint-rules/tex-build.js.map +1 -0
  155. package/dist/facts-file.d.ts.map +1 -1
  156. package/dist/facts-file.js +31 -27
  157. package/dist/facts-file.js.map +1 -1
  158. package/dist/hooks-settings.d.ts +1 -1
  159. package/dist/hooks-settings.d.ts.map +1 -1
  160. package/dist/hooks-settings.js +31 -11
  161. package/dist/hooks-settings.js.map +1 -1
  162. package/dist/init.d.ts +8 -9
  163. package/dist/init.d.ts.map +1 -1
  164. package/dist/init.js +33 -15
  165. package/dist/init.js.map +1 -1
  166. package/dist/latex-loop.d.ts +2 -2
  167. package/dist/latex-loop.d.ts.map +1 -1
  168. package/dist/latex-loop.js +2 -2
  169. package/dist/latex-loop.js.map +1 -1
  170. package/dist/lib/agent-cli-version.d.ts +65 -0
  171. package/dist/lib/agent-cli-version.d.ts.map +1 -0
  172. package/dist/lib/agent-cli-version.js +77 -0
  173. package/dist/lib/agent-cli-version.js.map +1 -0
  174. package/dist/lib/check.d.ts +19 -0
  175. package/dist/lib/check.d.ts.map +1 -0
  176. package/dist/lib/check.js +52 -0
  177. package/dist/lib/check.js.map +1 -0
  178. package/dist/lib/markdown.d.ts +180 -0
  179. package/dist/lib/markdown.d.ts.map +1 -0
  180. package/dist/lib/markdown.js +302 -0
  181. package/dist/lib/markdown.js.map +1 -0
  182. package/dist/lib/paper-config.d.ts +84 -0
  183. package/dist/lib/paper-config.d.ts.map +1 -0
  184. package/dist/lib/paper-config.js +138 -0
  185. package/dist/lib/paper-config.js.map +1 -0
  186. package/dist/lib/skill-eval-fixture.d.ts +29 -0
  187. package/dist/lib/skill-eval-fixture.d.ts.map +1 -0
  188. package/dist/lib/skill-eval-fixture.js +62 -0
  189. package/dist/lib/skill-eval-fixture.js.map +1 -0
  190. package/dist/link-skills.d.ts.map +1 -1
  191. package/dist/link-skills.js +5 -4
  192. package/dist/link-skills.js.map +1 -1
  193. package/dist/new-paper.d.ts.map +1 -1
  194. package/dist/new-paper.js +7 -3
  195. package/dist/new-paper.js.map +1 -1
  196. package/dist/paper-files.d.ts +4 -4
  197. package/dist/paper-files.d.ts.map +1 -1
  198. package/dist/paper-files.js +6 -6
  199. package/dist/paper-files.js.map +1 -1
  200. package/dist/paper-settings.d.ts.map +1 -1
  201. package/dist/paper-settings.js +8 -7
  202. package/dist/paper-settings.js.map +1 -1
  203. package/dist/pdf-facts.d.ts +51 -1
  204. package/dist/pdf-facts.d.ts.map +1 -1
  205. package/dist/pdf-facts.js +25 -16
  206. package/dist/pdf-facts.js.map +1 -1
  207. package/dist/pdf-geometry.d.ts.map +1 -1
  208. package/dist/pdf-geometry.js +3 -4
  209. package/dist/pdf-geometry.js.map +1 -1
  210. package/dist/ports/tool-installer.d.ts +2 -14
  211. package/dist/ports/tool-installer.d.ts.map +1 -1
  212. package/dist/presets.d.ts.map +1 -1
  213. package/dist/presets.js +19 -6
  214. package/dist/presets.js.map +1 -1
  215. package/dist/references.d.ts.map +1 -1
  216. package/dist/references.js +15 -10
  217. package/dist/references.js.map +1 -1
  218. package/dist/rules-config.d.ts +26 -1
  219. package/dist/rules-config.d.ts.map +1 -1
  220. package/dist/rules-config.js +15 -12
  221. package/dist/rules-config.js.map +1 -1
  222. package/dist/structure.js +1 -1
  223. package/dist/structure.js.map +1 -1
  224. package/dist/tex-requirements.d.ts +4 -0
  225. package/dist/tex-requirements.d.ts.map +1 -1
  226. package/dist/tex-requirements.js +22 -5
  227. package/dist/tex-requirements.js.map +1 -1
  228. package/dist/toolchain.d.ts.map +1 -1
  229. package/dist/toolchain.js +21 -16
  230. package/dist/toolchain.js.map +1 -1
  231. package/dist/types.d.ts +5 -3
  232. package/dist/types.d.ts.map +1 -1
  233. package/dist/venue-rules.d.ts.map +1 -1
  234. package/dist/venue-rules.js +21 -8
  235. package/dist/venue-rules.js.map +1 -1
  236. package/docs/configuration.md +2 -2
  237. package/docs/e2e.md +7 -7
  238. package/docs/install.md +1 -1
  239. package/docs/prior-art/README.md +1 -1
  240. package/docs/prior-art/package-location.md +5 -5
  241. package/docs/prior-art/repro/README.md +1 -1
  242. package/docs/prior-art/test-tooling.md +2 -2
  243. package/docs/testing.md +11 -8
  244. package/docs/toolchain.md +1 -1
  245. package/eslint-rules/{bib-reachable-entry.mjs → bib-reachable-entry.ts} +5 -4
  246. package/eslint-rules/install-path-literals.harness.mjs +1 -1
  247. package/eslint-rules/{install-path-literals.mjs → install-path-literals.ts} +18 -10
  248. package/eslint-rules/latex-language.harness.mjs +2 -2
  249. package/eslint-rules/latex-language.test.mjs +7 -1
  250. package/eslint-rules/{latex-language.mjs → latex-language.ts} +297 -109
  251. package/eslint-rules/paper-research-question.harness.mjs +3 -3
  252. package/eslint-rules/{paper-research-question.mjs → paper-research-question.ts} +35 -16
  253. package/eslint-rules/paper-stages.harness.mjs +2 -2
  254. package/eslint-rules/paper-stages.test.mjs +8 -2
  255. package/eslint-rules/{paper-stages.mjs → paper-stages.ts} +76 -31
  256. package/eslint-rules/paper-typography.test.mjs +3 -3
  257. package/eslint-rules/{paper-typography.mjs → paper-typography.ts} +134 -56
  258. package/eslint-rules/papers.harness.mjs +2 -2
  259. package/eslint-rules/{papers.mjs → papers.ts} +7 -4
  260. package/eslint-rules/pdf-last-page-balance.harness.mjs +3 -3
  261. package/eslint-rules/{pdf-last-page-balance.mjs → pdf-last-page-balance.ts} +89 -32
  262. package/eslint-rules/review-frontmatter.test.mjs +2 -2
  263. package/eslint-rules/{review-frontmatter.mjs → review-frontmatter.ts} +38 -16
  264. package/eslint-rules/rule-context.ts +62 -0
  265. package/eslint-rules/{sibling-frontmatter.mjs → sibling-frontmatter.ts} +1 -1
  266. package/eslint-rules/temp-root-realpath.harness.mjs +1 -1
  267. package/eslint-rules/{temp-root-realpath.mjs → temp-root-realpath.ts} +20 -7
  268. package/eslint-rules/tex-build.harness.mjs +2 -2
  269. package/eslint-rules/{tex-build.mjs → tex-build.ts} +6 -5
  270. package/eslint.config.mjs +146 -30
  271. package/fixtures/install-path-literals/clean.fixture.mjs +1 -1
  272. package/fixtures/install-path-literals/clean.md +2 -2
  273. package/fixtures/install-path-literals/defect.fixture.mjs +1 -1
  274. package/fixtures/install-path-literals/defect.md +2 -2
  275. package/fixtures/pdf-facts/README.md +2 -2
  276. package/fixtures/real-markdown-paper/{baseline.mjs → baseline.ts} +31 -10
  277. package/hooks/{hooks.harness.mjs → hooks.harness.ts} +51 -34
  278. package/hooks/paper-edit-guard.hook.mjs +1 -1
  279. package/hooks/{paper-edit-guard.test.mjs → paper-edit-guard.test.ts} +3 -7
  280. package/hooks/paper-skills-nudge.hook.mjs +1 -1
  281. package/hooks/{paper-skills-nudge.test.mjs → paper-skills-nudge.test.ts} +2 -6
  282. package/hooks/paper-status-gates.sh +9 -19
  283. package/hooks/{paper-status-gates.test.mjs → paper-status-gates.test.ts} +2 -6
  284. package/lib/agent-cli-version.harness.mjs +1 -1
  285. package/lib/{agent-cli-version.mjs → agent-cli-version.ts} +32 -18
  286. package/lib/check.test.ts +14 -9
  287. package/lib/{check.mjs → check.ts} +22 -9
  288. package/lib/markdown.test.mjs +7 -7
  289. package/lib/{markdown.mjs → markdown.ts} +68 -29
  290. package/lib/paper-config.harness.mjs +9 -9
  291. package/lib/{paper-config.mjs → paper-config.ts} +20 -19
  292. package/lib/skill-checks.mjs +24 -381
  293. package/lib/skill-checks.test.mjs +64 -116
  294. package/lib/skill-corpus.mjs +10 -251
  295. package/lib/skill-corpus.test.mjs +2 -13
  296. package/lib/skill-eval-kit.mjs +1 -1
  297. package/lib/skill-trigger-cases.mjs +1 -1
  298. package/package.json +42 -10
  299. package/scripts/{check.harness.mjs → check.harness.ts} +111 -10
  300. package/scripts/{check.test.mjs → check.test.ts} +13 -10
  301. package/scripts/{check.mjs → check.ts} +116 -42
  302. package/scripts/coverage-config.test.ts +80 -38
  303. package/scripts/eslint-report-guard.test.mjs +1 -1
  304. package/scripts/harness-api.frozen.json +14 -16
  305. package/scripts/harness-api.test.ts +8 -5
  306. package/scripts/layer-legacy-frozen.test.ts +17 -13
  307. package/scripts/{layer-legacy-frozen.mjs → layer-legacy-frozen.ts} +128 -39
  308. package/scripts/release-config.test.ts +12 -4
  309. package/scripts/rules-see-files.test.mjs +1 -1
  310. package/scripts/test.test.ts +78 -0
  311. package/scripts/test.ts +87 -0
  312. package/skills/analyze-sibling-paper/SKILL.md +1 -39
  313. package/skills/analyze-sibling-paper/SKILL.md.spec.ts +0 -38
  314. package/skills/argument-arc/SKILL.md +2 -36
  315. package/skills/argument-arc/SKILL.md.spec.ts +1 -43
  316. package/skills/build-benchmark/SKILL.md +1 -41
  317. package/skills/build-benchmark/SKILL.md.spec.ts +0 -40
  318. package/skills/camera-ready/SKILL.md +1 -34
  319. package/skills/camera-ready/SKILL.md.spec.ts +0 -33
  320. package/skills/cold-read-diff/SKILL.md +1 -35
  321. package/skills/cold-read-diff/SKILL.md.spec.ts +0 -34
  322. package/skills/draft-paper/SKILL.md +2 -41
  323. package/skills/draft-paper/SKILL.md.spec.ts +1 -50
  324. package/skills/extend-paper/SKILL.md +2 -33
  325. package/skills/extend-paper/SKILL.md.spec.ts +1 -42
  326. package/skills/find-venue/SKILL.md +2 -38
  327. package/skills/find-venue/SKILL.md.spec.ts +1 -47
  328. package/skills/grade-paper-writing/SKILL.md +3 -38
  329. package/skills/grade-paper-writing/SKILL.md.spec.ts +2 -47
  330. package/skills/grade-paper-writing/prose-lint.mjs +3 -4
  331. package/skills/grade-paper-writing/{prose-lint.test.mjs → prose-lint.test.ts} +3 -3
  332. package/skills/harden-paper/SKILL.md +1 -39
  333. package/skills/harden-paper/SKILL.md.spec.ts +0 -38
  334. package/skills/map-prior-work/SKILL.md +1 -35
  335. package/skills/map-prior-work/SKILL.md.spec.ts +0 -34
  336. package/skills/osf-artifact-upload/{osf-artifact-upload.harness.mjs → osf-artifact-upload.harness.ts} +3 -4
  337. package/skills/paper-adversarial-review/SKILL.md +2 -35
  338. package/skills/paper-adversarial-review/SKILL.md.spec.ts +1 -43
  339. package/skills/paper-pipeline/SKILL.md +2 -28
  340. package/skills/paper-pipeline/SKILL.md.spec.ts +1 -27
  341. package/skills/paper-pipeline/description-language.eval.mjs +1 -1
  342. package/skills/paper-pipeline/framing-vs-vocabulary.eval.mjs +1 -1
  343. package/skills/paper-pipeline/grade-paper-writing-ablation.eval.mjs +2 -2
  344. package/skills/paper-pipeline/paper-pipeline.harness.mjs +25 -40
  345. package/skills/paper-pipeline/pipeline-firing.eval.mjs +1 -1
  346. package/skills/paper-pipeline/pipeline-language.eval.mjs +1 -1
  347. package/skills/paper-pipeline/references/acceptance-gate.md +1 -6
  348. package/skills/paper-pipeline/references/pipeline-status-template.md +8 -35
  349. package/skills/paper-pipeline/scripts/README.md +30 -225
  350. package/skills/paper-pipeline/scripts/artifact-coverage.harness.mjs +4 -8
  351. package/skills/paper-pipeline/scripts/artifact-coverage.mjs +1 -1
  352. package/skills/paper-pipeline/scripts/{artifact-coverage.test.mjs → artifact-coverage.test.ts} +2 -2
  353. package/skills/paper-pipeline/scripts/consumer.harness.mjs +6 -104
  354. package/skills/paper-pipeline/scripts/consumer.mjs +24 -109
  355. package/skills/paper-pipeline/scripts/consumer.test.mjs +1 -1
  356. package/skills/paper-pipeline/scripts/extract-ref-facts.harness.mjs +1 -1
  357. package/skills/paper-pipeline/scripts/extract-ref-facts.mjs +1 -4
  358. package/skills/paper-pipeline/scripts/extract-ref-facts.test.mjs +1 -1
  359. package/skills/paper-pipeline/scripts/generated-code.harness.mjs +2 -3
  360. package/skills/paper-pipeline/scripts/{generated-code.test.mjs → generated-code.test.ts} +3 -3
  361. package/skills/paper-pipeline/scripts/population-map.harness.mjs +3 -3
  362. package/skills/paper-pipeline/scripts/population-map.mjs +1 -4
  363. package/skills/paper-pipeline/scripts/population-map.test.mjs +1 -1
  364. package/skills/paper-pipeline/scripts/{round-diff.harness.mjs → round-diff.harness.ts} +45 -18
  365. package/skills/paper-pipeline/scripts/round-diff.mjs +1 -1
  366. package/skills/paper-pipeline/scripts/round-diff.test.mjs +1 -1
  367. package/skills/paper-status/SKILL.md +2 -42
  368. package/skills/paper-status/SKILL.md.spec.ts +1 -41
  369. package/skills/pc-panel-review/SKILL.md +2 -46
  370. package/skills/pc-panel-review/SKILL.md.spec.ts +1 -45
  371. package/skills/plan-paper-timeline/SKILL.md +2 -36
  372. package/skills/plan-paper-timeline/SKILL.md.spec.ts +0 -36
  373. package/skills/plan-paper-timeline/fixtures/{fake-google-calendar.mjs → fake-google-calendar.ts} +101 -70
  374. package/skills/plan-paper-timeline/plan-paper-timeline.effects.harness.mjs +3 -3
  375. package/skills/render-paper/SKILL.md +1 -37
  376. package/skills/render-paper/SKILL.md.spec.ts +0 -36
  377. package/skills/render-paper/{extract-pdf-facts.harness.mjs → extract-pdf-facts.harness.ts} +34 -9
  378. package/skills/render-paper/render-paper.harness.mjs +2 -2
  379. package/skills/research-ideate/SKILL.md +2 -39
  380. package/skills/research-ideate/SKILL.md.spec.ts +1 -47
  381. package/skills/study-accepted-papers/SKILL.md +1 -34
  382. package/skills/study-accepted-papers/SKILL.md.spec.ts +0 -33
  383. package/skills/submit-paper/SKILL.md +1 -33
  384. package/skills/submit-paper/SKILL.md.spec.ts +0 -32
  385. package/skills/sweep-design-space/SKILL.md +1 -38
  386. package/skills/sweep-design-space/SKILL.md.spec.ts +0 -37
  387. package/skills/tighten-paper/SKILL.md +2 -39
  388. package/skills/tighten-paper/SKILL.md.spec.ts +1 -47
  389. package/skills/tighten-paper/structure.mjs +5 -12
  390. package/skills/tighten-paper/{structure.test.mjs → structure.test.ts} +4 -3
  391. package/skills/verify-citations/SKILL.md +2 -38
  392. package/skills/verify-citations/SKILL.md.spec.ts +1 -37
  393. package/skills/verify-citations/scripts/{bib-authors.cli.test.mjs → bib-authors.cli.test.ts} +14 -4
  394. package/skills/verify-citations/scripts/bib-authors.d.mts +63 -0
  395. package/skills/verify-citations/scripts/bib-authors.mjs +1 -4
  396. package/skills/verify-citations/scripts/{verify-cites.cli.test.mjs → verify-cites.cli.test.ts} +32 -9
  397. package/skills/verify-citations/scripts/verify-cites.d.mts +58 -0
  398. package/src/CLAUDE.md +1 -1
  399. package/src/adapters/banal/failure.ts +9 -10
  400. package/src/adapters/banal/index.test.ts +5 -8
  401. package/src/adapters/banal/index.ts +8 -7
  402. package/src/adapters/banal/install.ts +9 -4
  403. package/src/adapters/banal/invocation.test.ts +23 -9
  404. package/src/adapters/banal/invocation.ts +24 -9
  405. package/src/adapters/banal/locate.test.ts +2 -2
  406. package/src/adapters/banal/locate.ts +27 -21
  407. package/src/adapters/banal/output.test.ts +1 -1
  408. package/src/adapters/banal/run.test.ts +5 -5
  409. package/src/adapters/banal/run.ts +16 -10
  410. package/src/adapters/banal/settings.ts +2 -2
  411. package/src/adapters/banal/xml.test.ts +1 -1
  412. package/src/adapters/curl/download.test.ts +8 -6
  413. package/src/adapters/memory/index.ts +4 -3
  414. package/src/adapters/node/files.io.ts +2 -2
  415. package/src/adapters/node/files.test.ts +8 -4
  416. package/src/adapters/node/process.io.ts +13 -5
  417. package/src/adapters/node/process.test.ts +5 -5
  418. package/src/adapters/node/workspace.io.ts +9 -4
  419. package/src/adapters/node/workspace.test.ts +3 -1
  420. package/src/adapters/pdfjs/fill.test.ts +2 -2
  421. package/src/adapters/pdfjs/fill.ts +14 -7
  422. package/src/adapters/references/index.test.ts +31 -38
  423. package/src/adapters/references/index.ts +33 -60
  424. package/src/adapters/references/pool.ts +3 -5
  425. package/src/adapters/references/reach.io.ts +2 -1
  426. package/src/build-engine.harness.mjs +1 -1
  427. package/src/build-engine.test.ts +13 -8
  428. package/src/build-engine.ts +12 -7
  429. package/src/build.harness.mjs +2 -2
  430. package/src/build.test.ts +24 -24
  431. package/src/build.ts +34 -26
  432. package/src/caller-path.ts +13 -0
  433. package/src/cli.harness.mjs +6 -8
  434. package/src/cli.test.ts +31 -13
  435. package/src/cli.ts +117 -58
  436. package/src/doctor.harness.mjs +3 -3
  437. package/src/doctor.test.ts +1 -1
  438. package/src/doctor.ts +17 -7
  439. package/src/domain/lookup-cache.ts +21 -11
  440. package/src/domain/paths.test.ts +15 -0
  441. package/src/domain/paths.ts +20 -1
  442. package/src/domain/ready.ts +31 -0
  443. package/src/domain/record.test.ts +20 -0
  444. package/src/domain/record.ts +12 -0
  445. package/src/domain/result.test.ts +2 -2
  446. package/src/domain/sha256.ts +5 -3
  447. package/src/domain/text.test.ts +32 -0
  448. package/src/domain/text.ts +23 -0
  449. package/src/engine.harness.mjs +2 -2
  450. package/src/engine.ts +34 -5
  451. package/src/facts-file.test.ts +9 -7
  452. package/src/facts-file.ts +45 -29
  453. package/src/{hooks-settings.harness.mjs → hooks-settings.harness.ts} +56 -20
  454. package/src/hooks-settings.ts +48 -13
  455. package/src/init.test.ts +37 -14
  456. package/src/init.ts +48 -24
  457. package/src/{latex-log.harness.mjs → latex-log.harness.ts} +14 -14
  458. package/src/{latex-loop.harness.mjs → latex-loop.harness.ts} +64 -36
  459. package/src/latex-loop.ts +3 -3
  460. package/src/{link-skills.harness.mjs → link-skills.harness.ts} +19 -19
  461. package/src/link-skills.test.ts +3 -3
  462. package/src/link-skills.ts +5 -4
  463. package/src/lint-fix.test.ts +4 -6
  464. package/src/linted-set.test.ts +2 -2
  465. package/src/new-paper.harness.mjs +9 -25
  466. package/src/new-paper.test.ts +8 -10
  467. package/src/new-paper.ts +9 -5
  468. package/src/paper-files.test.ts +25 -24
  469. package/src/paper-files.ts +16 -27
  470. package/src/paper-settings-commands.test.ts +5 -4
  471. package/src/paper-settings.test.ts +7 -7
  472. package/src/paper-settings.ts +13 -14
  473. package/src/{pdf-facts.harness.mjs → pdf-facts.harness.ts} +9 -8
  474. package/src/pdf-facts.test.ts +25 -15
  475. package/src/pdf-facts.ts +81 -23
  476. package/src/{pdf-geometry.harness.mjs → pdf-geometry.harness.ts} +19 -12
  477. package/src/pdf-geometry.ts +8 -4
  478. package/src/ports/tool-installer.ts +2 -17
  479. package/src/presets.ts +31 -16
  480. package/src/references.test.ts +76 -62
  481. package/src/references.ts +18 -12
  482. package/src/{rules-config.harness.mjs → rules-config.harness.ts} +7 -13
  483. package/src/rules-config.ts +46 -14
  484. package/src/rules-off.test.ts +6 -4
  485. package/src/{structure.harness.mjs → structure.harness.ts} +26 -15
  486. package/src/structure.ts +1 -1
  487. package/src/{tex-requirements.harness.mjs → tex-requirements.harness.ts} +50 -22
  488. package/src/tex-requirements.test.ts +21 -0
  489. package/src/tex-requirements.ts +37 -11
  490. package/src/toolchain.harness.mjs +2 -2
  491. package/src/toolchain.test.ts +10 -6
  492. package/src/toolchain.ts +31 -17
  493. package/src/types.ts +5 -3
  494. package/src/venue-rules.test.ts +70 -68
  495. package/src/venue-rules.ts +22 -12
  496. package/tsconfig.json +6 -3
  497. package/tsconfig.pkg.json +10 -0
  498. package/lib/check.d.mts +0 -17
  499. package/lib/paper-config.d.mts +0 -31
  500. package/scripts/layer-legacy-frozen.d.mts +0 -36
  501. package/scripts/rules-are-content-only.harness.mjs +0 -105
  502. package/scripts/rules-are-content-only.mjs +0 -148
  503. package/scripts/rules-are-content-only.test.mjs +0 -61
  504. package/scripts/test.mjs +0 -44
  505. package/scripts/test.test.mjs +0 -52
  506. package/skills/paper-pipeline/scripts/announce.mjs +0 -67
  507. package/skills/paper-pipeline/scripts/announce.test.mjs +0 -102
  508. package/skills/paper-pipeline/scripts/fixtures/clean/PIPELINE-STATUS.md +0 -51
  509. package/skills/paper-pipeline/scripts/fixtures/dirty/PIPELINE-STATUS.md +0 -52
  510. package/skills/paper-pipeline/scripts/fixtures/dirty/paper.md +0 -6
  511. package/skills/paper-pipeline/scripts/fixtures/scorecard.mjs +0 -71
  512. package/skills/paper-pipeline/scripts/ledger.cli.test.mjs +0 -302
  513. package/skills/paper-pipeline/scripts/ledger.mjs +0 -623
  514. package/skills/paper-pipeline/scripts/ledger.test.mjs +0 -254
  515. package/skills/paper-pipeline/scripts/pipeline-check.cli.test.mjs +0 -474
  516. package/skills/paper-pipeline/scripts/pipeline-check.harness.mjs +0 -328
  517. package/skills/paper-pipeline/scripts/pipeline-check.mjs +0 -753
  518. package/skills/paper-pipeline/scripts/pipeline-edges.mjs +0 -169
  519. package/skills/paper-pipeline/scripts/pipeline-edges.test.mjs +0 -73
  520. package/skills/paper-pipeline/scripts/run-mechanical.mjs +0 -648
  521. package/skills/paper-pipeline/scripts/run-mechanical.test.mjs +0 -468
  522. package/skills/paper-pipeline/scripts/status.mjs +0 -295
  523. package/skills/paper-pipeline/scripts/status.test.mjs +0 -232
  524. /package/bin/{paperlint.test.mjs → paperlint.test.ts} +0 -0
  525. /package/lib/{skill-eval-fixture.mjs → skill-eval-fixture.ts} +0 -0
  526. /package/skills/grade-paper-writing/__snapshots__/{prose-lint.test.mjs.snap → prose-lint.test.ts.snap} +0 -0
  527. /package/skills/tighten-paper/__snapshots__/{structure.test.mjs.snap → structure.test.ts.snap} +0 -0
@@ -59,51 +59,87 @@
59
59
  * is REPORTED WITHOUT A FIX and the zero is offered as a suggestion: an autofix must never be the
60
60
  * one to decide what a number means. A real `.05` in any other paragraph is fixed as before.
61
61
  */
62
+ import assert from "node:assert/strict";
62
63
  import { getParser } from "@unified-latex/unified-latex-util-parse";
64
+ import type { Nodes } from "mdast";
65
+ import type { TexArg, TexNode, TexRoot } from "./latex-language.ts";
66
+ import type {
67
+ RuleContext,
68
+ RuleFixer,
69
+ RuleReport,
70
+ RuleSourceCode,
71
+ } from "./rule-context.ts";
72
+
73
+ /** Where a `.bib` embedded in a `filecontents` environment sits in the file. */
74
+ export interface BibRange {
75
+ readonly start: number;
76
+ readonly end: number;
77
+ readonly bodyStart: number;
78
+ readonly body: string;
79
+ }
80
+
81
+ /** The `.tex` language's source code: the one that carries the unprojected file. */
82
+ type TexSourceCode = RuleSourceCode & { readonly raw: string };
83
+
84
+ /** A run of visible text, and for each of its characters the offset it came from (or null). */
85
+ interface Run {
86
+ text: string;
87
+ offs: (number | null)[];
88
+ }
63
89
 
64
90
  /** The inline bibliography — a `filecontents` block writing a `.bib` — with its offsets. */
65
- export function bibRange(text) {
91
+ export function bibRange(text: string): BibRange | null {
66
92
  const m =
67
93
  /\\begin\{filecontents\*?\}(?:\[[^\]]*\])?\{[^}]*\.bib\}\r?\n([\s\S]*?)\\end\{filecontents\*?\}/d.exec(
68
94
  text,
69
95
  );
70
96
  if (!m) return null;
71
- return {
72
- start: m.index,
73
- end: m.index + m[0].length,
74
- bodyStart: m.indices[1][0],
75
- body: m[1],
76
- };
97
+ const [whole, body] = m;
98
+ const bodyStart = m.indices?.[1]?.[0];
99
+ // The `d` flag records every group's indices, and group 1 is not optional.
100
+ assert.ok(body !== undefined && bodyStart !== undefined, "no .bib body");
101
+ return { start: m.index, end: m.index + whole.length, bodyStart, body };
77
102
  }
78
103
 
79
104
  /**
80
105
  * The ranges a markup rule must not read: LaTeX comments (the language gives them as `html`
81
106
  * nodes, with positions) and the inline bibliography.
82
107
  */
83
- function skippedRanges(sourceCode, raw) {
84
- const out = [];
108
+ function skippedRanges(
109
+ sourceCode: RuleSourceCode,
110
+ raw: string,
111
+ ): [number, number][] {
112
+ const out: [number, number][] = [];
85
113
  for (const n of sourceCode.ast?.children ?? [])
86
- if (n.type === "html" && n.position)
87
- out.push([n.position.start.offset, n.position.end.offset]);
114
+ if (n.type === "html" && n.position) {
115
+ // An offset-less point compares as NaN: inside no range, as `undefined` always did.
116
+ const {
117
+ start: { offset: s = Number.NaN },
118
+ end: { offset: e = Number.NaN },
119
+ } = n.position;
120
+ out.push([s, e]);
121
+ }
88
122
  const bib = bibRange(raw);
89
123
  if (bib) out.push([bib.start, bib.end]);
90
124
  return out;
91
125
  }
92
- const inside = (ranges, i) => ranges.some(([s, e]) => i >= s && i < e);
126
+ const inside = (ranges: readonly [number, number][], i: number) =>
127
+ ranges.some(([s, e]) => i >= s && i < e);
93
128
 
94
129
  /** Every match of `re` in `raw` outside the skipped ranges. */
95
- function markupMatches(sourceCode, re) {
130
+ function markupMatches(sourceCode: TexSourceCode, re: RegExp) {
96
131
  const raw = sourceCode.raw;
97
132
  const skip = skippedRanges(sourceCode, raw);
98
133
  return [...raw.matchAll(re)].filter((m) => !inside(skip, m.index));
99
134
  }
100
135
 
101
- const isTex = (sourceCode) => typeof sourceCode.raw === "string";
136
+ const isTex = (sourceCode: RuleSourceCode): sourceCode is TexSourceCode =>
137
+ typeof sourceCode.raw === "string";
102
138
 
103
139
  // ── the text a READER sees, with the source offset of every character ─────────────────────
104
140
 
105
141
  /** A run of visible text; `offs[i]` is the source offset of `text[i]`, or null when unknown. */
106
- const run = () => ({ text: "", offs: [] });
142
+ const run = (): Run => ({ text: "", offs: [] });
107
143
 
108
144
  // Environments whose body is not typeset as text: code, drawings, the inline bibliography.
109
145
  const TEX_HIDDEN_ENV =
@@ -117,7 +153,7 @@ const TEX_LAST_ARG = /^(multicolumn|multirow|textcolor|href)$/;
117
153
  const TEX_MATH_HIDDEN =
118
154
  /^(label|ref|eqref|autoref|cite|hspace|vspace|hskip|vskip|kern|mkern|mskip|rule|phantom|hphantom|vphantom|color|raisebox|includegraphics)$/;
119
155
 
120
- const mandatory = (macro) =>
156
+ const mandatory = (macro: { readonly args?: readonly TexArg[] }) =>
121
157
  (macro.args || []).filter((a) => a.openMark === "{");
122
158
 
123
159
  /**
@@ -125,15 +161,15 @@ const mandatory = (macro) =>
125
161
  * arguments come back as the NEXT SIBLINGS: an optional `[…]` as bare strings, then groups.
126
162
  * They are parameters, not prose. Returns the index of the last sibling to skip after `i`.
127
163
  */
128
- function unparsedArgsEnd(nodes, i) {
164
+ /** Whether `n` is a string node whose text passes `test`. */
165
+ const isString = (n: TexNode | undefined, test: (s: string) => boolean) =>
166
+ n?.type === "string" && test(n.content);
167
+
168
+ function unparsedArgsEnd(nodes: readonly TexNode[], i: number): number {
129
169
  let j = i + 1;
130
- if (nodes[j]?.type === "string" && nodes[j].content.startsWith("[")) {
170
+ if (isString(nodes[j], (c) => c.startsWith("["))) {
131
171
  let k = j;
132
- while (
133
- k < nodes.length &&
134
- !(nodes[k].type === "string" && nodes[k].content.endsWith("]"))
135
- )
136
- k++;
172
+ while (k < nodes.length && !isString(nodes[k], (c) => c.endsWith("]"))) k++;
137
173
  if (k < nodes.length) j = k + 1;
138
174
  }
139
175
  while (nodes[j]?.type === "group") j++;
@@ -141,13 +177,22 @@ function unparsedArgsEnd(nodes, i) {
141
177
  }
142
178
 
143
179
  /** Append a string node to the current run, keeping each character's offset. */
144
- function appendString(cur, n, src) {
180
+ function appendString(
181
+ cur: Run,
182
+ n: {
183
+ readonly content: string;
184
+ readonly position?: { readonly start?: { readonly offset: number } };
185
+ },
186
+ src: string,
187
+ ) {
145
188
  const start = n.position?.start?.offset;
146
189
  const exact =
147
190
  typeof start === "number" &&
148
- src.slice(start, start + n.content.length) === n.content;
191
+ src.slice(start, start + n.content.length) === n.content
192
+ ? start
193
+ : null;
149
194
  for (let i = 0; i < n.content.length; i++)
150
- cur.offs.push(exact ? start + i : null);
195
+ cur.offs.push(exact === null ? null : exact + i);
151
196
  cur.text += n.content;
152
197
  }
153
198
 
@@ -156,14 +201,23 @@ function appendString(cur, n, src) {
156
201
  * `string` nodes with nothing between them: math splits `.05` into `.`, `0`, `5`, and prose
157
202
  * keeps `2310.05736` whole — both come back as one word. Anything else ends the run.
158
203
  */
159
- function texRuns(nodes, math, src, out) {
204
+ /** What stands in for an index past the end: a node every branch below passes over. */
205
+ const NOTHING: TexNode = { type: "whitespace" };
206
+
207
+ function texRuns(
208
+ nodes: readonly TexNode[] | undefined,
209
+ math: boolean,
210
+ src: string,
211
+ out: Run[],
212
+ ): Run[] {
160
213
  let cur = run();
161
214
  const flush = () => {
162
215
  if (cur.text) out.push(cur);
163
216
  cur = run();
164
217
  };
165
- for (let i = 0; i < (nodes || []).length; i++) {
166
- const n = nodes[i];
218
+ const list = nodes || [];
219
+ for (let i = 0; i < list.length; i++) {
220
+ const { [i]: n = NOTHING } = list;
167
221
  if (n.type === "string") {
168
222
  appendString(cur, n, src);
169
223
  continue;
@@ -192,9 +246,9 @@ function texRuns(nodes, math, src, out) {
192
246
  } else if (TEX_PROSE_ARG.test(n.content)) {
193
247
  for (const a of args) texRuns(a.content, false, src, out);
194
248
  } else if (TEX_LAST_ARG.test(n.content) && args.length) {
195
- texRuns(args[args.length - 1].content, false, src, out);
249
+ texRuns(args.at(-1)?.content, false, src, out);
196
250
  }
197
- if (!math && !(n.args || []).length) i = unparsedArgsEnd(nodes, i);
251
+ if (!math && !(n.args || []).length) i = unparsedArgsEnd(list, i);
198
252
  }
199
253
  }
200
254
  flush();
@@ -206,13 +260,16 @@ function texRuns(nodes, math, src, out) {
206
260
  * with the fields the walk tolerates being absent.
207
261
  */
208
262
  export function texVisibleRuns(
209
- raw,
210
- { parse = (s) => getParser().parse(s) } = {},
211
- ) {
263
+ raw: string,
264
+ {
265
+ parse = (s: string): TexRoot => getParser().parse(s),
266
+ }: { parse?: (s: string) => TexRoot } = {},
267
+ ): Run[] {
212
268
  const root = parse(raw).content;
213
269
  // Only the document body is typeset. A fragment with no `document` environment is all body.
214
270
  const document = root.find(
215
- (n) => n.type === "environment" && n.env === "document",
271
+ (n): n is Extract<TexNode, { type: "environment" | "mathenv" }> =>
272
+ n.type === "environment" && n.env === "document",
216
273
  );
217
274
  return texRuns(document ? document.content : root, false, raw, []);
218
275
  }
@@ -232,8 +289,8 @@ const MD_HIDDEN = new Set([
232
289
  * markdown's escapes (`\*`) and character references (`&lt;`) resolved, so the two are walked
233
290
  * side by side; a character whose source cannot be followed gets null (reported, not fixed).
234
291
  */
235
- function mdOffsets(value, src, start) {
236
- const offs = [];
292
+ function mdOffsets(value: string, src: string, start: number) {
293
+ const offs: (number | null)[] = [];
237
294
  let j = start;
238
295
  for (const ch of value) {
239
296
  if (src[j] === ch) offs.push(j++);
@@ -251,29 +308,39 @@ function mdOffsets(value, src, start) {
251
308
  return offs;
252
309
  }
253
310
 
254
- function mdVisibleRuns(node, src, out = []) {
311
+ function mdVisibleRuns(
312
+ node: Nodes | undefined,
313
+ src: string,
314
+ out: Run[] = [],
315
+ ): Run[] {
255
316
  if (!node || MD_HIDDEN.has(node.type)) return out;
256
317
  if (node.type === "text") {
257
- const start = node.position?.start?.offset;
318
+ const start = node.position?.start.offset;
258
319
  out.push({
259
320
  text: node.value,
260
321
  offs:
261
322
  typeof start === "number"
262
323
  ? mdOffsets(node.value, src, start)
263
- : [...node.value].map(() => null),
324
+ : Array.from(node.value, () => null),
264
325
  });
265
326
  }
266
- for (const c of node.children || []) mdVisibleRuns(c, src, out);
327
+ for (const c of "children" in node ? node.children : [])
328
+ mdVisibleRuns(c, src, out);
267
329
  return out;
268
330
  }
269
331
 
270
- const visibleRuns = (sourceCode) =>
332
+ const visibleRuns = (sourceCode: RuleSourceCode): Run[] =>
271
333
  isTex(sourceCode)
272
334
  ? texVisibleRuns(sourceCode.raw)
273
335
  : mdVisibleRuns(sourceCode.ast, sourceCode.text);
274
336
 
275
337
  /** A report at `[from, to)` of the file. */
276
- const at = (context, from, to, rest) => {
338
+ const at = (
339
+ context: RuleContext,
340
+ from: number,
341
+ to: number,
342
+ rest: Omit<RuleReport, "loc" | "node">,
343
+ ) => {
277
344
  const sc = context.sourceCode;
278
345
  context.report({
279
346
  loc: { start: sc.getLocFromIndex(from), end: sc.getLocFromIndex(to) },
@@ -288,14 +355,14 @@ const at = (context, from, to, rest) => {
288
355
  const SECTION_TEX = /(§|\\S(?![A-Za-z]))(~|[ \t]*)(?=(\\(?:auto)?ref\b)|(\d))/g;
289
356
  const SECTION_GLYPH = /§/g;
290
357
 
291
- function sectionWord(context) {
358
+ function sectionWord(context: RuleContext) {
292
359
  const sc = context.sourceCode;
293
360
  if (isTex(sc)) {
294
361
  const seen = new Set();
295
362
  for (const m of markupMatches(sc, SECTION_TEX)) {
296
363
  seen.add(m.index);
297
364
  const word = m[3] ? "Section~" : "Section ";
298
- const range = [m.index, m.index + m[0].length];
365
+ const range: [number, number] = [m.index, m.index + m[0].length];
299
366
  at(context, range[0], range[1], {
300
367
  messageId: "sign",
301
368
  fix: (f) => f.replaceTextRange(range, word),
@@ -313,7 +380,8 @@ function sectionWord(context) {
313
380
  const from = r.offs[m.index];
314
381
  if (from === null || from === undefined) continue;
315
382
  // The sign and the space after it; the number stays.
316
- const range = [from, from + 1 + m[1].length];
383
+ const [, gap = ""] = m;
384
+ const range: [number, number] = [from, from + 1 + gap.length];
317
385
  at(context, range[0], range[1], {
318
386
  messageId: "sign",
319
387
  ...(m[2] ? { fix: (f) => f.replaceTextRange(range, "Section ") } : {}),
@@ -334,13 +402,13 @@ const DESIGNATOR_BEFORE = /(?:[¶§]|\\[PS](?![A-Za-z]))[ \t~]*$/;
334
402
  const DESIGNATED = /(?:[¶§]|\\[PS](?![A-Za-z]))[ \t~]*\.\d/;
335
403
 
336
404
  /** The source of the paragraph holding `i`, up to `i`: from the last blank line before it. */
337
- const paragraphBefore = (src, i) => {
405
+ const paragraphBefore = (src: string, i: number) => {
338
406
  const head = src.slice(0, i);
339
407
  const blank = [...head.matchAll(/\n[ \t]*\n/g)].at(-1);
340
408
  return head.slice(blank ? blank.index + blank[0].length : 0);
341
409
  };
342
410
 
343
- function leadingZero(context) {
411
+ function leadingZero(context: RuleContext) {
344
412
  const sc = context.sourceCode;
345
413
  const src = isTex(sc) ? sc.raw : sc.text;
346
414
  for (const r of visibleRuns(sc))
@@ -349,7 +417,7 @@ function leadingZero(context) {
349
417
  if (dot === null || dot === undefined) continue;
350
418
  const before = paragraphBefore(src, dot);
351
419
  if (DESIGNATOR_BEFORE.test(before)) continue;
352
- const zero = (f) => f.insertTextBeforeRange([dot, dot], "0");
420
+ const zero = (f: RuleFixer) => f.insertTextBeforeRange([dot, dot], "0");
353
421
  at(
354
422
  context,
355
423
  dot,
@@ -371,7 +439,7 @@ function leadingZero(context) {
371
439
 
372
440
  const FIG_REF = /\b(Fig\.|Figure)(?=~?\\(?:ref|autoref)\b)/g;
373
441
 
374
- function figureRefStyle(context) {
442
+ function figureRefStyle(context: RuleContext) {
375
443
  const sc = context.sourceCode;
376
444
  if (!isTex(sc)) return;
377
445
  const all = markupMatches(sc, FIG_REF);
@@ -382,25 +450,35 @@ function figureRefStyle(context) {
382
450
  const [minority, word] =
383
451
  short.length > long.length ? [long, "Fig."] : [short, "Figure"];
384
452
  for (const m of minority) {
385
- const range = [m.index, m.index + m[1].length];
453
+ const [, form = ""] = m;
454
+ const range: [number, number] = [m.index, m.index + form.length];
386
455
  at(context, range[0], range[1], {
387
456
  messageId: "mixed",
388
- data: { form: m[1], word, n: String(all.length - minority.length) },
457
+ data: { form, word, n: String(all.length - minority.length) },
389
458
  fix: (f) => f.replaceTextRange(range, word),
390
459
  });
391
460
  }
392
461
  }
393
462
 
394
- const rule = (description, messages, check, meta = {}) => ({
463
+ const rule = (
464
+ description: string,
465
+ messages: Readonly<Record<string, string>>,
466
+ check: (context: RuleContext) => void,
467
+ meta: { readonly hasSuggestions?: boolean } = {},
468
+ ) => ({
395
469
  meta: {
396
- type: "suggestion",
397
- fixable: "code",
470
+ type: "suggestion" as const,
471
+ fixable: "code" as const,
398
472
  docs: { description },
399
473
  schema: [],
400
474
  messages,
401
475
  ...meta,
402
476
  },
403
- create: (context) => ({ "root:exit": () => check(context) }),
477
+ create: (context: RuleContext) => ({
478
+ "root:exit": () => {
479
+ check(context);
480
+ },
481
+ }),
404
482
  });
405
483
 
406
484
  export default {
@@ -17,7 +17,7 @@
17
17
  * not throw" as success; an exported `tests` object is run by nothing.
18
18
  */
19
19
  import assert from "node:assert/strict";
20
- import { PAPERS_DIR_FIELD } from "../lib/paper-config.mjs";
20
+ import { PAPERS_DIR_FIELD } from "../lib/paper-config.ts";
21
21
  import {
22
22
  mkdirSync,
23
23
  mkdtempSync,
@@ -31,7 +31,7 @@ import { tmpdir } from "node:os";
31
31
  import { dirname, isAbsolute, join } from "node:path";
32
32
  import { fileURLToPath } from "node:url";
33
33
 
34
- import { DEFAULT_PAPERS_ROOT, paperFiles, papersRoot } from "./papers.mjs";
34
+ import { DEFAULT_PAPERS_ROOT, paperFiles, papersRoot } from "./papers.ts";
35
35
 
36
36
  const HERE = dirname(fileURLToPath(import.meta.url));
37
37
  const ROOT = join(HERE, "..");
@@ -82,7 +82,7 @@ import {
82
82
  DEFAULT_PAPERS_ROOT,
83
83
  PAPERS_DIR_FIELD,
84
84
  CONFIG_FILE,
85
- } from "../lib/paper-config.mjs";
85
+ } from "../lib/paper-config.ts";
86
86
  export { DEFAULT_PAPERS_ROOT };
87
87
 
88
88
  /**
@@ -98,7 +98,10 @@ export { DEFAULT_PAPERS_ROOT };
98
98
  * globs, which ESLint interprets relative to the config, and an absolute path there
99
99
  * would change their meaning.
100
100
  */
101
- export function papersRoot(settings, baseDir = process.cwd()) {
101
+ export function papersRoot(
102
+ settings: Readonly<Record<string, unknown>> | null | undefined,
103
+ baseDir: string = process.cwd(),
104
+ ): string {
102
105
  const declared = settings?.[PAPERS_DIR_FIELD];
103
106
  // 🔴 `declared === undefined`, NOT `declared ?? DEFAULT`. The two differ on exactly one
104
107
  // input — `"papersDir": null` — and the difference is the whole point: `??` reads an explicit
@@ -118,7 +121,7 @@ export function papersRoot(settings, baseDir = process.cwd()) {
118
121
  ? `Nothing was declared, so the default "${DEFAULT_PAPERS_ROOT}" was used. Declare the ` +
119
122
  `real location in ${CONFIG_FILE}:\n` +
120
123
  ` { "${PAPERS_DIR_FIELD}": "path/to/papers" }`
121
- : `It is declared in ${CONFIG_FILE} as "${declared}". Fix it there, or create the ` +
124
+ : `It is declared in ${CONFIG_FILE} as "${root}". Fix it there, or create the ` +
122
125
  `directory.`) +
123
126
  `\nThis is thrown rather than ignored on purpose: a papers root that matches nothing ` +
124
127
  `makes every rule lint zero files, and a run with zero findings is indistinguishable ` +
@@ -136,7 +139,7 @@ export function papersRoot(settings, baseDir = process.cwd()) {
136
139
  * with the rule, in the same package, rather than as a line every consumer has to add by hand
137
140
  * to keep up. A hand-maintained list of paths is the thing that rots, and rots silently.
138
141
  */
139
- export function paperFiles(root) {
142
+ export function paperFiles(root: string) {
140
143
  return {
141
144
  /** Markdown drafts — the two names a paper's prose is allowed to have. */
142
145
  md: [`${root}/*/paper.md`, `${root}/*/draft.md`],
@@ -23,12 +23,12 @@ import { tmpdir } from "node:os";
23
23
  import { dirname, join } from "node:path";
24
24
  import { fileURLToPath } from "node:url";
25
25
  import { ESLint } from "eslint";
26
- import { texLanguage } from "./latex-language.mjs";
27
- import { createChecker } from "../lib/check.mjs";
26
+ import { texLanguage } from "./latex-language.ts";
27
+ import { createChecker } from "../lib/check.ts";
28
28
 
29
29
  const HERE = dirname(fileURLToPath(import.meta.url));
30
30
  const { parseFacts, judgeColumns, DEFAULT_TOLERANCE_PT } = await import(
31
- join(HERE, "pdf-last-page-balance.mjs")
31
+ join(HERE, "pdf-last-page-balance.ts")
32
32
  );
33
33
  const { buildConfig } = await import(join(HERE, "..", "src", "cli.ts"));
34
34
 
@@ -35,7 +35,58 @@
35
35
  import { createHash } from "node:crypto";
36
36
  import { existsSync, readFileSync } from "node:fs";
37
37
  import { basename, dirname, isAbsolute, join } from "node:path";
38
- import { CONFIG_FILE } from "../lib/paper-config.mjs";
38
+ import assert from "node:assert/strict";
39
+ import { z } from "zod";
40
+ import { CONFIG_FILE } from "../lib/paper-config.ts";
41
+ import type { RuleContext } from "./rule-context.ts";
42
+
43
+ /** A reason to report, in ESLint's terms. */
44
+ export interface Finding {
45
+ readonly messageId: string;
46
+ readonly data: Readonly<Record<string, string>>;
47
+ }
48
+
49
+ /** The `last_page` field, as far as this rule can judge it. */
50
+ export type LastPage =
51
+ | { readonly kind: "stub" | "review" }
52
+ | {
53
+ readonly kind: "measured";
54
+ readonly left: number;
55
+ readonly right: number;
56
+ };
57
+
58
+ /** What `parseFacts` reads out of `paper.facts.json`, or why it cannot. */
59
+ export type ParsedFacts =
60
+ | {
61
+ readonly ok: true;
62
+ readonly pdf: string;
63
+ /** Whatever the file holds there; it matched the digest pattern, and is compared as-is. */
64
+ readonly sha: unknown;
65
+ readonly last: LastPage;
66
+ }
67
+ | ({ readonly ok: false } & Finding);
68
+
69
+ // The fields read, each as it is written; anything that is not a mapping has none of them.
70
+ const FactsFields = z
71
+ .looseObject({
72
+ schema: z.unknown().optional(),
73
+ pdf: z.unknown().optional(),
74
+ pdf_sha256: z.unknown().optional(),
75
+ last_page: z.unknown().optional(),
76
+ })
77
+ .catch({});
78
+ const LastPageFields = z
79
+ .looseObject({
80
+ kind: z.unknown().optional(),
81
+ columns_pt: z.unknown().optional(),
82
+ })
83
+ .catch({});
84
+ /** Two column heights: finite numbers, exactly two of them. */
85
+ const Columns = z.tuple([z.number(), z.number()]);
86
+ const Settings = z.looseObject({ extends: z.unknown().optional() }).catch({});
87
+ const RuleOptions = z
88
+ .object({ tolerancePt: z.number().optional() })
89
+ .default({});
39
90
 
40
91
  /** The difference, in points, that two columns may end apart. See the harness for why 120. */
41
92
  export const DEFAULT_TOLERANCE_PT = 120;
@@ -43,44 +94,40 @@ export const DEFAULT_TOLERANCE_PT = 120;
43
94
  export const FACTS_SCHEMA = 2;
44
95
  export const FACTS_REL = join("_build", "paper.facts.json");
45
96
 
46
- const isHeight = (x) => typeof x === "number" && Number.isFinite(x);
47
-
48
97
  /** The `last_page` field, parsed. */
49
- function lastPageOf(v) {
50
- if (v?.kind === "stub" || v?.kind === "review") return { kind: v.kind };
51
- const c = v?.columns_pt;
52
- if (
53
- v?.kind === "measured" &&
54
- Array.isArray(c) &&
55
- c.length === 2 &&
56
- c.every(isHeight)
57
- )
58
- return { kind: "measured", left: c[0], right: c[1] };
98
+ function lastPageOf(v: unknown): LastPage | null {
99
+ const { kind, columns_pt } = LastPageFields.parse(v);
100
+ if (kind === "stub" || kind === "review") return { kind };
101
+ const c = Columns.safeParse(columns_pt);
102
+ if (kind === "measured" && c.success)
103
+ return { kind: "measured", left: c.data[0], right: c.data[1] };
59
104
  return null;
60
105
  }
61
106
 
62
107
  /**
63
108
  * The facts file's text → what this rule needs, or the reason it cannot be judged. Pure.
64
- *
65
- * @returns {{ ok: true, pdf: string, sha: string, last: object } | { ok: false, messageId: string, data: object }}
66
109
  */
67
- export function parseFacts(text) {
68
- let d;
110
+ export function parseFacts(text: string): ParsedFacts {
111
+ let raw: unknown;
69
112
  try {
70
- d = JSON.parse(text);
113
+ raw = JSON.parse(text);
71
114
  } catch (e) {
115
+ // `JSON.parse` throws a `SyntaxError` and nothing else.
116
+ assert.ok(e instanceof Error);
72
117
  return { ok: false, messageId: "factsBroken", data: { why: e.message } };
73
118
  }
74
- if (d?.schema !== FACTS_SCHEMA)
119
+ const d = FactsFields.parse(raw);
120
+ if (d.schema !== FACTS_SCHEMA)
75
121
  return {
76
122
  ok: false,
77
123
  messageId: "schema",
78
- data: { got: JSON.stringify(d?.schema ?? null) },
124
+ data: { got: JSON.stringify(d.schema ?? null) },
79
125
  };
80
126
  const last = lastPageOf(d.last_page);
81
127
  if (
82
128
  typeof d.pdf !== "string" ||
83
- !/^[0-9a-f]{64}$/.test(d.pdf_sha256 ?? "") ||
129
+ typeof d.pdf_sha256 !== "string" ||
130
+ !/^[0-9a-f]{64}$/.test(d.pdf_sha256) ||
84
131
  !last
85
132
  )
86
133
  return {
@@ -94,9 +141,12 @@ export function parseFacts(text) {
94
141
  }
95
142
 
96
143
  /** Whether the facts describe the PDF on disk: null when they do, else the reason. */
97
- function staleness(paperDir, facts) {
144
+ function staleness(
145
+ paperDir: string,
146
+ facts: { readonly pdf: string; readonly sha: unknown },
147
+ ): Finding | null {
98
148
  const pdf = isAbsolute(facts.pdf) ? facts.pdf : join(paperDir, facts.pdf);
99
- let got;
149
+ let got: string;
100
150
  try {
101
151
  got = createHash("sha256").update(readFileSync(pdf)).digest("hex");
102
152
  } catch {
@@ -108,7 +158,10 @@ function staleness(paperDir, facts) {
108
158
  }
109
159
 
110
160
  /** The finding for a measured page, or null when it is balanced. Pure. */
111
- export function judgeColumns(last, tolerancePt) {
161
+ export function judgeColumns(
162
+ last: LastPage,
163
+ tolerancePt: number,
164
+ ): Finding | null {
112
165
  if (last.kind !== "measured") return null;
113
166
  const diff = Math.abs(last.left - last.right);
114
167
  if (diff <= tolerancePt) return null;
@@ -124,7 +177,7 @@ export function judgeColumns(last, tolerancePt) {
124
177
  }
125
178
 
126
179
  /** Where a finding goes: the `\documentclass` line, where the class options that fix it live. */
127
- function reportLine(file) {
180
+ function reportLine(file: string): number {
128
181
  try {
129
182
  const lines = readFileSync(file, "utf8").split("\n");
130
183
  const i = lines.findIndex((l) => /^\s*\\documentclass\b/.test(l));
@@ -140,17 +193,19 @@ function reportLine(file) {
140
193
  * repeating it as an error would fail a lint-only CI for every paper of a venue whose preset turns
141
194
  * this rule on.
142
195
  */
143
- function extendsPreset(paperDir) {
196
+ function extendsPreset(paperDir: string): boolean {
144
197
  try {
145
- const s = JSON.parse(readFileSync(join(paperDir, CONFIG_FILE), "utf8"));
146
- return typeof s?.extends === "string" && s.extends !== "";
198
+ const s = Settings.parse(
199
+ JSON.parse(readFileSync(join(paperDir, CONFIG_FILE), "utf8")),
200
+ );
201
+ return typeof s.extends === "string" && s.extends !== "";
147
202
  } catch {
148
203
  return false;
149
204
  }
150
205
  }
151
206
 
152
207
  /** Everything the rule decides for one paper, as a finding or null. */
153
- function verdict(paperDir, tolerancePt) {
208
+ function verdict(paperDir: string, tolerancePt: number): Finding | null {
154
209
  const factsFile = join(paperDir, FACTS_REL);
155
210
  if (!existsSync(factsFile))
156
211
  return extendsPreset(paperDir)
@@ -163,7 +218,7 @@ function verdict(paperDir, tolerancePt) {
163
218
 
164
219
  const rule = {
165
220
  meta: {
166
- type: "problem",
221
+ type: "problem" as const,
167
222
  docs: {
168
223
  description:
169
224
  "the last page's two columns end at about the same height (optional; for venues that require it)",
@@ -204,10 +259,12 @@ const rule = {
204
259
  "differs), so judging it would judge an earlier build — rebuild the paper",
205
260
  },
206
261
  },
207
- create(context) {
262
+ create(context: RuleContext) {
208
263
  // Judged once per paper, on its paper.tex: the paper directory is the file's directory.
209
264
  if (basename(context.filename) !== "paper.tex") return {};
210
- const tolerancePt = context.options[0]?.tolerancePt ?? DEFAULT_TOLERANCE_PT;
265
+ const { tolerancePt = DEFAULT_TOLERANCE_PT } = RuleOptions.parse(
266
+ context.options[0],
267
+ );
211
268
  return {
212
269
  root() {
213
270
  const v = verdict(dirname(context.filename), tolerancePt);
@@ -5,8 +5,8 @@
5
5
  import { describe, expect, it } from "vitest";
6
6
  import { ESLint } from "eslint";
7
7
  import markdown from "@eslint/markdown";
8
- import review, { errorsOf } from "./review-frontmatter.mjs";
9
- import sibling from "./sibling-frontmatter.mjs";
8
+ import review, { errorsOf } from "./review-frontmatter.ts";
9
+ import sibling from "./sibling-frontmatter.ts";
10
10
 
11
11
  async function lint(text) {
12
12
  const eslint = new ESLint({