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
package/src/cli.ts CHANGED
@@ -30,7 +30,8 @@ import { spawnSync } from "node:child_process";
30
30
  import { fileURLToPath } from "node:url";
31
31
  import { join, dirname, resolve, relative, basename, sep } from "node:path";
32
32
  import markdown from "@eslint/markdown";
33
- // Types come from consumer.d.mts beside it, the same arrangement as lib/paper-config.d.mts.
33
+ import { z } from "zod";
34
+ // Types come from consumer.d.mts beside it.
34
35
  import {
35
36
  isMain,
36
37
  packageVenuesDir,
@@ -96,43 +97,39 @@ import {
96
97
  type PaperFormat,
97
98
  type VenueSetting,
98
99
  } from "./new-paper.ts";
99
- // The one source for the consumer's config key lives in the .mjs half of the package (the ESLint
100
- // rules and the skill scripts import it too); its types are in lib/paper-config.d.mts.
100
+ // The one source for the consumer's config key lives in lib/ (the ESLint rules and the skill scripts
101
+ // import it too). It is imported compiled, from dist/, the one path that resolves the same from src/
102
+ // and from dist/ — see CONTRIBUTING.md.
101
103
  import {
102
104
  CONFIG_FILE,
103
105
  DEFAULT_PAPERS_ROOT,
104
106
  PAPERS_DIR_FIELD,
105
107
  SETTINGS_KEYS,
106
108
  findProjectRoot,
107
- } from "../lib/paper-config.mjs";
109
+ } from "#lib/paper-config";
108
110
  import {
109
111
  parseRuleBlocks,
110
112
  parseRuleEntries,
111
113
  shippedRuleIds,
112
114
  unknownKeys,
113
115
  type Parsed,
116
+ type ConfigBlock,
114
117
  type RuleBlock,
115
118
  type RuleEntry,
116
119
  } from "./rules-config.ts";
117
120
  export { init };
118
121
  export { nextSteps } from "./init.ts";
119
122
 
120
- // @ts-expect-error — an ESLint rule in .mjs, it has no types
121
- import paperStages from "../eslint-rules/paper-stages.mjs";
122
- // @ts-expect-error — an ESLint rule in .mjs, it has no types
123
- import researchQuestion from "../eslint-rules/paper-research-question.mjs";
124
- // @ts-expect-error — an ESLint rule in .mjs, it has no types
125
- import typography from "../eslint-rules/paper-typography.mjs";
126
- // @ts-expect-error — an ESLint rule in .mjs, it has no types
127
- import texBuild from "../eslint-rules/tex-build.mjs";
128
- // @ts-expect-error — an ESLint rule in .mjs, it has no types
129
- import bibReachable from "../eslint-rules/bib-reachable-entry.mjs";
130
- // @ts-expect-error — an ESLint rule in .mjs, it has no types
131
- import reviewFrontmatter from "../eslint-rules/review-frontmatter.mjs";
132
- // @ts-expect-error — an ESLint rule in .mjs, it has no types
133
- import siblingFrontmatter from "../eslint-rules/sibling-frontmatter.mjs";
134
- // @ts-expect-error — an ESLint rule in .mjs, it has no types
135
- import pdfRules from "../eslint-rules/pdf-last-page-balance.mjs";
123
+ import paperStages from "#eslint-rules/paper-stages";
124
+ import researchQuestion from "#eslint-rules/paper-research-question";
125
+ import typography from "#eslint-rules/paper-typography";
126
+ import texBuild from "#eslint-rules/tex-build";
127
+ import bibReachable from "#eslint-rules/bib-reachable-entry";
128
+ import reviewFrontmatter from "#eslint-rules/review-frontmatter";
129
+ import siblingFrontmatter from "#eslint-rules/sibling-frontmatter";
130
+ import pdfRules from "#eslint-rules/pdf-last-page-balance";
131
+ import { messageOf } from "./domain/text.ts";
132
+ import { isRecord } from "./domain/record.ts";
136
133
 
137
134
  /** The shipped venue presets, read from the package's venues directory — the one list. */
138
135
  const SHIPPED_VENUES = (): string[] => shippedPresets(packageVenuesDir());
@@ -226,7 +223,7 @@ settings — paperlint.json, at two levels, one schema. Both are optional.
226
223
  export function buildConfig(
227
224
  opts: PaperlintConfig = {},
228
225
  texLanguage: unknown,
229
- ): unknown[] {
226
+ ): ConfigBlock[] {
230
227
  const paperRules = { ...researchQuestion.rules, ...typography.rules };
231
228
  // The reference rules judge `_build/references.json`, and only on `paper.tex`.
232
229
  const texPaperRules = {
@@ -244,8 +241,7 @@ export function buildConfig(
244
241
  languageOptions: { frontmatter: "yaml" },
245
242
  };
246
243
 
247
- // eslint-disable-next-line @typescript-eslint/no-explicit-any -- #49: replace with a real type
248
- const cfg: any[] = [
244
+ const cfg: ConfigBlock[] = [
249
245
  // 🔴 THE PROJECT'S PAPER TEMPLATE IS NOT A PAPER. `paperlint new` reads `<papers>/.template/`, and
250
246
  // its files carry every marker a paper does. Flat config does NOT ignore dot-directories by
251
247
  // default (only `node_modules/` and `.git/`), so without this block `paperlint lint` would lint the
@@ -336,6 +332,21 @@ export function buildConfig(
336
332
  return cfg;
337
333
  }
338
334
 
335
+ /**
336
+ * paperlint's blocks as ESLint's config type — the one place the two meet. ESLint types a plugin's
337
+ * rule as one that runs on ANY language; paperlint's run on the language they were written for, so
338
+ * no static proof that a block fits exists (`ConfigBlock`, rules-config.ts). ESLint checks every block
339
+ * when it loads the config and throws on one it cannot use; what is left to check here is that each
340
+ * is an object.
341
+ */
342
+ const isEslintBlock = (b: unknown): b is Linter.Config =>
343
+ typeof b === "object" && b !== null;
344
+
345
+ export function eslintConfig(blocks: readonly ConfigBlock[]): Linter.Config[] {
346
+ const loose: readonly unknown[] = blocks;
347
+ return loose.filter(isEslintBlock);
348
+ }
349
+
339
350
  /**
340
351
  * The rule ids a consumer may name in `rules`: every rule paperlint's own config defines, read off that
341
352
  * config rather than listed again. `@eslint/markdown` is a dependency's plugin, not paperlint's.
@@ -364,9 +375,7 @@ export interface RulePlugin {
364
375
  export function rulePlugins(texLanguage?: unknown): Record<string, RulePlugin> {
365
376
  const out: Record<string, { rules: Record<string, unknown> }> = {};
366
377
  for (const block of buildConfig({}, texLanguage ?? { sentinel: "tex" }))
367
- for (const [name, plugin] of Object.entries(
368
- (block as { plugins?: Record<string, RulePlugin> }).plugins ?? {},
369
- ))
378
+ for (const [name, plugin] of Object.entries(block.plugins ?? {}))
370
379
  if (plugin !== markdown)
371
380
  out[name] = { rules: { ...out[name]?.rules, ...plugin.rules } };
372
381
  if (out["tex"] && texLanguage !== undefined)
@@ -389,10 +398,10 @@ export function rulePlugins(texLanguage?: unknown): Record<string, RulePlugin> {
389
398
  * switched off in THIS run — `paperlint lint` still reports a directive that silences nothing.
390
399
  */
391
400
  export function rulesOff(texLanguage?: unknown): Linter.Config[] {
392
- return [
401
+ return eslintConfig([
393
402
  {
394
403
  name: "paperlint/rules-off",
395
- plugins: rulePlugins(texLanguage) as Linter.Config["plugins"],
404
+ plugins: rulePlugins(texLanguage),
396
405
  rules: Object.fromEntries([...SHIPPED_RULES].map((id) => [id, "off"])),
397
406
  },
398
407
  {
@@ -400,12 +409,12 @@ export function rulesOff(texLanguage?: unknown): Linter.Config[] {
400
409
  files: PAPER_FILE_PATTERNS,
401
410
  linterOptions: { reportUnusedDisableDirectives: "off" },
402
411
  },
403
- ];
412
+ ]);
404
413
  }
405
414
 
406
415
  /** Whether a rule entry (`"error"`, `2`, `["warn", {…}]`) turns the rule on. */
407
416
  const isOn = (entry: unknown): boolean => {
408
- const sev = Array.isArray(entry) ? entry[0] : entry;
417
+ const sev: unknown = Array.isArray(entry) ? entry[0] : entry;
409
418
  return sev !== undefined && sev !== "off" && sev !== 0;
410
419
  };
411
420
 
@@ -417,7 +426,7 @@ export const OPTIONAL_RULES: ReadonlySet<string> = new Set(
417
426
  [...SHIPPED_RULES].filter(
418
427
  (id) =>
419
428
  !buildConfig({}, { sentinel: "tex language" }).some((b) =>
420
- isOn((b as { rules?: Record<string, unknown> }).rules?.[id]),
429
+ isOn(b.rules?.[id]),
421
430
  ),
422
431
  ),
423
432
  );
@@ -434,8 +443,13 @@ export const OPTIONAL_RULES: ReadonlySet<string> = new Set(
434
443
  * papers a and b (#103) — a block that reaches a paper outside this run is not dead. A glob that
435
444
  * reaches no paper of the project at all still fails, from a subset as from the whole.
436
445
  */
446
+ /** What `calculateConfigForFile` answers, read for its rules: undefined for a file outside ESLint's cwd or scope. */
447
+ const ComputedConfig = z
448
+ .object({ rules: z.record(z.string(), z.unknown()).optional() })
449
+ .optional();
450
+
437
451
  export async function silentOptionalRules(
438
- eslint: ESLint,
452
+ eslint: Pick<ESLint, "calculateConfigForFile">,
439
453
  files: readonly string[],
440
454
  opts: PaperlintConfig,
441
455
  ): Promise<string[]> {
@@ -449,8 +463,7 @@ export async function silentOptionalRules(
449
463
  const reached = new Set<string>();
450
464
  for (const f of new Set(files.filter((p) => basename(p) === MAIN))) {
451
465
  // Undefined for a file outside ESLint's cwd or scope: it reaches nothing.
452
- const cfg = (await eslint.calculateConfigForFile(f)) as
453
- { rules?: Record<string, unknown> } | undefined;
466
+ const cfg = ComputedConfig.parse(await eslint.calculateConfigForFile(f));
454
467
  for (const id of turnedOn) if (isOn(cfg?.rules?.[id])) reached.add(id);
455
468
  }
456
469
  return [...turnedOn].filter((id) => !reached.has(id));
@@ -470,7 +483,10 @@ export function paperRuleBlocks(
470
483
  existsSync(p) && statSync(p).isFile() ? dirname(p) : p,
471
484
  );
472
485
  const papers = [...new Set(dirs.flatMap((p) => [p, ...papersIn(p)]))];
473
- const out = { preset: [] as RuleBlock[], own: [] as RuleBlock[] };
486
+ const out: { preset: RuleBlock[]; own: RuleBlock[] } = {
487
+ preset: [],
488
+ own: [],
489
+ };
474
490
  for (const dir of papers) {
475
491
  const p = paperPreset(dir, PRESET_DEPS);
476
492
  if (p.kind === "settings-problem")
@@ -545,6 +561,20 @@ const PAPER_FILE_PATTERNS: string[] = ownedPatterns(
545
561
  buildConfig({}, { sentinel: "tex language" }),
546
562
  );
547
563
 
564
+ /** `structure` as `checkStructure` reads it: off, or lists of file names over the defaults. */
565
+ const Names = z.array(z.string()).exactOptional();
566
+ const Structure = z
567
+ .union([
568
+ z.literal(false),
569
+ z.object({
570
+ markers: Names,
571
+ require: Names,
572
+ requireOneOf: z.array(z.array(z.string())).exactOptional(),
573
+ ignore: Names,
574
+ }),
575
+ ])
576
+ .optional();
577
+
548
578
  /**
549
579
  * The root `paperlint.json` after the boundary: an unknown key is refused by name, the paper
550
580
  * defaults (`extends`, `kind`, `pdf`) are checked the way a paper's own are, and `rules` becomes
@@ -555,10 +585,9 @@ export function parseSettings(
555
585
  where: string,
556
586
  baseDir: string,
557
587
  ): Parsed<PaperlintConfig> {
558
- if (typeof json !== "object" || json === null || Array.isArray(json))
588
+ if (!isRecord(json))
559
589
  return { ok: false, error: `${where}: must be a JSON object` };
560
- const raw = json as Record<string, unknown>;
561
- const opts = raw as PaperlintConfig;
590
+ const raw = json;
562
591
  const unknown = unknownKeys(raw);
563
592
  if (unknown.length > 0)
564
593
  return {
@@ -569,9 +598,22 @@ export function parseSettings(
569
598
  };
570
599
  const defaults = stringFields(raw);
571
600
  if (!defaults.ok) return { ok: false, error: `${where}: ${defaults.error}` };
601
+ const structure = Structure.safeParse(raw["structure"]);
602
+ if (!structure.success)
603
+ return {
604
+ ok: false,
605
+ error: `${where}: "structure" must be false, or an object whose markers, require and ignore are lists of file names and requireOneOf a list of such lists`,
606
+ };
572
607
  const rules = parseRuleBlocks(raw["rules"], where, SHIPPED_RULES, baseDir);
573
608
  if (!rules.ok) return rules;
574
- return { ok: true, value: { ...opts, rules: rules.value } };
609
+ return {
610
+ ok: true,
611
+ value: {
612
+ papersDir: raw[PAPERS_DIR_FIELD],
613
+ structure: structure.data,
614
+ rules: rules.value,
615
+ },
616
+ };
575
617
  }
576
618
 
577
619
  /**
@@ -701,8 +743,13 @@ export function ownVersion(
701
743
  readFileSync(new URL("../package.json", import.meta.url), "utf8"),
702
744
  ): string | undefined {
703
745
  try {
704
- const v = JSON.parse(readManifest())?.version;
705
- return typeof v === "string" ? v : undefined;
746
+ const manifest: unknown = JSON.parse(readManifest());
747
+ return typeof manifest === "object" &&
748
+ manifest !== null &&
749
+ "version" in manifest &&
750
+ typeof manifest.version === "string"
751
+ ? manifest.version
752
+ : undefined;
706
753
  } catch {
707
754
  return undefined;
708
755
  }
@@ -747,7 +794,7 @@ export function readConfig(
747
794
  try {
748
795
  parsed = JSON.parse(readFileSync(configPath, "utf8"));
749
796
  } catch (e) {
750
- err(`${configPath} is not valid JSON: ${(e as Error).message}`);
797
+ err(`${configPath} is not valid JSON: ${messageOf(e)}`);
751
798
  return { code: 2 };
752
799
  }
753
800
  // The discovered config is NAMED out loud. Otherwise a run from someone else's directory picks
@@ -767,7 +814,7 @@ export function readConfig(
767
814
 
768
815
  /** The papers directory field of the settings, read by its one declared name; the default when absent. */
769
816
  export function papersDirOf(opts: PaperlintConfig): unknown {
770
- const declared = (opts as Record<string, unknown>)[PAPERS_DIR_FIELD];
817
+ const declared = opts.papersDir;
771
818
  return declared === undefined ? DEFAULT_PAPERS_ROOT : declared;
772
819
  }
773
820
 
@@ -787,7 +834,9 @@ export const papersRoots = (cfg: {
787
834
  export function toPaths(papers: unknown): string[] {
788
835
  if (typeof papers === "string") return papers.trim() ? [papers.trim()] : [];
789
836
  if (Array.isArray(papers))
790
- return papers.filter((x) => typeof x === "string" && x.trim());
837
+ return papers.filter(
838
+ (x): x is string => typeof x === "string" && x.trim() !== "",
839
+ );
791
840
  return [];
792
841
  }
793
842
 
@@ -834,7 +883,12 @@ export function runHook(
834
883
  createRequire(import.meta.url).resolve(spec),
835
884
  }: {
836
885
  err?: typeof console.error;
837
- run?: typeof spawnSync;
886
+ /** Runs the hook with the terminal's streams: `spawnSync` is one, and only its status is read. */
887
+ run?: (
888
+ file: string,
889
+ args: readonly string[],
890
+ options: { readonly stdio: "inherit" },
891
+ ) => { readonly status: number | null };
838
892
  resolve?: (spec: string) => string;
839
893
  } = {},
840
894
  ): number {
@@ -1453,8 +1507,7 @@ export async function run(
1453
1507
  // failing outright — the LaTeX rules simply have no language to run in.
1454
1508
  let texLanguage: unknown = null;
1455
1509
  try {
1456
- // @ts-expect-error — the module is .mjs and has no types
1457
- ({ texLanguage } = await import("../eslint-rules/latex-language.mjs"));
1510
+ ({ texLanguage } = await import("#eslint-rules/latex-language"));
1458
1511
  } catch {
1459
1512
  texLanguage = null;
1460
1513
  }
@@ -1462,7 +1515,7 @@ export async function run(
1462
1515
  const eslint = new ESLint({
1463
1516
  cwd: lintRoot(root, paths),
1464
1517
  overrideConfigFile: true,
1465
- overrideConfig: buildConfig(withPapers, texLanguage) as Linter.Config[],
1518
+ overrideConfig: eslintConfig(buildConfig(withPapers, texLanguage)),
1466
1519
  fix: a.fix,
1467
1520
  });
1468
1521
 
@@ -1478,8 +1531,7 @@ export async function run(
1478
1531
  // reached, which is what the very first run over an empty directory showed: instead of a clear
1479
1532
  // message a stack from the depths of eslint-helpers.js flew out. A failure stays a failure, but
1480
1533
  // an explicable one.
1481
- // eslint-disable-next-line @typescript-eslint/no-explicit-any -- #49: replace with a real type
1482
- let results: any[];
1534
+ let results: ESLint.LintResult[];
1483
1535
  try {
1484
1536
  results = await eslint.lintFiles(paths);
1485
1537
  } catch (e) {
@@ -1540,12 +1592,20 @@ async function firstUnownedFile(
1540
1592
  * is outside paperlint's scope (`all-matched-files-ignored` — a papers directory holding only
1541
1593
  * vendored scripts). Both mean "nothing was linted", which the caller reports itself.
1542
1594
  */
1595
+ /** What a thrown ESLint error says about itself; anything else thrown says nothing. */
1596
+ const EslintFailure = z
1597
+ .object({
1598
+ messageTemplate: z.string().optional(),
1599
+ message: z.string().optional(),
1600
+ })
1601
+ .catch({});
1602
+
1543
1603
  const isEmptySet = (e: unknown): boolean => {
1544
- const fail = e as { messageTemplate?: string; message?: string } | null;
1604
+ const fail = EslintFailure.parse(e);
1545
1605
  return (
1546
- fail?.messageTemplate === "file-not-found" ||
1547
- fail?.messageTemplate === "all-matched-files-ignored" ||
1548
- /No files matching/i.test(String(fail?.message))
1606
+ fail.messageTemplate === "file-not-found" ||
1607
+ fail.messageTemplate === "all-matched-files-ignored" ||
1608
+ /No files matching/i.test(String(fail.message))
1549
1609
  );
1550
1610
  };
1551
1611
 
@@ -1555,8 +1615,7 @@ const isEmptySet = (e: unknown): boolean => {
1555
1615
  */
1556
1616
  async function reportLint(
1557
1617
  eslint: ESLint,
1558
- // eslint-disable-next-line @typescript-eslint/no-explicit-any -- #49: replace with a real type
1559
- results: any[],
1618
+ results: ESLint.LintResult[],
1560
1619
  structure: ReturnType<typeof checkStructure>,
1561
1620
  {
1562
1621
  a,
@@ -1598,7 +1657,7 @@ async function reportLint(
1598
1657
  out.trim() ||
1599
1658
  (structure.length > 0
1600
1659
  ? ""
1601
- : `✓ ${results.length} file(s) checked, no findings`),
1660
+ : `✓ ${String(results.length)} file(s) checked, no findings`),
1602
1661
  );
1603
1662
  }
1604
1663
  if (structure.length > 0 || results.some((r) => r.errorCount > 0)) return 1;
@@ -1608,7 +1667,7 @@ async function reportLint(
1608
1667
  const warnings = results.reduce((n, r) => n + r.warningCount, 0);
1609
1668
  if (warnings > a.maxWarnings) {
1610
1669
  err(
1611
- `${warnings} warning(s) exceed the --max-warnings limit of ${a.maxWarnings}`,
1670
+ `${String(warnings)} warning(s) exceed the --max-warnings limit of ${String(a.maxWarnings)}`,
1612
1671
  );
1613
1672
  return 1;
1614
1673
  }
@@ -10,7 +10,7 @@
10
10
  *
11
11
  * Run: npx vigiles test src/doctor.harness.mjs
12
12
  */
13
- import { PAPERS_DIR_FIELD } from "../lib/paper-config.mjs";
13
+ import { PAPERS_DIR_FIELD } from "../lib/paper-config.ts";
14
14
  import {
15
15
  mkdtempSync,
16
16
  mkdirSync,
@@ -21,7 +21,7 @@ import {
21
21
  import { tmpdir } from "node:os";
22
22
  import { dirname, join } from "node:path";
23
23
  import { fileURLToPath } from "node:url";
24
- import { createChecker } from "../lib/check.mjs";
24
+ import { createChecker } from "../lib/check.ts";
25
25
 
26
26
  const HERE = dirname(fileURLToPath(import.meta.url));
27
27
  const { doctor, detectPapers, PROGRAMS, found } = await import(
@@ -279,7 +279,7 @@ const runDoctor = (
279
279
 
280
280
  // ── VII-bis. SKILLS THAT ARE NOT LINKED ARE NAMED — AND DO NOT FAIL THE RUN ─────────────────
281
281
  // Before this section a consumer without `.claude/skills/` links had no `/paper-pipeline`, and
282
- // nothing anywhere said so. The link state itself is `link-skills.harness.mjs`'s subject; here
282
+ // nothing anywhere said so. The link state itself is `link-skills.harness.ts`'s subject; here
283
283
  // only doctor's REPORTING of it is judged, so the state is handed in rather than built on disk.
284
284
  {
285
285
  const dir = consumer({ papersDir: "papers", declared: "papers" });
@@ -44,7 +44,7 @@ test("doctor: a settings file silent on papersDir, an occupied entry, an unreada
44
44
  const code = doctor({
45
45
  cwd: root,
46
46
  log: (s: string) => lines.push(s),
47
- run: (() => ({ status: 0 })) as never,
47
+ run: () => ({ status: 0 }),
48
48
  cliPapers: join(root, "papers"),
49
49
  skillLinks: () => ({
50
50
  ok: true,
package/src/doctor.ts CHANGED
@@ -36,6 +36,8 @@ import {
36
36
  import { PAPER_MARKERS } from "./build.ts";
37
37
  import { linkSkills, SKILLS_HOME, type LinkReport } from "./link-skills.ts";
38
38
  import { doctorHooks } from "./hooks-settings.ts";
39
+ import { messageOf } from "./domain/text.ts";
40
+ import { fieldOf } from "./domain/record.ts";
39
41
 
40
42
  export interface Program {
41
43
  readonly bin: string;
@@ -98,7 +100,17 @@ export const PROGRAMS: readonly Program[] = [
98
100
  },
99
101
  ];
100
102
 
101
- export const found = (bin: string, run = spawnSync): boolean =>
103
+ /**
104
+ * Asks the shell whether a program is on PATH. `spawnSync` is one; a fake answers with a status
105
+ * alone, because a status is all `found` reads.
106
+ */
107
+ export type ProgramProbe = (
108
+ file: string,
109
+ args: readonly string[],
110
+ options: { readonly shell: true; readonly stdio: "ignore" },
111
+ ) => { readonly status: number | null };
112
+
113
+ export const found = (bin: string, run: ProgramProbe = spawnSync): boolean =>
102
114
  run("command", ["-v", bin], { shell: true, stdio: "ignore" }).status === 0;
103
115
 
104
116
  /** Lists a directory's entries; the disk by default, a test stages an unreadable child. */
@@ -169,15 +181,13 @@ function declarationVerdict(raw: string | null): {
169
181
  } catch (e) {
170
182
  return {
171
183
  lines: [
172
- ` ✗ ${CONFIG_FILE} is not valid JSON: ${(e as Error).message}`,
184
+ ` ✗ ${CONFIG_FILE} is not valid JSON: ${messageOf(e)}`,
173
185
  ` the edit guard refuses every Bash command until it parses — fix it with Edit or Write`,
174
186
  ],
175
187
  bad: 1,
176
188
  };
177
189
  }
178
- const declared = (settings as Record<string, unknown> | null)?.[
179
- PAPERS_DIR_FIELD
180
- ];
190
+ const declared = fieldOf(settings, PAPERS_DIR_FIELD);
181
191
  return {
182
192
  lines: [
183
193
  declared === undefined
@@ -233,7 +243,7 @@ export interface DoctorOptions {
233
243
  cwd?: string;
234
244
  /** The project root the hooks are anchored to. Claude Code sets this; it defaults to `cwd`. */
235
245
  projectDir?: string;
236
- run?: typeof spawnSync;
246
+ run?: ProgramProbe;
237
247
  /** The directory the CLI resolved from its own config, so both sides can be compared. */
238
248
  cliPapers?: string | null;
239
249
  /** Reads the skill links without writing any. Injected only so a test can fake the install. */
@@ -334,7 +344,7 @@ export function doctor({
334
344
  try {
335
345
  out.push(...doctorHooks(root));
336
346
  } catch (e) {
337
- out.push(`hooks`, ` ⚠ not checked — ${(e as Error).message}`);
347
+ out.push(`hooks`, ` ⚠ not checked — ${messageOf(e)}`);
338
348
  }
339
349
  out.push("");
340
350
  log(out.join("\n"));
@@ -31,6 +31,7 @@
31
31
  * never read as empty.
32
32
  */
33
33
  import { err, ok, type Result } from "./result.ts";
34
+ import { messageOf } from "./text.ts";
34
35
 
35
36
  export const LOOKUP_CACHE_SCHEMA = 1;
36
37
 
@@ -106,7 +107,7 @@ function sectionOf<T>(
106
107
  name: string,
107
108
  section: unknown,
108
109
  known: readonly string[],
109
- one: (o: Json, at: string) => Result<T, string>,
110
+ one: (o: Json, fetched: string, at: string) => Result<T, string>,
110
111
  ): Result<Map<string, T>, string> {
111
112
  if (!isObject(section)) return err(`\`${name}\`: expected an object`);
112
113
  const out = new Map<string, T>();
@@ -115,18 +116,23 @@ function sectionOf<T>(
115
116
  if (!isObject(v)) return err(`${at}\`: expected an object`);
116
117
  const extra = unknownField(v, known);
117
118
  if (extra !== undefined) return err(`${at}\`: unknown field \`${extra}\``);
118
- if (!isDate(v["fetched"]))
119
+ const fetched = v["fetched"];
120
+ if (!isDate(fetched))
119
121
  return err(`${at}.fetched\`: expected a date, YYYY-MM-DD`);
120
- const r = one(v, at);
122
+ const r = one(v, fetched, at);
121
123
  if (!r.ok) return r;
122
124
  out.set(key, r.value);
123
125
  }
124
126
  return ok(out);
125
127
  }
126
128
 
127
- const cachedResponse = (o: Json, at: string): Result<CachedResponse, string> =>
129
+ const cachedResponse = (
130
+ o: Json,
131
+ fetched: string,
132
+ at: string,
133
+ ): Result<CachedResponse, string> =>
128
134
  isObject(o["response"]) && o["response"]["transport"] === "ok"
129
- ? ok({ fetched: o["fetched"] as string, response: o["response"] })
135
+ ? ok({ fetched, response: o["response"] })
130
136
  : err(
131
137
  `${at}.response\`: expected a successful response ({"transport": "ok", …})`,
132
138
  );
@@ -137,7 +143,11 @@ const isHit = (h: unknown): h is DblpHit =>
137
143
  Array.isArray(h["authors"]) &&
138
144
  h["authors"].every((a) => typeof a === "string");
139
145
 
140
- function cachedDblp(o: Json, at: string): Result<CachedDblp, string> {
146
+ function cachedDblp(
147
+ o: Json,
148
+ fetched: string,
149
+ at: string,
150
+ ): Result<CachedDblp, string> {
141
151
  const { title, hits } = o;
142
152
  if (typeof title !== "string") return err(`${at}.title\`: expected a string`);
143
153
  if (!Array.isArray(hits)) return err(`${at}.hits\`: expected an array`);
@@ -147,9 +157,9 @@ function cachedDblp(o: Json, at: string): Result<CachedDblp, string> {
147
157
  `${at}.hits[${String(bad)}]\`: expected {venue, year, type, title: string; authors: string[]}`,
148
158
  );
149
159
  return ok({
150
- fetched: o["fetched"] as string,
160
+ fetched,
151
161
  title,
152
- hits: hits as DblpHit[],
162
+ hits: hits.filter(isHit),
153
163
  });
154
164
  }
155
165
 
@@ -159,7 +169,7 @@ export function parseLookupCache(text: string): Result<LookupCache, string> {
159
169
  try {
160
170
  json = JSON.parse(text);
161
171
  } catch (e) {
162
- return err(`not JSON: ${(e as Error).message}`);
172
+ return err(`not JSON: ${messageOf(e)}`);
163
173
  }
164
174
  if (!isObject(json))
165
175
  return err("expected an object with `schema`, `citations` and `dblp`");
@@ -186,8 +196,8 @@ export function parseLookupCache(text: string): Result<LookupCache, string> {
186
196
  return ok({ citations: citations.value, dblp: dblp.value });
187
197
  }
188
198
 
189
- const sorted = <T>(m: ReadonlyMap<string, T>): Record<string, T> =>
190
- Object.fromEntries([...m.keys()].sort().map((k) => [k, m.get(k) as T]));
199
+ const sorted = <T>(m: ReadonlyMap<string, T>): Record<string, T | undefined> =>
200
+ Object.fromEntries([...m.keys()].sort().map((k) => [k, m.get(k)]));
191
201
 
192
202
  /** The cache as the file holds it: keys sorted, so one new answer is a small diff. */
193
203
  export const serializeLookupCache = (c: LookupCache): string =>
@@ -0,0 +1,15 @@
1
+ import assert from "node:assert/strict";
2
+ import { test } from "vitest";
3
+ import { absolutePath, joinPath } from "./paths.ts";
4
+
5
+ test("absolutePath keeps an absolute path as it is", () => {
6
+ assert.equal(absolutePath("/a/b"), "/a/b");
7
+ });
8
+
9
+ test("absolutePath refuses a relative path instead of letting the process's cwd decide it", () => {
10
+ assert.throws(() => absolutePath("a/b"), /not an absolute path: a\/b/);
11
+ });
12
+
13
+ test("joinPath stays under its absolute base", () => {
14
+ assert.equal(joinPath(absolutePath("/a"), "b", "c"), "/a/b/c");
15
+ });
@@ -1,5 +1,24 @@
1
1
  /** Paths as the domain holds them. */
2
+ import { isAbsolute, join } from "node:path";
2
3
  import type { Opaque } from "ts-essentials";
3
4
 
4
- /** An absolute path. Minted at the composition root (`abs`), so nothing inside resolves a cwd. */
5
+ /**
6
+ * An absolute path. Minted by `absolutePath` (a checked string) or `joinPath` (an absolute base and
7
+ * segments) only, so nothing holding one has to ask whether it is relative.
8
+ */
5
9
  export type AbsolutePath = Opaque<string, "AbsolutePath">;
10
+
11
+ // The brand's one owner: the predicate IS the check, so no assertion is needed to mint the type.
12
+ const isAbsolutePath = (p: string): p is AbsolutePath => isAbsolute(p);
13
+
14
+ /** A path the caller holds as text, checked where it becomes a value. A relative one throws. */
15
+ export function absolutePath(p: string): AbsolutePath {
16
+ if (!isAbsolutePath(p)) throw new Error(`not an absolute path: ${p}`);
17
+ return p;
18
+ }
19
+
20
+ /** `segments` under `base`: absolute because `base` is. */
21
+ export const joinPath = (
22
+ base: AbsolutePath,
23
+ ...segments: string[]
24
+ ): AbsolutePath => absolutePath(join(base, ...segments));
@@ -0,0 +1,31 @@
1
+ /** A program `paperlint toolchain` installed or found: present, verified, running. */
2
+
3
+ // Not exported: a `Ready` carries this property, and the one way to write it is `ready` below.
4
+ const READY = Symbol("Ready");
5
+
6
+ /**
7
+ * Present, verified, running. BRANDED: a literal `{ where, fresh, verified }` is not a `Ready`, so
8
+ * only an installer's adapter — one minting call each (banal's is `ready` in
9
+ * `adapters/banal/index.ts`) — can make one, and a ready line cannot be printed without it.
10
+ */
11
+ export interface Ready {
12
+ /** Where it is: the program's path or its bin directory. */
13
+ readonly where: string;
14
+ /** This run installed it. */
15
+ readonly fresh: boolean;
16
+ /** What was checked before calling it ready, as a person reads it. */
17
+ readonly verified: string;
18
+ readonly [READY]: true;
19
+ }
20
+
21
+ /** An installer's verdict that its program is ready. Called by an installer adapter, after its checks. */
22
+ export const ready = (r: {
23
+ readonly where: string;
24
+ readonly fresh: boolean;
25
+ readonly verified: string;
26
+ }): Ready => ({
27
+ where: r.where,
28
+ fresh: r.fresh,
29
+ verified: r.verified,
30
+ [READY]: true,
31
+ });
@@ -0,0 +1,20 @@
1
+ import assert from "node:assert/strict";
2
+ import { test } from "vitest";
3
+ import { fieldOf, isRecord, numbersOf } from "./record.ts";
4
+
5
+ test("isRecord: an object, and not null, an array or a string", () => {
6
+ assert.deepEqual(
7
+ [isRecord({}), isRecord(null), isRecord([]), isRecord("x")],
8
+ [true, false, false, false],
9
+ );
10
+ });
11
+
12
+ test("fieldOf reads an own or inherited field of a record, and nothing of anything else", () => {
13
+ assert.equal(fieldOf({ a: 1 }, "a"), 1);
14
+ assert.equal(fieldOf(new TypeError("x"), "name"), "TypeError");
15
+ assert.equal(fieldOf("text", "length"), undefined);
16
+ });
17
+
18
+ test("numbersOf keeps numbers as they are", () => {
19
+ assert.deepEqual(numbersOf([1, -0.5, 0]), [1, -0.5, 0]);
20
+ });