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
@@ -12,7 +12,7 @@
12
12
  * The fixture package keeps its skills where the real one does, under `SHIPPED_SKILLS_DIR` from
13
13
  * consumer.mjs — the same constant the linker and the install e2e read.
14
14
  *
15
- * Run: node src/link-skills.harness.mjs
15
+ * Run: node src/link-skills.harness.ts
16
16
  */
17
17
  import {
18
18
  existsSync,
@@ -26,17 +26,14 @@ import {
26
26
  writeFileSync,
27
27
  } from "node:fs";
28
28
  import { tmpdir } from "node:os";
29
- import { dirname, isAbsolute, join } from "node:path";
30
- import { fileURLToPath } from "node:url";
31
- import { createChecker } from "../lib/check.mjs";
29
+ import { isAbsolute, join } from "node:path";
30
+ import { createChecker } from "../lib/check.ts";
31
+ import type { LinkReport } from "./link-skills.ts";
32
32
 
33
- const HERE = dirname(fileURLToPath(import.meta.url));
34
- const { linkSkills, locatePackage, shippedSkills } = await import(
35
- join(HERE, "link-skills.ts")
36
- );
37
- const { SHIPPED_SKILLS_DIR: SHIPS, PACKAGE_NAME: PKG } = await import(
38
- join(HERE, "..", "skills", "paper-pipeline", "scripts", "consumer.mjs")
39
- );
33
+ const { linkSkills, locatePackage, shippedSkills } =
34
+ await import("./link-skills.ts");
35
+ const { SHIPPED_SKILLS_DIR: SHIPS, PACKAGE_NAME: PKG } =
36
+ await import("../skills/paper-pipeline/scripts/consumer.mjs");
40
37
 
41
38
  const check = createChecker();
42
39
 
@@ -46,7 +43,7 @@ const work = realpathSync(
46
43
  );
47
44
 
48
45
  /** A package as a manager would unpack it: manifest, skills, one non-skill. */
49
- function writePackage(dir) {
46
+ function writePackage(dir: string): void {
50
47
  mkdirSync(dir, { recursive: true });
51
48
  writeFileSync(
52
49
  join(dir, "package.json"),
@@ -63,7 +60,7 @@ function writePackage(dir) {
63
60
  }
64
61
 
65
62
  /** A consumer project with the package installed the npm way or the pnpm way. */
66
- function consumer(name, manager = "npm") {
63
+ function consumer(name: string, manager = "npm"): string {
67
64
  const dir = join(work, name);
68
65
  mkdirSync(join(dir, "node_modules"), { recursive: true });
69
66
  writeFileSync(
@@ -89,8 +86,11 @@ function consumer(name, manager = "npm") {
89
86
  return dir;
90
87
  }
91
88
 
92
- const home = (dir) => join(dir, ".claude", "skills");
93
- const status = (report, name) => report.links.find((l) => l.name === name);
89
+ const home = (dir: string) => join(dir, ".claude", "skills");
90
+ /** The report's links; a refusal has none. */
91
+ const linksOf = (report: LinkReport) => (report.ok ? report.links : []);
92
+ const status = (report: LinkReport, name: string) =>
93
+ linksOf(report).find((l) => l.name === name);
94
94
 
95
95
  try {
96
96
  // ── I. A FRESH npm CONSUMER: EVERY DECLARED SKILL IS LINKED, AND ONLY SKILLS ───────────
@@ -99,11 +99,11 @@ try {
99
99
  const r = linkSkills(dir);
100
100
  check(
101
101
  "🔴 it links every skill the package ships",
102
- r.ok === true && SKILLS.every((s) => status(r, s)?.status === "created"),
102
+ r.ok && SKILLS.every((s) => status(r, s)?.status === "created"),
103
103
  );
104
104
  check(
105
105
  "and nothing that is not a skill — a directory without SKILL.md is not one",
106
- r.links.length === SKILLS.length &&
106
+ linksOf(r).length === SKILLS.length &&
107
107
  !existsSync(join(home(dir), "shared")),
108
108
  );
109
109
  check(
@@ -195,7 +195,7 @@ try {
195
195
  const r = linkSkills(dir);
196
196
  check(
197
197
  "a project that does not have the package gets an error, not links into somebody's cache",
198
- r.ok === false && /does not resolve from/.test(r.error),
198
+ !r.ok && /does not resolve from/.test(r.error),
199
199
  );
200
200
  check("and nothing is created", !existsSync(join(dir, ".claude")));
201
201
  }
@@ -229,5 +229,5 @@ try {
229
229
  }
230
230
 
231
231
  console.log(
232
- `✓ ${check.count} assertions passed — link-skills: the skills are where Claude Code looks`,
232
+ `✓ ${String(check.count)} assertions passed — link-skills: the skills are where Claude Code looks`,
233
233
  );
@@ -14,7 +14,7 @@ import {
14
14
  } from "node:fs";
15
15
  import { join } from "node:path";
16
16
  import { test } from "vitest";
17
- import { useTempDir, writeTree } from "../test/support.mjs";
17
+ import { useTempDir, writeTree } from "../test/support.ts";
18
18
  import { linkSkills, locatePackage, type LinkFs } from "./link-skills.ts";
19
19
 
20
20
  const root = useTempDir("paperlint-link-skills-");
@@ -62,7 +62,7 @@ test("a package without a skills directory, or one holding a dangling link, is n
62
62
  symlinkSync(join(root, "gone"), join(dangling, "skills", "b"));
63
63
  const r = linkSkills(join(root, "p2"), { locate: at(dangling) });
64
64
  assert.equal(r.ok, false);
65
- assert.match(!r.ok ? r.error : "", /^1 skill link\(s\) in .* lead nowhere:/);
65
+ assert.match(r.error, /^1 skill link\(s\) in .* lead nowhere:/);
66
66
  });
67
67
 
68
68
  test("a skills home that cannot be made is named", () => {
@@ -71,7 +71,7 @@ test("a skills home that cannot be made is named", () => {
71
71
  });
72
72
  const r = linkSkills(project, { locate: at(pkg("home", ["a"])) });
73
73
  assert.equal(r.ok, false);
74
- assert.match(!r.ok ? r.error : "", /^cannot create .*\.claude\/skills: /);
74
+ assert.match(r.error, /^cannot create .*\.claude\/skills: /);
75
75
  });
76
76
 
77
77
  test("entries that are someone else's: a plain file, one that cannot be resolved, a link that cannot be made", () => {
@@ -44,6 +44,7 @@ import {
44
44
  PACKAGE_NAME,
45
45
  SHIPPED_SKILLS_DIR,
46
46
  } from "../skills/paper-pipeline/scripts/consumer.mjs";
47
+ import { messageOf, codeOf } from "./domain/text.ts";
47
48
 
48
49
  export { PACKAGE_NAME };
49
50
  /** Where Claude Code looks for project skills, relative to the project root. */
@@ -74,7 +75,7 @@ export function locatePackage(
74
75
  manifest = resolveManifest(project);
75
76
  } catch (e) {
76
77
  return {
77
- error: `${(e as NodeJS.ErrnoException).code ?? "error"}: ${(e as Error).message.split("\n")[0]}`,
78
+ error: `${codeOf(e) ?? "error"}: ${String(messageOf(e).split("\n")[0])}`,
78
79
  };
79
80
  }
80
81
  const dir = realpathSync(dirname(manifest));
@@ -108,7 +109,7 @@ export function shippedSkills(
108
109
  try {
109
110
  return { skillsDir, names: installedSkills(skillsDir) };
110
111
  } catch (e) {
111
- return { error: (e as Error).message };
112
+ return { error: messageOf(e) };
112
113
  }
113
114
  }
114
115
 
@@ -207,7 +208,7 @@ function linkOne(
207
208
  return {
208
209
  name,
209
210
  status: "foreign",
210
- reason: `could not create the link: ${(e as Error).message}`,
211
+ reason: `could not create the link: ${messageOf(e)}`,
211
212
  };
212
213
  }
213
214
  }
@@ -218,7 +219,7 @@ function makeHome(home: string): string | null {
218
219
  mkdirSync(home, { recursive: true });
219
220
  return null;
220
221
  } catch (e) {
221
- return `cannot create ${home}: ${(e as Error).message}`;
222
+ return `cannot create ${home}: ${messageOf(e)}`;
222
223
  }
223
224
  }
224
225
 
@@ -13,6 +13,7 @@ import { tmpdir } from "node:os";
13
13
  import { join } from "node:path";
14
14
  import { afterEach, describe, expect, it } from "vitest";
15
15
  import { run } from "./cli.ts";
16
+ import { lintReport } from "../test/lint-report.ts";
16
17
 
17
18
  const dirs: string[] = [];
18
19
  afterEach(() => {
@@ -50,8 +51,8 @@ async function lint(root: string, args: string[] = []) {
50
51
  return { code, out: out.join("\n"), err: err.join("\n") };
51
52
  }
52
53
  const ruleIds = (stdout: string): string[] =>
53
- (JSON.parse(stdout) as { messages: { ruleId: string }[] }[]).flatMap((r) =>
54
- r.messages.map((m) => m.ruleId),
54
+ lintReport(stdout).flatMap((r) =>
55
+ r.messages.map((m) => m.ruleId ?? "(fatal)"),
55
56
  );
56
57
 
57
58
  describe("paperlint lint --fix", () => {
@@ -85,10 +86,7 @@ describe("sibling cards: `read:` is required, from the package's own schema", ()
85
86
  "papers/a/siblings/jones2024.md": "---\nread: abstract\n---\n# Jones\n",
86
87
  },
87
88
  );
88
- const results = JSON.parse((await lint(root)).out) as {
89
- filePath: string;
90
- messages: { ruleId: string; severity: number }[];
91
- }[];
89
+ const results = lintReport((await lint(root)).out);
92
90
  const of = (name: string) =>
93
91
  results.find((r) => r.filePath.endsWith(name))?.messages ?? null;
94
92
  expect(of("smith2025.md")?.map((m) => [m.ruleId, m.severity])).toEqual([
@@ -19,6 +19,7 @@ import { tmpdir } from "node:os";
19
19
  import { join, relative } from "node:path";
20
20
  import { afterEach, describe, expect, it } from "vitest";
21
21
  import { run } from "./cli.ts";
22
+ import { lintReport } from "../test/lint-report.ts";
22
23
 
23
24
  const dirs: string[] = [];
24
25
  afterEach(() => {
@@ -91,9 +92,8 @@ async function lintedSet(root: string, args: string[]): Promise<string[]> {
91
92
  log: (s: string) => out.push(s),
92
93
  err: () => {},
93
94
  });
94
- type Result = { filePath: string; messages: { ruleId: string | null }[] };
95
95
  return (
96
- (JSON.parse(out.join("\n")) as Result[])
96
+ lintReport(out.join("\n"))
97
97
  // A structure finding reports a MISSING file in the same shape; it was not linted.
98
98
  .filter(
99
99
  (r) => !r.messages.some((m) => m.ruleId === "structure/required-file"),
@@ -7,9 +7,8 @@
7
7
  * from a missing file into a created one;
8
8
  * - the scorecard carries `researchQuestion` and NO `stages` — a new paper has shipped nothing,
9
9
  * and a placeholder stage would be a red `paper/stages` on the very first run;
10
- * - the scorecard PARSES as a scorecard (`pipeline-check.mjs` finds its four sections). The
11
- * template used to live in a code fence; moved into a file, it must still be the format the
12
- * skills' checker reads;
10
+ * - the scorecard carries the `**Readiness verdict:**` line the status hook surfaces on every
11
+ * paper edit — without it the hook reports that the paper cannot say whether it is ready;
13
12
  * - it never overwrites, and on an existing folder adds only what is missing;
14
13
  * - the project's `<papers>/.template/` wins over the package, file by file.
15
14
  *
@@ -25,13 +24,12 @@ import {
25
24
  rmSync,
26
25
  writeFileSync,
27
26
  } from "node:fs";
28
- import { spawnSync } from "node:child_process";
29
27
  import { tmpdir } from "node:os";
30
28
  import { dirname, join } from "node:path";
31
29
  import { fileURLToPath } from "node:url";
32
30
  import { load } from "js-yaml";
33
- import { frontmatterBlock } from "../lib/markdown.mjs";
34
- import { createChecker } from "../lib/check.mjs";
31
+ import { frontmatterBlock } from "../lib/markdown.ts";
32
+ import { createChecker } from "../lib/check.ts";
35
33
 
36
34
  const HERE = dirname(fileURLToPath(import.meta.url));
37
35
  const ROOT = dirname(HERE);
@@ -124,26 +122,12 @@ try {
124
122
  /1 problem \(0 errors, 1 warning\)/.test(l.text),
125
123
  );
126
124
 
127
- // The format contract of the skills' checker: four sections, parsed.
128
- const pc = spawnSync(
129
- process.execPath,
130
- [
131
- join(ROOT, "skills", "paper-pipeline", "scripts", "pipeline-check.mjs"),
132
- join(papers, "demo"),
133
- "--json",
134
- "--today=2026-09-23",
135
- ],
136
- { cwd: work, encoding: "utf8" },
137
- );
138
- let parsed = null;
139
- try {
140
- parsed = JSON.parse(pc.stdout);
141
- } catch {
142
- parsed = null;
143
- }
125
+ // What the status hook reads on every paper edit: the verdict line.
144
126
  check(
145
- "the scorecard PARSES for pipeline-check — its four sections are found (it prints JSON, not the format warning)",
146
- Array.isArray(parsed) && !/expected the four sections/.test(pc.stderr),
127
+ "the scorecard carries the `**Readiness verdict:**` line the status hook surfaces",
128
+ /^\*\*Readiness verdict:\*\*/m.test(
129
+ readFileSync(join(papers, "demo", STATUS_FILE), "utf8"),
130
+ ),
147
131
  );
148
132
 
149
133
  // ── never overwrite; on an existing folder only the missing files ─────────────────────
@@ -20,6 +20,8 @@ import { parsePaperSettings } from "./paper-settings.ts";
20
20
  import { chooseVenue, run } from "./cli.ts";
21
21
  import { shippedPresets } from "./presets.ts";
22
22
  import { packageVenuesDir } from "../skills/paper-pipeline/scripts/consumer.mjs";
23
+ import { lintReport } from "../test/lint-report.ts";
24
+ import { z } from "zod";
23
25
 
24
26
  const dirs: string[] = [];
25
27
  afterEach(() => {
@@ -31,10 +33,9 @@ const tmp = () => {
31
33
  return d;
32
34
  };
33
35
  const settingsOf = (dir: string) =>
34
- JSON.parse(readFileSync(join(dir, "paperlint.json"), "utf8")) as Record<
35
- string,
36
- unknown
37
- >;
36
+ z
37
+ .record(z.string(), z.unknown())
38
+ .parse(JSON.parse(readFileSync(join(dir, "paperlint.json"), "utf8")));
38
39
 
39
40
  describe("paperlint new — paperlint.json", () => {
40
41
  it("is always written, from the package template: no venue chosen yet, and valid", () => {
@@ -107,11 +108,7 @@ describe("paperlint lint — a paper with no venue preset chosen", () => {
107
108
  log: (s: string) => out.push(s),
108
109
  err: () => {},
109
110
  });
110
- const messages = (
111
- JSON.parse(out.join("\n")) as {
112
- messages: { ruleId: string; severity: number; message: string }[];
113
- }[]
114
- ).flatMap((r) => r.messages);
111
+ const messages = lintReport(out.join("\n")).flatMap((r) => r.messages);
115
112
  return { code, messages };
116
113
  }
117
114
 
@@ -266,7 +263,8 @@ describe("chooseVenue — on a terminal", () => {
266
263
  paperDir: "/p/papers/demo",
267
264
  cwd: "/p",
268
265
  interactive: true,
269
- ask: async (q: string) => (asked.push(q), answers.shift() ?? ""),
266
+ ask: (q: string) =>
267
+ Promise.resolve((asked.push(q), answers.shift() ?? "")),
270
268
  },
271
269
  };
272
270
  };
package/src/new-paper.ts CHANGED
@@ -25,7 +25,8 @@ import {
25
25
  /* eslint-enable boundaries/dependencies */
26
26
  import { join } from "node:path";
27
27
  import { fileURLToPath } from "node:url";
28
- import { CONFIG_FILE } from "../lib/paper-config.mjs";
28
+ import { CONFIG_FILE } from "#lib/paper-config";
29
+ import { isRecord } from "./domain/record.ts";
29
30
 
30
31
  export type PaperFormat = "tex" | "md";
31
32
  export const FORMATS: readonly PaperFormat[] = ["tex", "md"];
@@ -50,7 +51,7 @@ export const SOURCE_FILE: Readonly<Record<PaperFormat, string>> = {
50
51
  };
51
52
 
52
53
  export const isFormat = (v: unknown): v is PaperFormat =>
53
- FORMATS.includes(v as PaperFormat);
54
+ FORMATS.some((f) => f === v);
54
55
 
55
56
  /**
56
57
  * Why a name is refused, or null. `[a-z0-9._-]+` is the charset; a LEADING dot is refused on top
@@ -115,15 +116,18 @@ function withVenue(
115
116
  src: string,
116
117
  venue: VenueSetting,
117
118
  ): { ok: true; text: string } | { ok: false; reason: string } {
118
- let obj: Record<string, unknown>;
119
+ let obj: unknown;
119
120
  try {
120
- obj = JSON.parse(text) as Record<string, unknown>;
121
+ obj = JSON.parse(text);
121
122
  } catch {
123
+ obj = null;
124
+ }
125
+ // Not JSON, or JSON that is not an object: there is no field to write "extends" into.
126
+ if (!isRecord(obj))
122
127
  return {
123
128
  ok: false,
124
129
  reason: `--venue cannot be written into ${src}: it is not plain JSON — set "extends" by hand`,
125
130
  };
126
- }
127
131
  const out: Record<string, unknown> = { ...obj, extends: venue.extends };
128
132
  delete out["$comment"];
129
133
  if (venue.kind !== null) out["kind"] = venue.kind;
@@ -8,7 +8,7 @@
8
8
  * 74 errors — parse errors and "Definition for rule … was not found" — none of them on a paper
9
9
  * file, and the run failed. Nothing in the settings could turn that off.
10
10
  */
11
- import { ESLint, type Linter } from "eslint";
11
+ import { ESLint } from "eslint";
12
12
  import {
13
13
  mkdirSync,
14
14
  mkdtempSync,
@@ -19,7 +19,7 @@ import {
19
19
  import { tmpdir } from "node:os";
20
20
  import { join } from "node:path";
21
21
  import { afterEach, describe, expect, it } from "vitest";
22
- import { buildConfig, run } from "./cli.ts";
22
+ import { buildConfig, eslintConfig, run } from "./cli.ts";
23
23
  import {
24
24
  narrowToOwners,
25
25
  ownedPatterns,
@@ -27,6 +27,11 @@ import {
27
27
  ruleOwners,
28
28
  scopeToOwned,
29
29
  } from "./paper-files.ts";
30
+ import { lintReport } from "../test/lint-report.ts";
31
+ import { z } from "zod";
32
+
33
+ /** What `calculateConfigForFile` answers for a file inside the config's scope, read for its rules. */
34
+ const Computed = z.object({ rules: z.record(z.string(), z.unknown()) });
30
35
 
31
36
  const dirs: string[] = [];
32
37
  afterEach(() => {
@@ -67,7 +72,7 @@ async function lint(
67
72
  }
68
73
 
69
74
  const linted = (stdout: string): string[] =>
70
- (JSON.parse(stdout) as { filePath: string }[]).map((r) => r.filePath);
75
+ lintReport(stdout).map((r) => r.filePath);
71
76
 
72
77
  /** Vendored code under a paper: a syntax error, an unknown rule in a directive, plain CommonJS. */
73
78
  const VENDORED = {
@@ -208,10 +213,7 @@ describe("🔴 a generated block never reaches a file paperlint does not lint (#
208
213
  async (_, settings) => {
209
214
  const root = project({ ...SIBLINGS, ...settings });
210
215
  const r = await lint(root);
211
- const results = JSON.parse(r.out) as {
212
- filePath: string;
213
- messages: { ruleId: string | null; message: string }[];
214
- }[];
216
+ const results = lintReport(r.out);
215
217
  // Guards: the class — no file reaches ESLint's default JavaScript parser.
216
218
  expect(
217
219
  results.flatMap((x) => x.messages).filter((m) => m.ruleId === null),
@@ -234,25 +236,24 @@ describe("`rules` blocks — where the rule lands", () => {
234
236
  const eslint = new ESLint({
235
237
  cwd: root,
236
238
  overrideConfigFile: true,
237
- overrideConfig: buildConfig(
238
- {
239
- rules: [
240
- {
241
- basePath: root,
242
- files: ["papers/**"],
243
- rules: { "paper/source": "warn", "paper/section-word": "off" },
244
- },
245
- ],
246
- },
247
- null,
248
- ) as Linter.Config[],
239
+ overrideConfig: eslintConfig(
240
+ buildConfig(
241
+ {
242
+ rules: [
243
+ {
244
+ basePath: root,
245
+ files: ["papers/**"],
246
+ rules: { "paper/source": "warn", "paper/section-word": "off" },
247
+ },
248
+ ],
249
+ },
250
+ null,
251
+ ),
252
+ ),
249
253
  });
250
254
  const on = async (file: string) =>
251
- (
252
- (await eslint.calculateConfigForFile(join(root, file))) as {
253
- rules: Record<string, unknown>;
254
- }
255
- ).rules;
255
+ Computed.parse(await eslint.calculateConfigForFile(join(root, file)))
256
+ .rules;
256
257
  const status = await on("papers/a/PIPELINE-STATUS.md");
257
258
  expect(status["paper/source"]).toEqual([1]);
258
259
  expect(status["paper/section-word"]).toBeUndefined();
@@ -40,16 +40,7 @@
40
40
  * - later global-ignore patterns win, across blocks, in config order;
41
41
  * - `files: [["a", "b"]]` matches a file only when BOTH patterns match.
42
42
  */
43
- import type { RuleBlock, RuleEntry } from "./rules-config.ts";
44
-
45
- /** What this module reads of a config block. */
46
- interface Block {
47
- readonly files?: readonly unknown[];
48
- readonly ignores?: readonly unknown[];
49
- readonly plugins?: Readonly<
50
- Record<string, { readonly rules?: Readonly<Record<string, unknown>> }>
51
- >;
52
- }
43
+ import type { ConfigBlock, RuleBlock, RuleEntry } from "./rules-config.ts";
53
44
 
54
45
  /**
55
46
  * A set of files paperlint lints: an own block's globs and the exceptions that block makes. A file is
@@ -99,20 +90,18 @@ const merged = (scopes: readonly OwnedScope[]): OwnedScope[] => {
99
90
  * THE FILES PAPERLINT LINTS: every own block that names `files`, with its `ignores`. A block without
100
91
  * `files` (a global ignore, or the block registering `pdf` for every file) claims nothing.
101
92
  */
102
- export function ownedScopes(own: readonly unknown[]): OwnedScope[] {
93
+ export function ownedScopes(own: readonly ConfigBlock[]): OwnedScope[] {
103
94
  return merged(
104
- own
105
- .map((raw) => raw as Block)
106
- .flatMap((b) =>
107
- b.files === undefined
108
- ? []
109
- : [{ files: globsOf(b.files), ignores: stringsOf(b.ignores) }],
110
- ),
95
+ own.flatMap((b) =>
96
+ b.files === undefined
97
+ ? []
98
+ : [{ files: globsOf(b.files), ignores: stringsOf(b.ignores) }],
99
+ ),
111
100
  );
112
101
  }
113
102
 
114
103
  /** Every glob paperlint's own blocks lint, once, in config order. */
115
- export function ownedPatterns(own: readonly unknown[]): string[] {
104
+ export function ownedPatterns(own: readonly ConfigBlock[]): string[] {
116
105
  return [...new Set(ownedScopes(own).flatMap((s) => s.files))];
117
106
  }
118
107
 
@@ -134,7 +123,7 @@ export function scopeToOwned(owned: readonly string[]): {
134
123
  }
135
124
 
136
125
  /** Every `<plugin>/<rule>` a block registers. */
137
- const ruleIdsOf = (b: Block): string[] =>
126
+ const ruleIdsOf = (b: ConfigBlock): string[] =>
138
127
  Object.entries(b.plugins ?? {}).flatMap(([name, plugin]) =>
139
128
  Object.keys(plugin.rules ?? {}).map((rule) => `${name}/${rule}`),
140
129
  );
@@ -145,12 +134,11 @@ const ruleIdsOf = (b: Block): string[] =>
145
134
  * one file more.
146
135
  */
147
136
  export function ruleOwners(
148
- own: readonly unknown[],
137
+ own: readonly ConfigBlock[],
149
138
  ): Map<string, readonly OwnedScope[]> {
150
139
  const all = ownedScopes(own);
151
140
  const found = new Map<string, OwnedScope[]>();
152
- for (const raw of own) {
153
- const b = raw as Block;
141
+ for (const b of own) {
154
142
  const scopes =
155
143
  b.files === undefined
156
144
  ? all
@@ -171,17 +159,18 @@ export function narrowToOwners(
171
159
  block: RuleBlock,
172
160
  owners: ReadonlyMap<string, readonly OwnedScope[]>,
173
161
  ): ScopedBlock[] {
162
+ // Keyed by the owner's JSON, so rules with equal owners share one group of blocks.
174
163
  const groups = new Map<string, Record<string, RuleEntry>>();
164
+ const ownerOf = new Map<string, readonly OwnedScope[]>();
175
165
  for (const [id, entry] of Object.entries(block.rules)) {
176
166
  const owner = owners.get(id);
177
167
  if (owner === undefined) continue;
178
168
  const key = JSON.stringify(owner);
179
169
  groups.set(key, { ...groups.get(key), [id]: entry });
170
+ ownerOf.set(key, owner);
180
171
  }
181
- return [...groups].flatMap(([key, rules]) =>
182
- (JSON.parse(key) as OwnedScope[]).map((scope) =>
183
- within(block, scope, rules),
184
- ),
172
+ return [...ownerOf].flatMap(([key, owner]) =>
173
+ owner.map((scope) => within(block, scope, { ...groups.get(key) })),
185
174
  );
186
175
  }
187
176
 
@@ -13,6 +13,7 @@ import { tmpdir } from "node:os";
13
13
  import { join } from "node:path";
14
14
  import { afterEach, describe, expect, it } from "vitest";
15
15
  import { run, toolchainTex } from "./cli.ts";
16
+ import { lintReport } from "../test/lint-report.ts";
16
17
 
17
18
  const dirs: string[] = [];
18
19
  afterEach(() => {
@@ -55,9 +56,9 @@ async function lint(
55
56
  }
56
57
 
57
58
  const rulesIn = (stdout: string, paper: string): string[] =>
58
- (JSON.parse(stdout) as { filePath: string; messages: { ruleId: string }[] }[])
59
+ lintReport(stdout)
59
60
  .filter((r) => r.filePath.endsWith(join(paper, "paper.tex")))
60
- .flatMap((r) => r.messages.map((m) => m.ruleId));
61
+ .flatMap((r) => r.messages.map((m) => m.ruleId ?? "(fatal)"));
61
62
 
62
63
  describe("🔴 an optional rule that reaches no paper — judged against the PROJECT, not the run (#103)", () => {
63
64
  const THIRD = {
@@ -151,7 +152,7 @@ describe("papersDir — optional, `papers` by default", () => {
151
152
  const r = await lint(project());
152
153
  expect(r.code).toBe(0);
153
154
  expect(
154
- (JSON.parse(r.out) as { filePath: string }[]).map((f) =>
155
+ lintReport(r.out).map((f) =>
155
156
  f.filePath.slice(f.filePath.indexOf("papers/")),
156
157
  ),
157
158
  ).toEqual(
@@ -178,7 +179,7 @@ describe("papersDir — optional, `papers` by default", () => {
178
179
  "docs/drafts/c/PIPELINE-STATUS.md": "---\nstages: []\n---\n",
179
180
  });
180
181
  const r = await lint(root);
181
- const linted = (JSON.parse(r.out) as { filePath: string }[]).map((f) =>
182
+ const linted = lintReport(r.out).map((f) =>
182
183
  f.filePath.slice(root.length + 1),
183
184
  );
184
185
  expect(linted).toContain("docs/drafts/c/paper.tex");
@@ -4,8 +4,8 @@
4
4
  */
5
5
  import { describe, expect, it } from "vitest";
6
6
  import { memoryFiles } from "./adapters/memory/index.ts";
7
- import type { AbsolutePath } from "./domain/paths.ts";
8
- import { findProjectRoot } from "../lib/paper-config.mjs";
7
+ import { absolutePath } from "./domain/paths.ts";
8
+ import { findProjectRoot } from "../lib/paper-config.ts";
9
9
  import {
10
10
  paperRules,
11
11
  parsePaperSettings,
@@ -128,12 +128,12 @@ describe("readPaperSettings — a root paperlint.json that cannot be used", () =
128
128
  readPaperSettings(withFiles({ [`${ROOT}/paperlint.json`]: root }), PAPER);
129
129
  const notJson = broken("{ nope");
130
130
  expect(!notJson.ok && notJson.error.kind).toBe("broken");
131
+ const why: unknown = expect.stringMatching(
132
+ /^the root paperlint\.json: .*kind/,
133
+ );
131
134
  expect(broken('{"kind":3}')).toEqual({
132
135
  ok: false,
133
- error: {
134
- kind: "broken",
135
- why: expect.stringMatching(/^the root paperlint\.json: .*kind/),
136
- },
136
+ error: { kind: "broken", why },
137
137
  });
138
138
  });
139
139
  });
@@ -215,7 +215,7 @@ describe("findProjectRoot — where the root paperlint.json is looked for", () =
215
215
  [`${PAPER}/paperlint.json`]: '{"kind":"research"}',
216
216
  [`${ROOT}/paperlint.json`]: '{"extends":"paperlint:aisec"}',
217
217
  });
218
- const isFile = (p: string) => files.isFile(p as AbsolutePath);
218
+ const isFile = (p: string) => files.isFile(absolutePath(p));
219
219
  expect(findProjectRoot(PAPER, isFile)).toBe(ROOT);
220
220
  // No root file anywhere: the package.json directory.
221
221
  expect(findProjectRoot(PAPER, (p) => p === `${ROOT}/package.json`)).toBe(