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
@@ -23,24 +23,26 @@ import { spawnSync } from "node:child_process";
23
23
  import { globSync, readFileSync } from "node:fs";
24
24
  import { dirname, join, matchesGlob, resolve } from "node:path";
25
25
  import { fileURLToPath } from "node:url";
26
- // @ts-expect-error — js-yaml ships no types, and the package does not depend on @types/js-yaml
27
26
  import yaml from "js-yaml";
28
27
  import ts from "typescript";
29
28
  import { test } from "vitest";
29
+ import { z } from "zod";
30
30
 
31
31
  const ROOT = dirname(dirname(fileURLToPath(import.meta.url)));
32
- const CONFIG = JSON.parse(readFileSync(join(ROOT, ".c8rc.json"), "utf8")) as {
33
- lines: number;
34
- statements: number;
35
- functions: number;
36
- branches: number;
37
- excludeAfterRemap: boolean;
38
- include: string[];
39
- exclude: string[];
40
- extension: string[];
41
- typeOnly: string[];
42
- evalOnly: string[];
43
- };
32
+ const CONFIG = z
33
+ .object({
34
+ lines: z.number(),
35
+ statements: z.number(),
36
+ functions: z.number(),
37
+ branches: z.number(),
38
+ excludeAfterRemap: z.boolean(),
39
+ include: z.array(z.string()),
40
+ exclude: z.array(z.string()),
41
+ extension: z.array(z.string()),
42
+ typeOnly: z.array(z.string()),
43
+ evalOnly: z.array(z.string()),
44
+ })
45
+ .parse(JSON.parse(readFileSync(join(ROOT, ".c8rc.json"), "utf8")));
44
46
 
45
47
  const expand = (patterns: readonly string[]): string[] =>
46
48
  patterns.flatMap((p) => globSync(p, { cwd: ROOT })).sort();
@@ -53,13 +55,45 @@ export function runtimeStatements(fileName: string, source: string): string[] {
53
55
  (s) =>
54
56
  !ts.isInterfaceDeclaration(s) &&
55
57
  !ts.isTypeAliasDeclaration(s) &&
56
- !(ts.isImportDeclaration(s) && s.importClause?.isTypeOnly) &&
58
+ !(
59
+ ts.isImportDeclaration(s) &&
60
+ s.importClause?.phaseModifier === ts.SyntaxKind.TypeKeyword
61
+ ) &&
57
62
  !(ts.isExportDeclaration(s) && s.isTypeOnly),
58
63
  )
59
64
  .map((s) => ts.SyntaxKind[s.kind]);
60
65
  }
61
66
 
62
67
  /** Every relative module specifier a source imports, statically or with `import("…")`. */
68
+ /**
69
+ * The package's subpath imports (`#lib/*`, `#eslint-rules/*`), each as its prefix and the source
70
+ * file pattern its `paperlint-source` condition names — so an import written `#lib/x` is followed
71
+ * to `lib/x.ts` like a relative one, instead of being skipped as a bare package name.
72
+ */
73
+ const SUBPATHS = Object.entries(
74
+ z
75
+ .object({
76
+ imports: z.record(
77
+ z.string().endsWith("/*"),
78
+ z.object({ "paperlint-source": z.string() }),
79
+ ),
80
+ })
81
+ .parse(JSON.parse(readFileSync(join(ROOT, "package.json"), "utf8")))
82
+ .imports,
83
+ ).map(([key, t]) => ({
84
+ prefix: key.slice(0, -"*".length),
85
+ target: t["paperlint-source"],
86
+ }));
87
+
88
+ /** Where a specifier points inside the repository: a relative path, or a subpath import's source. */
89
+ function resolvedSpecifier(fileName: string, spec: string): string[] {
90
+ if (spec.startsWith(".")) return [resolve(dirname(fileName), spec)];
91
+ return SUBPATHS.filter(({ prefix }) => spec.startsWith(prefix)).map(
92
+ ({ prefix, target }) =>
93
+ resolve(ROOT, target.replace("*", spec.slice(prefix.length))),
94
+ );
95
+ }
96
+
63
97
  export function importedPaths(fileName: string, source: string): string[] {
64
98
  const sf = ts.createSourceFile(fileName, source, ts.ScriptTarget.Latest);
65
99
  const found: string[] = [];
@@ -70,8 +104,8 @@ export function importedPaths(fileName: string, source: string): string[] {
70
104
  node.expression.kind === ts.SyntaxKind.ImportKeyword
71
105
  ? node.arguments[0]
72
106
  : undefined;
73
- if (spec && ts.isStringLiteral(spec) && spec.text.startsWith("."))
74
- found.push(resolve(dirname(fileName), spec.text));
107
+ if (spec && ts.isStringLiteral(spec))
108
+ found.push(...resolvedSpecifier(fileName, spec.text));
75
109
  ts.forEachChild(node, visit);
76
110
  };
77
111
  visit(sf);
@@ -179,7 +213,10 @@ export function comments(fileName: string, source: string): string[] {
179
213
  const visit = (node: ts.Node): void => {
180
214
  if (ts.isJSDoc(node)) return; // its text is the comment already collected before its node
181
215
  const kids = node.getChildren(sf);
182
- if (kids.length > 0) return kids.forEach(visit);
216
+ if (kids.length > 0) {
217
+ kids.forEach(visit);
218
+ return;
219
+ }
183
220
  for (const r of [
184
221
  ...(ts.getLeadingCommentRanges(source, node.pos) ?? []),
185
222
  ...(ts.getTrailingCommentRanges(source, node.end) ?? []),
@@ -234,32 +271,36 @@ test("the floor is 100 on all four measures, and the script that checks it canno
234
271
  [CONFIG.lines, CONFIG.statements, CONFIG.functions, CONFIG.branches],
235
272
  [100, 100, 100, 100],
236
273
  );
237
- const pkg = JSON.parse(readFileSync(join(ROOT, "package.json"), "utf8")) as {
238
- scripts: Record<string, string>;
239
- };
274
+ const pkg = z
275
+ .object({ scripts: z.record(z.string(), z.string()) })
276
+ .parse(JSON.parse(readFileSync(join(ROOT, "package.json"), "utf8")));
240
277
  // Pinned whole: c8's command-line flags override .c8rc.json, so `--lines 50` or `--exclude "**"`
241
278
  // added here would bypass every threshold and the pinned exclude list below.
242
279
  assert.equal(
243
280
  pkg.scripts["coverage"],
244
- 'NODE_OPTIONS="--import=./test/coverage-src.mjs $NODE_OPTIONS" c8 --check-coverage npm test --',
281
+ 'NODE_OPTIONS="--import=./test/coverage-src.ts $NODE_OPTIONS" c8 --check-coverage npm test --',
245
282
  );
246
283
  });
247
284
 
248
285
  /** The gates job and the one step of it that runs coverage, read from the parsed workflow. */
249
- interface Step {
250
- readonly run?: string;
251
- readonly if?: unknown;
252
- readonly "continue-on-error"?: unknown;
253
- }
254
- interface Job {
255
- readonly if?: unknown;
256
- readonly "continue-on-error"?: unknown;
257
- readonly steps?: readonly Step[];
258
- }
286
+ const StepSchema = z.looseObject({
287
+ run: z.string().optional(),
288
+ if: z.unknown().optional(),
289
+ "continue-on-error": z.unknown().optional(),
290
+ });
291
+ const JobSchema = z.looseObject({
292
+ if: z.unknown().optional(),
293
+ "continue-on-error": z.unknown().optional(),
294
+ steps: z.array(StepSchema).optional(),
295
+ });
296
+ type Step = z.infer<typeof StepSchema>;
297
+ type Job = z.infer<typeof JobSchema>;
259
298
  export function coverageSteps(
260
299
  workflow: string,
261
300
  ): { job: string; definition: Job; step: Step }[] {
262
- const ci = yaml.load(workflow) as { jobs: Record<string, Job> };
301
+ const ci = z
302
+ .looseObject({ jobs: z.record(z.string(), JobSchema) })
303
+ .parse(yaml.load(workflow));
263
304
  return Object.entries(ci.jobs).flatMap(([job, definition]) =>
264
305
  (definition.steps ?? [])
265
306
  .filter((step) => (step.run ?? "").trim().startsWith("npm run coverage"))
@@ -273,11 +314,12 @@ const DRAFT_GATE =
273
314
  /** Why CI's coverage step would not fail the run on a coverage drop — empty when it would. */
274
315
  export function coverageStepHoles(workflow: string): string[] {
275
316
  const found = coverageSteps(workflow);
276
- if (found.length !== 1)
317
+ const [only, ...more] = found;
318
+ if (only === undefined || more.length > 0)
277
319
  return [
278
320
  `expected one step running \`npm run coverage\`, found ${String(found.length)}`,
279
321
  ];
280
- const [{ job, definition, step }] = found as [(typeof found)[number]];
322
+ const { job, definition, step } = only;
281
323
  return [
282
324
  ...(job === "gates" ? [] : [`the step is in job ${job}, not gates`]),
283
325
  ...(definition.if === DRAFT_GATE
@@ -345,7 +387,7 @@ test("dist/ is excluded BEFORE source maps: a direct load is not a second copy o
345
387
  test("the exclude list is exactly the justified set", () => {
346
388
  assert.deepEqual(CONFIG.exclude, [
347
389
  // tests themselves
348
- "**/*.harness.mjs",
390
+ "**/*.harness.*",
349
391
  "**/*.test.*",
350
392
  // PAID LLM evaluations, never run by `npm test`
351
393
  "**/*.eval.mjs",
@@ -356,7 +398,7 @@ test("the exclude list is exactly the justified set", () => {
356
398
  "**/*.d.mts",
357
399
  // inputs the tests read
358
400
  "**/fixtures/**",
359
- // build output (the coverage run resolves it to src/ — test/coverage-src.mjs)
401
+ // build output (the coverage run resolves it to src/ — test/coverage-src.ts)
360
402
  "dist/**",
361
403
  // test support
362
404
  "test/**",
@@ -388,11 +430,11 @@ test("the exclude list is exactly the justified set", () => {
388
430
  "src/types.ts",
389
431
  "src/ports/*.ts",
390
432
  "src/domain/page-layout.ts",
391
- "src/domain/paths.ts",
433
+ "eslint-rules/rule-context.ts",
392
434
  ]);
393
435
  assert.deepEqual(CONFIG.evalOnly, [
394
436
  "lib/skill-eval-kit.mjs",
395
- "lib/skill-eval-fixture.mjs",
437
+ "lib/skill-eval-fixture.ts",
396
438
  "lib/trigger-ledger.mjs",
397
439
  ]);
398
440
  });
@@ -7,7 +7,7 @@ import assert from "node:assert/strict";
7
7
  import { writeFileSync } from "node:fs";
8
8
  import { join } from "node:path";
9
9
  import { test } from "vitest";
10
- import { useTempDir } from "../test/support.mjs";
10
+ import { useTempDir } from "../test/support.ts";
11
11
  import { guard } from "./eslint-report-guard.mjs";
12
12
 
13
13
  test("warnings and rule-less messages are listed; a file with no messages counts as linted", () => {
@@ -11,12 +11,11 @@
11
11
  "eslint-rules/pdf-last-page-balance.harness.mjs",
12
12
  "eslint-rules/temp-root-realpath.harness.mjs",
13
13
  "eslint-rules/tex-build.harness.mjs",
14
- "fixtures/real-markdown-paper/real-markdown-paper.harness.mjs",
14
+ "fixtures/real-markdown-paper/real-markdown-paper.harness.ts",
15
15
  "lib/agent-cli-version.harness.mjs",
16
16
  "lib/paper-config.harness.mjs",
17
17
  "lib/skill-trigger-cases.harness.mjs",
18
- "scripts/check.harness.mjs",
19
- "scripts/rules-are-content-only.harness.mjs",
18
+ "scripts/check.harness.ts",
20
19
  "scripts/rules-see-files.harness.mjs",
21
20
  "skills/analyze-sibling-paper/analyze-sibling-paper.harness.mjs",
22
21
  "skills/argument-arc/argument-arc.harness.mjs",
@@ -29,21 +28,20 @@
29
28
  "skills/grade-paper-writing/grade-paper-writing.harness.mjs",
30
29
  "skills/harden-paper/harden-paper.harness.mjs",
31
30
  "skills/map-prior-work/map-prior-work.harness.mjs",
32
- "skills/osf-artifact-upload/osf-artifact-upload.harness.mjs",
31
+ "skills/osf-artifact-upload/osf-artifact-upload.harness.ts",
33
32
  "skills/paper-adversarial-review/paper-adversarial-review.harness.mjs",
34
33
  "skills/paper-pipeline/paper-pipeline.harness.mjs",
35
34
  "skills/paper-pipeline/scripts/artifact-coverage.harness.mjs",
36
35
  "skills/paper-pipeline/scripts/consumer.harness.mjs",
37
36
  "skills/paper-pipeline/scripts/extract-ref-facts.harness.mjs",
38
37
  "skills/paper-pipeline/scripts/generated-code.harness.mjs",
39
- "skills/paper-pipeline/scripts/pipeline-check.harness.mjs",
40
38
  "skills/paper-pipeline/scripts/population-map.harness.mjs",
41
39
  "skills/paper-pipeline/scripts/provenance.harness.mjs",
42
- "skills/paper-pipeline/scripts/round-diff.harness.mjs",
40
+ "skills/paper-pipeline/scripts/round-diff.harness.ts",
43
41
  "skills/paper-status/paper-status.harness.mjs",
44
42
  "skills/pc-panel-review/pc-panel-review.harness.mjs",
45
43
  "skills/plan-paper-timeline/plan-paper-timeline.harness.mjs",
46
- "skills/render-paper/extract-pdf-facts.harness.mjs",
44
+ "skills/render-paper/extract-pdf-facts.harness.ts",
47
45
  "skills/render-paper/render-paper.harness.mjs",
48
46
  "skills/research-ideate/research-ideate.harness.mjs",
49
47
  "skills/study-accepted-papers/study-accepted-papers.harness.mjs",
@@ -56,16 +54,16 @@
56
54
  "src/cli.harness.mjs",
57
55
  "src/doctor.harness.mjs",
58
56
  "src/engine.harness.mjs",
59
- "src/hooks-settings.harness.mjs",
60
- "src/latex-log.harness.mjs",
61
- "src/latex-loop.harness.mjs",
62
- "src/link-skills.harness.mjs",
57
+ "src/hooks-settings.harness.ts",
58
+ "src/latex-log.harness.ts",
59
+ "src/latex-loop.harness.ts",
60
+ "src/link-skills.harness.ts",
63
61
  "src/new-paper.harness.mjs",
64
- "src/pdf-facts.harness.mjs",
65
- "src/pdf-geometry.harness.mjs",
66
- "src/rules-config.harness.mjs",
67
- "src/structure.harness.mjs",
68
- "src/tex-requirements.harness.mjs",
62
+ "src/pdf-facts.harness.ts",
63
+ "src/pdf-geometry.harness.ts",
64
+ "src/rules-config.harness.ts",
65
+ "src/structure.harness.ts",
66
+ "src/tex-requirements.harness.ts",
69
67
  "src/toolchain.harness.mjs"
70
68
  ]
71
69
  }
@@ -18,13 +18,16 @@ import { basename, dirname, join } from "node:path";
18
18
  import { test } from "vitest";
19
19
  import { fileURLToPath } from "node:url";
20
20
  import ts from "typescript";
21
+ import { z } from "zod";
21
22
 
22
23
  const ROOT = dirname(dirname(fileURLToPath(import.meta.url)));
23
- const FROZEN: readonly string[] = (
24
- JSON.parse(
25
- readFileSync(join(ROOT, "scripts", "harness-api.frozen.json"), "utf8"),
26
- ) as { harnesses: string[] }
27
- ).harnesses;
24
+ const FROZEN: readonly string[] = z
25
+ .object({ harnesses: z.array(z.string()) })
26
+ .parse(
27
+ JSON.parse(
28
+ readFileSync(join(ROOT, "scripts", "harness-api.frozen.json"), "utf8"),
29
+ ),
30
+ ).harnesses;
28
31
 
29
32
  /** vigiles' agent-testing API — what a harness exists to call. */
30
33
  export const AGENT_API = new Set(["runHook", "runHarnessTest", "runEval"]);
@@ -1,5 +1,5 @@
1
1
  /**
2
- * The legacy-exemption ratchet (`layer-legacy-frozen.mjs`): each of its refusals fires on a planted
2
+ * The legacy-exemption ratchet (`layer-legacy-frozen.ts`): each of its refusals fires on a planted
3
3
  * case and stays silent on the matching clean one. The inputs are ESLint's own result shape, so the
4
4
  * tally is tested on what the linter reports, not on source text.
5
5
  */
@@ -14,8 +14,8 @@ import {
14
14
  main,
15
15
  tally,
16
16
  type Suppressed,
17
- } from "./layer-legacy-frozen.mjs";
18
- import { useTempDir, writeTree } from "../test/support.mjs";
17
+ } from "./layer-legacy-frozen.ts";
18
+ import { useTempDir, writeTree } from "../test/support.ts";
19
19
 
20
20
  const IO = "legacy I/O, moves behind a port in #76";
21
21
  const LAYER = "legacy layer, moves behind a port in #76";
@@ -131,7 +131,7 @@ test("main: problems are listed and fail; a clean count passes", async () => {
131
131
  }) => {
132
132
  const out: string[] = [];
133
133
  const code = await main({
134
- check: async () => ({ ...r, frozen: {} }),
134
+ check: () => Promise.resolve({ ...r, frozen: {} }),
135
135
  log: (s: string) => out.push(s),
136
136
  err: (s: string) => out.push(`E ${s}`),
137
137
  });
@@ -191,11 +191,14 @@ test("checkFrozen: a lint that returns no result reports that nothing was counte
191
191
  writeTree(root, {
192
192
  [FROZEN_FILE]: JSON.stringify({ files: { "src/a.ts": { x: 1 } } }),
193
193
  });
194
- assert.deepEqual(await checkFrozen(root, { lint: async () => [] }), {
195
- problems: ["src/**/*.ts matched no file — nothing was counted."],
196
- frozen: { "src/a.ts": { x: 1 } },
197
- counts: {},
198
- });
194
+ assert.deepEqual(
195
+ await checkFrozen(root, { lint: () => Promise.resolve([]) }),
196
+ {
197
+ problems: ["src/**/*.ts matched no file — nothing was counted."],
198
+ frozen: { "src/a.ts": { x: 1 } },
199
+ counts: {},
200
+ },
201
+ );
199
202
  });
200
203
 
201
204
  test("tally: optional parts of ESLint's result shape are read as absent", () => {
@@ -221,9 +224,10 @@ test("checkFrozen: a frozen file with no `files` freezes nothing", async () => {
221
224
  const root = useTempDir("layer-frozen-nofiles-");
222
225
  writeTree(root, { [FROZEN_FILE]: "{}\n" });
223
226
  const r = await checkFrozen(root, {
224
- lint: async () => [
225
- { filePath: `${root}/src/a.ts`, suppressedMessages: [] },
226
- ],
227
+ lint: () =>
228
+ Promise.resolve([
229
+ { filePath: `${root}/src/a.ts`, suppressedMessages: [] },
230
+ ]),
227
231
  });
228
232
  assert.deepEqual(r, { problems: [], frozen: {}, counts: {} });
229
233
  });
@@ -245,7 +249,7 @@ test("run as a program: the gate's verdict is the exit code", () => {
245
249
  [
246
250
  "--import",
247
251
  preload,
248
- fileURLToPath(new URL("./layer-legacy-frozen.mjs", import.meta.url)),
252
+ fileURLToPath(new URL("./layer-legacy-frozen.ts", import.meta.url)),
249
253
  ],
250
254
  { encoding: "utf8" },
251
255
  );
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * layer-legacy-frozen.mjs — the legacy exemptions from `src/`'s layer rules may only SHRINK.
3
+ * layer-legacy-frozen.ts — the legacy exemptions from `src/`'s layer rules may only SHRINK.
4
4
  *
5
5
  * `src/` is hexagonal on two axes (src/CLAUDE.md): who may know whom, and where effects may be
6
6
  * written. The flat `src/*.ts` files predate both. Each site in them that still breaks a rule
@@ -32,22 +32,55 @@
32
32
  * counted by construction — and a range directive (`/* eslint-disable … *\/` around a multi-line
33
33
  * import) counts exactly what it silences.
34
34
  *
35
- * Run: `node scripts/layer-legacy-frozen.mjs` (also part of `npm run check`)
35
+ * Run: `node scripts/layer-legacy-frozen.ts` (also part of `npm run check`)
36
36
  */
37
37
  import { isMain } from "../skills/paper-pipeline/scripts/consumer.mjs";
38
38
  import { readFileSync } from "node:fs";
39
39
  import { dirname, join, relative, sep } from "node:path";
40
40
  import { fileURLToPath } from "node:url";
41
41
  import { ESLint } from "eslint";
42
+ import { z } from "zod";
43
+
44
+ /** One file's result, in the shape ESLint's `LintResult` reports suppressions. */
45
+ export interface Suppressed {
46
+ readonly filePath: string;
47
+ readonly suppressedMessages?: readonly {
48
+ readonly ruleId: string | null;
49
+ readonly line: number;
50
+ readonly suppressions?: readonly { readonly justification?: string }[];
51
+ }[];
52
+ }
53
+
54
+ /** Per file, per rule: a count of suppressed findings. */
55
+ export type Counts = Record<string, Record<string, number>>;
56
+
57
+ /** What `checkFrozen` reports: the problems, and both sides of the comparison. */
58
+ export interface FrozenCheck {
59
+ problems: string[];
60
+ frozen: Counts;
61
+ counts: Counts;
62
+ }
63
+
64
+ /** The frozen list on disk. A file with no `files` key freezes nothing. */
65
+ const FrozenFile = z.object({
66
+ files: z.record(z.string(), z.record(z.string(), z.number())).optional(),
67
+ });
42
68
 
43
69
  const ROOT = join(dirname(fileURLToPath(import.meta.url)), "..");
44
70
  export const FROZEN_FILE = join("scripts", "layer-legacy.frozen.json");
45
71
 
46
72
  /** The rules that enforce the layers. A suppression of any other rule is not this check's business. */
47
- export const LAYER_RULES = ["boundaries/dependencies", "no-restricted-globals"];
73
+ export const LAYER_RULES: readonly string[] = [
74
+ "boundaries/dependencies",
75
+ "no-restricted-globals",
76
+ ];
48
77
 
49
78
  /** Where a legacy suppression may never appear: the folders written under the rules. */
50
- export const NO_LEGACY_UNDER = ["src/domain/", "src/ports/", "src/adapters/"];
79
+ export const NO_LEGACY_UNDER: readonly string[] = [
80
+ "src/domain/",
81
+ "src/ports/",
82
+ "src/adapters/",
83
+ ];
51
84
 
52
85
  /** The justification every legacy directive carries — `I/O` or `layer`, then the issue. */
53
86
  export const LEGACY = /^legacy (I\/O|layer), moves behind a port in #76$/;
@@ -57,12 +90,16 @@ export const LEGACY = /^legacy (I\/O|layer), moves behind a port in #76$/;
57
90
  * results (`filePath` relative to the repository, POSIX separators) and returns the tally plus
58
91
  * every layer suppression that is not a legacy one.
59
92
  */
60
- export function tally(results) {
61
- const counts = {};
62
- const unexplained = [];
93
+ export function tally(results: readonly Suppressed[]): {
94
+ counts: Counts;
95
+ unexplained: string[];
96
+ } {
97
+ const counts: Counts = {};
98
+ const unexplained: string[] = [];
63
99
  for (const r of results)
64
100
  for (const m of r.suppressedMessages ?? []) {
65
- if (!LAYER_RULES.includes(m.ruleId ?? "")) continue;
101
+ const rule = m.ruleId ?? "";
102
+ if (!LAYER_RULES.includes(rule)) continue;
66
103
  const why = (m.suppressions ?? []).map((s) => s.justification ?? "");
67
104
  if (!why.some((j) => LEGACY.test(j.trim()))) {
68
105
  unexplained.push(
@@ -71,39 +108,64 @@ export function tally(results) {
71
108
  continue;
72
109
  }
73
110
  const file = (counts[r.filePath] ??= {});
74
- file[m.ruleId] = (file[m.ruleId] ?? 0) + 1;
111
+ file[rule] = (file[rule] ?? 0) + 1;
75
112
  }
76
113
  return { counts, unexplained };
77
114
  }
78
115
 
116
+ /** A file carrying a legacy exemption where none may be, or one the list does not know. */
117
+ function misplaced(f: string, frozen: Counts): string[] {
118
+ if (NO_LEGACY_UNDER.some((d) => f.startsWith(d)))
119
+ return [
120
+ `${f}: carries a legacy layer exemption, and nothing under ${NO_LEGACY_UNDER.join(", ")} may — those folders are written under the rules. Fix the site (src/CLAUDE.md).`,
121
+ ];
122
+ if (!(f in frozen))
123
+ return [
124
+ `${f}: a new legacy layer exemption is not accepted (#76). A module that needs the world is a new *.io.ts in the adapter of the program it talks to; an app file takes a port.`,
125
+ ];
126
+ return [];
127
+ }
128
+
129
+ /** One rule's count in one file against its frozen number: grown, new, or shrunk without a record. */
130
+ function compareCount(
131
+ f: string,
132
+ rule: string,
133
+ got: number,
134
+ frozen: Counts,
135
+ ): string[] {
136
+ const want = frozen[f]?.[rule];
137
+ if (want === undefined)
138
+ return f in frozen
139
+ ? [
140
+ `${f}: ${String(got)} legacy ${rule} suppression(s), frozen at 0 — no new exemptions in existing files (#76).`,
141
+ ]
142
+ : [];
143
+ if (got > want)
144
+ return [
145
+ `${f}: ${String(got)} legacy ${rule} suppressions, frozen at ${String(want)} — no new exemptions in existing files (#76).`,
146
+ ];
147
+ if (got < want)
148
+ return [
149
+ `${f}: ${String(got)} legacy ${rule} suppressions, frozen at ${String(want)} — lower it to ${String(got)} in ${FROZEN_FILE} (the list must shrink with the code).`,
150
+ ];
151
+ return [];
152
+ }
153
+
79
154
  /** Compare the tally with the frozen list. Pure. `frozen` is `{ path: { rule: count } }`. */
80
- export function judge({ counts, unexplained, frozen }) {
155
+ export function judge({
156
+ counts,
157
+ unexplained,
158
+ frozen,
159
+ }: {
160
+ counts: Counts;
161
+ unexplained: readonly string[];
162
+ frozen: Counts;
163
+ }): string[] {
81
164
  const problems = [...unexplained];
82
165
  for (const [f, rules] of Object.entries(counts)) {
83
- if (NO_LEGACY_UNDER.some((d) => f.startsWith(d)))
84
- problems.push(
85
- `${f}: carries a legacy layer exemption, and nothing under ${NO_LEGACY_UNDER.join(", ")} may — those folders are written under the rules. Fix the site (src/CLAUDE.md).`,
86
- );
87
- else if (!(f in frozen))
88
- problems.push(
89
- `${f}: a new legacy layer exemption is not accepted (#76). A module that needs the world is a new *.io.ts in the adapter of the program it talks to; an app file takes a port.`,
90
- );
91
- for (const [rule, got] of Object.entries(rules)) {
92
- const want = frozen[f]?.[rule];
93
- if (want === undefined) {
94
- if (f in frozen)
95
- problems.push(
96
- `${f}: ${String(got)} legacy ${rule} suppression(s), frozen at 0 — no new exemptions in existing files (#76).`,
97
- );
98
- } else if (got > want)
99
- problems.push(
100
- `${f}: ${String(got)} legacy ${rule} suppressions, frozen at ${String(want)} — no new exemptions in existing files (#76).`,
101
- );
102
- else if (got < want)
103
- problems.push(
104
- `${f}: ${String(got)} legacy ${rule} suppressions, frozen at ${String(want)} — lower it to ${String(got)} in ${FROZEN_FILE} (the list must shrink with the code).`,
105
- );
106
- }
166
+ problems.push(...misplaced(f, frozen));
167
+ for (const [rule, got] of Object.entries(rules))
168
+ problems.push(...compareCount(f, rule, got, frozen));
107
169
  }
108
170
  for (const [f, rules] of Object.entries(frozen))
109
171
  for (const [rule, want] of Object.entries(rules))
@@ -114,12 +176,35 @@ export function judge({ counts, unexplained, frozen }) {
114
176
  return problems;
115
177
  }
116
178
 
179
+ /**
180
+ * The repository's configuration, narrowed to what this check counts: `LAYER_RULES` and their
181
+ * suppressions, with no type information. The configuration is
182
+ * type-aware, and a type-aware lint builds the whole TypeScript program first — 33 s on a CI runner
183
+ * under coverage, for rules that read only import paths. Suppressions of the filtered rules are
184
+ * still reported, which is what `tally` reads.
185
+ */
186
+ function layerLint(cwd: string): ESLint {
187
+ return new ESLint({
188
+ cwd,
189
+ ruleFilter: ({ ruleId }) => LAYER_RULES.includes(ruleId),
190
+ overrideConfig: {
191
+ languageOptions: {
192
+ parserOptions: { project: false, projectService: false },
193
+ },
194
+ },
195
+ });
196
+ }
197
+
117
198
  /** Lint `src/` with the repository's configuration and run the whole check. */
118
199
  export async function checkFrozen(
119
- root = ROOT,
120
- { lint = (cwd) => new ESLint({ cwd }).lintFiles(["src/**/*.ts"]) } = {},
121
- ) {
122
- const data = JSON.parse(readFileSync(join(root, FROZEN_FILE), "utf8"));
200
+ root: string = ROOT,
201
+ {
202
+ lint = (cwd: string) => layerLint(cwd).lintFiles(["src/**/*.ts"]),
203
+ }: { lint?: (cwd: string) => Promise<readonly Suppressed[]> } = {},
204
+ ): Promise<FrozenCheck> {
205
+ const data = FrozenFile.parse(
206
+ JSON.parse(readFileSync(join(root, FROZEN_FILE), "utf8")),
207
+ );
123
208
  const frozen = data.files ?? {};
124
209
  // ESLint itself THROWS when the glob matches nothing ("No files matching 'src/**/*.ts' were
125
210
  // found") or only ignored files — measured. An empty result from any other linter (or a
@@ -147,7 +232,11 @@ export async function main({
147
232
  check = () => checkFrozen(ROOT),
148
233
  log = console.log,
149
234
  err = console.error,
150
- } = {}) {
235
+ }: {
236
+ check?: () => Promise<FrozenCheck>;
237
+ log?: (line: string) => void;
238
+ err?: (line: string) => void;
239
+ } = {}): Promise<number> {
151
240
  const { problems, counts } = await check();
152
241
  if (problems.length) {
153
242
  err(
@@ -15,11 +15,19 @@ import { describe, expect, test } from "vitest";
15
15
  import { readFileSync } from "node:fs";
16
16
  import { analyzeCommits } from "@semantic-release/commit-analyzer";
17
17
  import { generateNotes } from "@semantic-release/release-notes-generator";
18
+ import { z } from "zod";
18
19
 
19
- type PluginEntry = string | [string, Record<string, unknown>];
20
- const releaserc = JSON.parse(
21
- readFileSync(new URL("../.releaserc.json", import.meta.url), "utf8"),
22
- ) as { plugins: PluginEntry[] };
20
+ const PluginEntry = z.union([
21
+ z.string(),
22
+ z.tuple([z.string(), z.record(z.string(), z.unknown())]),
23
+ ]);
24
+ const releaserc = z
25
+ .object({ plugins: z.array(PluginEntry) })
26
+ .parse(
27
+ JSON.parse(
28
+ readFileSync(new URL("../.releaserc.json", import.meta.url), "utf8"),
29
+ ),
30
+ );
23
31
 
24
32
  /** The options `.releaserc.json` gives one plugin, exactly as semantic-release would pass them. */
25
33
  function optionsFor(name: string): Record<string, unknown> {
@@ -5,7 +5,7 @@ import assert from "node:assert/strict";
5
5
  import { spawnSync } from "node:child_process";
6
6
  import { fileURLToPath } from "node:url";
7
7
  import { test } from "vitest";
8
- import { useTempDir, writeTree } from "../test/support.mjs";
8
+ import { useTempDir, writeTree } from "../test/support.ts";
9
9
  import { main as seeMain } from "./rules-see-files.mjs";
10
10
 
11
11
  const capture = () => {