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
@@ -0,0 +1,12 @@
1
+ /** Reading fields off a value typed `unknown` — a parsed JSON file, an object a library hands back. */
2
+
3
+ /** Any non-null object that is not an array, readable by key. */
4
+ export const isRecord = (v: unknown): v is Readonly<Record<string, unknown>> =>
5
+ typeof v === "object" && v !== null && !Array.isArray(v);
6
+
7
+ /** `v[key]` when `v` is a record; undefined for anything else. Inherited fields count, as `.` reads them. */
8
+ export const fieldOf = (v: unknown, key: string): unknown =>
9
+ isRecord(v) ? v[key] : undefined;
10
+
11
+ /** A number array a library types as `any[]`, read as the numbers it holds. */
12
+ export const numbersOf = (xs: readonly unknown[]): number[] => xs.map(Number);
@@ -3,7 +3,7 @@ import { test } from "vitest";
3
3
  import { andThen, err, map, match, ok, type Result } from "./result.ts";
4
4
 
5
5
  const half = (n: number): Result<number, string> =>
6
- n % 2 === 0 ? ok(n / 2) : err(`${n} is odd`);
6
+ n % 2 === 0 ? ok(n / 2) : err(`${String(n)} is odd`);
7
7
 
8
8
  test("map and andThen run on ok and pass an error through untouched", () => {
9
9
  assert.deepEqual(
@@ -24,7 +24,7 @@ test("map and andThen run on ok and pass an error through untouched", () => {
24
24
 
25
25
  test("match picks the branch by `ok`", () => {
26
26
  const say = (r: Result<number, string>) =>
27
- match(r, { ok: (n) => `got ${n}`, err: (e) => `no: ${e}` });
27
+ match(r, { ok: (n) => `got ${String(n)}`, err: (e) => `no: ${e}` });
28
28
  assert.equal(say(half(4)), "got 2");
29
29
  assert.equal(say(half(5)), "no: 5 is odd");
30
30
  });
@@ -9,13 +9,15 @@ import type { Opaque } from "ts-essentials";
9
9
  export type Sha256 = Opaque<string, "Sha256">;
10
10
 
11
11
  const SHA256 = /^[0-9a-f]{64}$/;
12
+ // The brand's one owner: the predicate IS the check, so no assertion is needed to mint the type.
13
+ const isSha256 = (s: string): s is Sha256 => SHA256.test(s);
12
14
 
13
15
  /** A sha256 written down as text — a pin, an option — checked where it is written, not where it is used. */
14
16
  export function parseSha256(s: string): Sha256 {
15
- if (!SHA256.test(s))
17
+ if (!isSha256(s))
16
18
  throw new Error(`not a sha256 (64 lowercase hex digits): ${s}`);
17
- return s as Sha256;
19
+ return s;
18
20
  }
19
21
 
20
22
  export const sha256Hex = (bytes: Uint8Array): Sha256 =>
21
- createHash("sha256").update(bytes).digest("hex") as Sha256;
23
+ parseSha256(createHash("sha256").update(bytes).digest("hex"));
@@ -0,0 +1,32 @@
1
+ import assert from "node:assert/strict";
2
+ import { test } from "vitest";
3
+ import { codeOf, firstLine, messageOf, printed } from "./text.ts";
4
+
5
+ test("messageOf: an Error's message, and anything else thrown as its text", () => {
6
+ assert.equal(messageOf(new Error("boom")), "boom");
7
+ // `throw "x"` and `throw 3` are legal JavaScript; `(e as Error).message` read them as undefined.
8
+ assert.equal(messageOf("plain string"), "plain string");
9
+ assert.equal(messageOf(3), "3");
10
+ });
11
+
12
+ test("codeOf: a string `code` only", () => {
13
+ assert.equal(
14
+ codeOf(Object.assign(new Error("x"), { code: "ENOENT" })),
15
+ "ENOENT",
16
+ );
17
+ assert.equal(codeOf(new Error("no code")), undefined);
18
+ assert.equal(codeOf({ code: 3 }), undefined);
19
+ assert.equal(codeOf(null), undefined);
20
+ assert.equal(codeOf("ENOENT"), undefined);
21
+ });
22
+
23
+ test("printed: a stream a failed spawn left undefined reads as empty text", () => {
24
+ assert.equal(printed(undefined), "");
25
+ assert.equal(printed(null), "");
26
+ assert.equal(printed("out"), "out");
27
+ });
28
+
29
+ test("firstLine: the first non-blank line, trimmed", () => {
30
+ assert.equal(firstLine("\n \n first \nsecond"), "first");
31
+ assert.equal(firstLine(""), "");
32
+ });
@@ -9,3 +9,26 @@ export const firstLine = (s: string): string =>
9
9
  .split("\n")
10
10
  .map((l) => l.trim())
11
11
  .find(Boolean) ?? "";
12
+
13
+ /**
14
+ * What a process printed on one stream, as text. Node types `spawnSync(…, { encoding })`'s
15
+ * `stdout`/`stderr` as `string`, but both are `undefined` when the spawn itself failed (measured:
16
+ * `spawnSync("no-such-program", [], { encoding: "utf8" }).stdout` is `undefined`). The parameter
17
+ * takes the stream as it really arrives, so the fallback guards a real case instead of reading
18
+ * as dead code against a type that lies.
19
+ */
20
+ export const printed = (stream: string | null | undefined): string =>
21
+ stream ?? "";
22
+
23
+ /** What a caught value says: an `Error`'s message, anything else as text (`catch` gets `unknown`). */
24
+ export const messageOf = (e: unknown): string =>
25
+ e instanceof Error ? e.message : String(e);
26
+
27
+ /** The `code` a Node error carries (`ENOENT`, `EACCES`, …), when it carries a string one. */
28
+ export const codeOf = (e: unknown): string | undefined =>
29
+ typeof e === "object" &&
30
+ e !== null &&
31
+ "code" in e &&
32
+ typeof e.code === "string"
33
+ ? e.code
34
+ : undefined;
@@ -5,7 +5,7 @@
5
5
  * 1. `resolveEngine` over `EngineFacts` — one row per branch of the order;
6
6
  * 2. the fact readers: `missingPackages` over kpsewhich output, `missingTools`, `whichOnPath`,
7
7
  * `probeTree` through a fake runner.
8
- * All pure or port-driven: no TeX needed. The real-TeX half is `test/e2e/build.mjs`.
8
+ * All pure or port-driven: no TeX needed. The real-TeX half is `test/e2e/build.ts`.
9
9
  */
10
10
  import {
11
11
  chmodSync,
@@ -18,7 +18,7 @@ import {
18
18
  import { tmpdir } from "node:os";
19
19
  import { dirname, join } from "node:path";
20
20
  import { fileURLToPath } from "node:url";
21
- import { createChecker } from "../lib/check.mjs";
21
+ import { createChecker } from "../lib/check.ts";
22
22
 
23
23
  const HERE = dirname(fileURLToPath(import.meta.url));
24
24
  const {
package/src/engine.ts CHANGED
@@ -24,9 +24,38 @@ import { spawnSync } from "node:child_process";
24
24
  import { accessSync, constants, statSync } from "node:fs";
25
25
  import { basename, delimiter, join } from "node:path";
26
26
  import type { TexRequirements } from "./tex-requirements.ts";
27
+ import { printed } from "./domain/text.ts";
27
28
 
28
- /** A process runner with `spawnSync`'s shape — the port the harness replaces. */
29
- export type Runner = typeof spawnSync;
29
+ /** The options paperlint hands a process runner: text out, both streams captured. */
30
+ export interface RunOptions {
31
+ readonly encoding: "utf8" | "latin1";
32
+ readonly stdio: ["ignore", "pipe", "pipe"];
33
+ readonly env?: NodeJS.ProcessEnv;
34
+ readonly cwd?: string;
35
+ readonly maxBuffer?: number;
36
+ readonly timeout?: number;
37
+ }
38
+
39
+ /**
40
+ * What paperlint reads of a finished process. `spawnSync`'s result has this shape; a fake needs
41
+ * nothing more. Both streams are optional: they are `undefined` when the spawn itself failed.
42
+ */
43
+ export interface RunResult {
44
+ readonly status: number | null;
45
+ readonly stdout?: string;
46
+ readonly stderr?: string;
47
+ readonly error?: Error;
48
+ }
49
+
50
+ /**
51
+ * A process runner: `spawnSync`'s text overload, narrowed to what paperlint passes and reads — the
52
+ * port the harness replaces. `spawnSync` itself is one.
53
+ */
54
+ export type Runner = (
55
+ file: string,
56
+ args: readonly string[],
57
+ options: RunOptions,
58
+ ) => RunResult;
30
59
 
31
60
  /** One TeX Live and what it lacks for the papers at hand. */
32
61
  export interface TreeProbe {
@@ -161,7 +190,7 @@ export function probeTree(
161
190
  stdio: ["ignore", "pipe", "pipe"],
162
191
  });
163
192
  if (r.error) return Object.keys(packages).sort();
164
- return missingPackages(packages, String(r.stdout ?? ""));
193
+ return missingPackages(packages, printed(r.stdout));
165
194
  }
166
195
 
167
196
  /**
@@ -181,7 +210,7 @@ export function missingDependencies(
181
210
  stdio: ["ignore", "pipe", "pipe"],
182
211
  });
183
212
  if (r.error) return [];
184
- const lines = String(r.stdout ?? "").split("\n");
213
+ const lines = printed(r.stdout).split("\n");
185
214
  const start = lines.findIndex(
186
215
  (l) => l.trim() === "DEPENDS WITHOUT PACKAGES:",
187
216
  );
@@ -193,7 +222,7 @@ export function missingDependencies(
193
222
  return [
194
223
  ...new Set(
195
224
  (end < 0 ? section : section.slice(0, end)).map((l) =>
196
- l.split(" in: ")[0]!.trim(),
225
+ l.replace(/ in: [\s\S]*/, "").trim(),
197
226
  ),
198
227
  ),
199
228
  ].sort();
@@ -4,8 +4,8 @@
4
4
  * The document is assembled from hand-made measurements (pure), and `measurePaper` runs on
5
5
  * in-memory files with a FAKE pdf.js reader and a FAKE measurer, so each outcome — read failed, not
6
6
  * measured, measured — is chosen rather than hoped for. Reading real PDFs is
7
- * `pdf-facts.harness.mjs`'s job; banal as the measurer is `adapters/banal/index.test.ts`, and the
8
- * real banal is `test/e2e/banal.mjs`.
7
+ * `pdf-facts.harness.ts`'s job; banal as the measurer is `adapters/banal/index.test.ts`, and the
8
+ * real banal is `test/e2e/banal.ts`.
9
9
  */
10
10
  import assert from "node:assert/strict";
11
11
  import { readdirSync, readFileSync } from "node:fs";
@@ -147,10 +147,11 @@ test("🔴 geometry_source and the nine columns come from ONE branch", () => {
147
147
  const PAPER = "/r/papers/p";
148
148
  const PDF = `${PAPER}/paper.pdf`;
149
149
  const PDF_BYTES = "%PDF-pretend";
150
- const good: PdfReader = async () => ({
151
- ok: true,
152
- facts: { pages: 2, fonts: FONTS, last: lastPage(60, 60), layout: [] },
153
- });
150
+ const good: PdfReader = () =>
151
+ Promise.resolve({
152
+ ok: true,
153
+ facts: { pages: 2, fonts: FONTS, last: lastPage(60, 60), layout: [] },
154
+ });
154
155
  /** The shipped presets at their real paths: `extends` resolves through the same Files port. */
155
156
  const SHIPPED_PRESETS = Object.fromEntries(
156
157
  readdirSync(packageVenuesDir())
@@ -232,7 +233,8 @@ test("a failed read is the one error line, and nothing is written", async () =>
232
233
  const before = files.map.size;
233
234
  const m = await measurePaper(PAPER, PDF, {
234
235
  ...o,
235
- readPdf: async () => ({ ok: false, reason: "unreadable", detail: "boom" }),
236
+ readPdf: () =>
237
+ Promise.resolve({ ok: false, reason: "unreadable", detail: "boom" }),
236
238
  });
237
239
  assert.match(!m.ok ? m.error : "", /pdf\.js \(unreadable\): boom/);
238
240
  assert.equal(files.map.size, before);
package/src/facts-file.ts CHANGED
@@ -45,11 +45,12 @@ import {
45
45
  } from "./domain/geometry.ts";
46
46
  import { parseSha256, sha256Hex, type Sha256 } from "./domain/sha256.ts";
47
47
  import type { MeasureGeometry } from "./ports/measure-geometry.ts";
48
- import type { AbsolutePath } from "./domain/paths.ts";
48
+ import { callerPath } from "./caller-path.ts";
49
49
  import type { Files } from "./ports/files.ts";
50
50
  import { err, ok, type Result } from "./domain/result.ts";
51
51
  import { paperPreset, paperPresetProblem } from "./presets.ts";
52
52
  import { packageVenuesDir } from "../skills/paper-pipeline/scripts/consumer.mjs";
53
+ import { messageOf } from "./domain/text.ts";
53
54
 
54
55
  export const FACTS_SCHEMA = 2;
55
56
  export const FACTS_DIR = "_build";
@@ -59,12 +60,6 @@ export const FACTS_FILE = "paper.facts.json";
59
60
  export const factsPath = (paperDir: string): string =>
60
61
  join(paperDir, FACTS_DIR, FACTS_FILE);
61
62
 
62
- /**
63
- * A path as the caller gave it. The `Files` adapter resolves a relative one against the process's
64
- * cwd, which is what these callers have always meant; the brand is not a claim this code checked it.
65
- */
66
- const at = (p: string): AbsolutePath => p as AbsolutePath;
67
-
68
63
  /** What the facts need from a paper's `paperlint.json`: its venue's label, its kind, where its PDF is. */
69
64
  export interface VenueDecl {
70
65
  /** The venue preset's display label (`agenticdev`), or null when the paper extends none. */
@@ -212,7 +207,7 @@ export async function measurePaper(
212
207
  ): Promise<Result<Measured, string>> {
213
208
  const r = await o.readPdf(pdf);
214
209
  if (!r.ok) return err(describeFailure(r, pdf));
215
- const bytes = o.files.readBytes(at(pdf));
210
+ const bytes = o.files.readBytes(callerPath(pdf));
216
211
  if (bytes === null) return err(`${pdf}: gone after pdf.js read it`);
217
212
  const geometry = o.measure.measure(r.facts.layout);
218
213
  const decl = declaredVenue(o.files, paperDir);
@@ -238,7 +233,7 @@ export function writeFactsFile(
238
233
  ): string {
239
234
  const out = factsPath(paperDir);
240
235
  files.writeAtomic(
241
- at(out),
236
+ callerPath(out),
242
237
  new TextEncoder().encode(`${JSON.stringify(facts, null, 2)}\n`),
243
238
  );
244
239
  return out;
@@ -287,6 +282,34 @@ const MAYBE_NUMBERS = [
287
282
  ] as const;
288
283
  const NUMBERS = ["body_pages", "ref_pages", "appendix_pages"] as const;
289
284
 
285
+ /** The geometry columns as the file holds them once checked. */
286
+ type GeometryColumns = {
287
+ readonly [K in (typeof MAYBE_NUMBERS)[number]]: number | null;
288
+ } & { readonly [K in (typeof NUMBERS)[number]]: number } & {
289
+ readonly pages_by_type: Readonly<Record<string, number>>;
290
+ };
291
+
292
+ const isCounts = (v: unknown): v is Readonly<Record<string, number>> =>
293
+ isRecord(v) && Object.values(v).every(isNum);
294
+
295
+ /** Every geometry column has its type: the check the type claims, and nothing else. */
296
+ const hasGeometryColumns = (
297
+ d: Readonly<Record<string, unknown>>,
298
+ ): d is Readonly<Record<string, unknown>> & GeometryColumns =>
299
+ MAYBE_NUMBERS.every((k) => numOrNull(d[k])) &&
300
+ NUMBERS.every((k) => isNum(d[k])) &&
301
+ isCounts(d["pages_by_type"]);
302
+
303
+ /** Which geometry column is wrong, for a record `hasGeometryColumns` refused. */
304
+ function geometryError(d: Readonly<Record<string, unknown>>): string {
305
+ const bad =
306
+ MAYBE_NUMBERS.find((k) => !numOrNull(d[k])) ??
307
+ NUMBERS.find((k) => !isNum(d[k]));
308
+ return bad
309
+ ? `\`${bad}\` is not a number`
310
+ : "`pages_by_type` is not an object of page counts";
311
+ }
312
+
290
313
  /** The geometry columns when a measurer ran; the shape error otherwise. */
291
314
  function geometryOf(
292
315
  d: Readonly<Record<string, unknown>>,
@@ -294,24 +317,17 @@ function geometryOf(
294
317
  if (d["geometry_source"] === null) return ok(null);
295
318
  if (typeof d["geometry_source"] !== "string")
296
319
  return err("`geometry_source` is neither a measurer's name nor null");
297
- const bad =
298
- MAYBE_NUMBERS.find((k) => !numOrNull(d[k])) ??
299
- NUMBERS.find((k) => !isNum(d[k]));
300
- if (bad) return err(`\`${bad}\` is not a number`);
301
- const pagesByType = d["pages_by_type"];
302
- if (!isRecord(pagesByType) || !Object.values(pagesByType).every(isNum))
303
- return err("`pages_by_type` is not an object of page counts");
304
- const n = (k: (typeof MAYBE_NUMBERS)[number]) => d[k] as number | null;
320
+ if (!hasGeometryColumns(d)) return err(geometryError(d));
305
321
  return ok({
306
- page_w_in: n("page_w_in"),
307
- page_h_in: n("page_h_in"),
308
- columns: n("columns"),
309
- body_pt: n("body_pt"),
310
- ref_pt: n("ref_pt"),
311
- body_pages: d["body_pages"] as number,
312
- ref_pages: d["ref_pages"] as number,
313
- appendix_pages: d["appendix_pages"] as number,
314
- pages_by_type: pagesByType as Readonly<Record<string, number>>,
322
+ page_w_in: d.page_w_in,
323
+ page_h_in: d.page_h_in,
324
+ columns: d.columns,
325
+ body_pt: d.body_pt,
326
+ ref_pt: d.ref_pt,
327
+ body_pages: d.body_pages,
328
+ ref_pages: d.ref_pages,
329
+ appendix_pages: d.appendix_pages,
330
+ pages_by_type: d.pages_by_type,
315
331
  });
316
332
  }
317
333
 
@@ -338,7 +354,7 @@ export function parseFactsText(text: string): Result<ReadFacts, FactsProblem> {
338
354
  try {
339
355
  d = JSON.parse(text);
340
356
  } catch (e) {
341
- return err({ kind: "broken", why: `not JSON (${(e as Error).message})` });
357
+ return err({ kind: "broken", why: `not JSON (${messageOf(e)})` });
342
358
  }
343
359
  if (!isRecord(d)) return err({ kind: "broken", why: "not a JSON object" });
344
360
  if (d["schema"] !== FACTS_SCHEMA)
@@ -347,7 +363,7 @@ export function parseFactsText(text: string): Result<ReadFacts, FactsProblem> {
347
363
  if (!artifact.ok) return err({ kind: "broken", why: artifact.error });
348
364
  const rawFonts = d["fonts"];
349
365
  const fonts = Array.isArray(rawFonts) ? rawFonts.map(fontOf) : null;
350
- if (fonts === null || fonts.some((f) => f === null))
366
+ if (fonts === null || !fonts.every((f) => f !== null))
351
367
  return err({
352
368
  kind: "broken",
353
369
  why: "`fonts` is not a list of { name, type, embedded, program }",
@@ -356,7 +372,7 @@ export function parseFactsText(text: string): Result<ReadFacts, FactsProblem> {
356
372
  if (!geometry.ok) return err({ kind: "broken", why: geometry.error });
357
373
  return ok({
358
374
  ...artifact.value,
359
- fonts: fonts as FontEntry[],
375
+ fonts,
360
376
  geometry: geometry.value,
361
377
  });
362
378
  }
@@ -25,11 +25,21 @@ import {
25
25
  writeFileSync,
26
26
  } from "node:fs";
27
27
  import { tmpdir } from "node:os";
28
- import { dirname, join } from "node:path";
29
- import { fileURLToPath } from "node:url";
30
- import { createChecker } from "../lib/check.mjs";
28
+ import { join } from "node:path";
29
+ import { createChecker } from "../lib/check.ts";
30
+ import { isDeepStrictEqual } from "node:util";
31
+ import { z } from "zod";
32
+ import type { Merge, Settings } from "./hooks-settings.ts";
33
+
34
+ /** Settings with a PreToolUse entry — the shape the "wired twice" case edits. */
35
+ const PreToolUseSettings = z.looseObject({
36
+ hooks: z.looseObject({
37
+ PreToolUse: z.array(
38
+ z.looseObject({ hooks: z.array(z.looseObject({ command: z.string() })) }),
39
+ ),
40
+ }),
41
+ });
31
42
 
32
- const HERE = dirname(fileURLToPath(import.meta.url));
33
43
  const {
34
44
  hookRun,
35
45
  shippedWiring,
@@ -39,16 +49,17 @@ const {
39
49
  readSettings,
40
50
  MANAGED_BY,
41
51
  SETTINGS_PATH,
42
- } = await import(join(HERE, "hooks-settings.ts"));
52
+ } = await import("./hooks-settings.ts");
43
53
  const { claudeCodeHookProtocol } = await import("vigiles/claude-code");
44
- const merge = (e, c, m) => claudeCodeHookProtocol.mergeRegistrations(e, c, m);
54
+ const merge: Merge = (e, c, m) =>
55
+ claudeCodeHookProtocol.mergeRegistrations(e, c, m);
45
56
 
46
57
  const check = createChecker();
47
58
 
48
59
  // ── the contract with vigiles, checked before anything relies on it ──────────────────────
49
60
  check(
50
61
  "🔴 vigiles/claude-code exports claudeCodeHookProtocol.mergeRegistrations — the merge init calls",
51
- typeof claudeCodeHookProtocol?.mergeRegistrations === "function",
62
+ typeof claudeCodeHookProtocol.mergeRegistrations === "function",
52
63
  );
53
64
 
54
65
  // ── the one source: plugin/hooks/hooks.json ─────────────────────────────────────────────
@@ -82,7 +93,7 @@ check(
82
93
  try {
83
94
  shippedWiring(file);
84
95
  } catch (e) {
85
- thrown = e.message;
96
+ thrown = e instanceof Error ? e.message : String(e);
86
97
  }
87
98
  rmSync(file);
88
99
  check(
@@ -107,9 +118,26 @@ try {
107
118
  threw,
108
119
  );
109
120
 
121
+ const malformed = join(work, "malformed-hooks.json");
122
+ writeFileSync(
123
+ malformed,
124
+ '{"hooks":{"PreToolUse":[{"hooks":[{"type":"command"}]}]}}',
125
+ );
126
+ let why = "";
127
+ try {
128
+ shippedWiring(malformed);
129
+ } catch (e) {
130
+ why = e instanceof Error ? e.message : String(e);
131
+ }
132
+ check(
133
+ "a wiring file whose `hooks` is not Claude Code's shape THROWS, naming the shape",
134
+ why.startsWith(`${malformed}: "hooks" is not a map of event`),
135
+ why,
136
+ );
137
+
110
138
  // ── which spelling runs which hook ─────────────────────────────────────────────────────
111
139
  const ours = `node "\${CLAUDE_PROJECT_DIR}/${MANAGED_BY}" hook paper-edit-guard`;
112
- const cases = [
140
+ const cases: [string, { name: string; ours: boolean } | null][] = [
113
141
  [ours, { name: "paper-edit-guard", ours: true }],
114
142
  [
115
143
  `node "$CLAUDE_PROJECT_DIR/${MANAGED_BY}" hook paper-status-gates`,
@@ -140,7 +168,7 @@ try {
140
168
  JSON.stringify(hookRun(cmd)) === JSON.stringify(want),
141
169
  );
142
170
 
143
- const project = (name, settings) => {
171
+ const project = (name: string, settings?: Settings | string) => {
144
172
  const dir = join(work, name);
145
173
  mkdirSync(join(dir, ".claude"), { recursive: true });
146
174
  if (settings !== undefined)
@@ -152,14 +180,19 @@ try {
152
180
  );
153
181
  return dir;
154
182
  };
155
- const text = (dir) => readFileSync(join(dir, SETTINGS_PATH), "utf8");
183
+ const text = (dir: string) => readFileSync(join(dir, SETTINGS_PATH), "utf8");
184
+ const settingsIn = (dir: string): Settings => {
185
+ const r = readSettings(dir);
186
+ if (r.status === "unparsable") throw new Error(r.reason);
187
+ return r.settings;
188
+ };
156
189
 
157
190
  // ── a fresh project ────────────────────────────────────────────────────────────────────
158
191
  {
159
192
  const dir = join(work, "fresh");
160
193
  mkdirSync(dir);
161
194
  const r = wireHooks(dir, merge, wiring);
162
- const s = JSON.parse(text(dir));
195
+ const s = settingsIn(dir);
163
196
  check(
164
197
  "no settings file → it is created with the three hooks",
165
198
  r.status === "written" &&
@@ -189,14 +222,14 @@ try {
189
222
  },
190
223
  });
191
224
  wireHooks(dir, merge, wiring);
192
- const s = JSON.parse(text(dir));
225
+ const s = settingsIn(dir);
193
226
  check(
194
227
  "🔴 the user's own hook in the SAME matcher survives",
195
228
  JSON.stringify(s).includes("node my-own-lint.mjs"),
196
229
  );
197
230
  check(
198
231
  "and every other key is kept (permissions)",
199
- s.permissions?.allow?.[0] === "Bash(ls:*)",
232
+ isDeepStrictEqual(s["permissions"], { allow: ["Bash(ls:*)"] }),
200
233
  );
201
234
  check(
202
235
  "and ours are there, once each",
@@ -243,11 +276,14 @@ try {
243
276
  );
244
277
  check(
245
278
  "the report names the hook and the command it found",
246
- r.found.length === 1 && r.found[0].name === "paper-edit-guard",
279
+ r.status === "foreign" &&
280
+ r.found.length === 1 &&
281
+ r.found[0]?.name === "paper-edit-guard",
247
282
  );
248
283
  check(
249
284
  "and names the hooks that are not wired in ANY form",
250
- r.missing.join() === "paper-skills-nudge,paper-status-gates",
285
+ r.status === "foreign" &&
286
+ r.missing.join() === "paper-skills-nudge,paper-status-gates",
251
287
  );
252
288
  // The control: without the check, vigiles' merge WOULD add a second copy. This is the
253
289
  // reason the check exists, measured rather than asserted.
@@ -257,7 +293,7 @@ try {
257
293
  );
258
294
  check(
259
295
  "(control) vigiles' merge alone would wire paper-edit-guard TWICE here",
260
- c.ours === 1 && c.other === 1,
296
+ c?.ours === 1 && c.other === 1,
261
297
  );
262
298
  }
263
299
 
@@ -276,7 +312,7 @@ try {
276
312
  }
277
313
 
278
314
  // ── doctor's section ───────────────────────────────────────────────────────────────────
279
- const doc = (settings) => {
315
+ const doc = (settings?: Settings | string) => {
280
316
  const dir = project(`doc-${String(check.count)}`, settings);
281
317
  return doctorHooks(dir, wiring).join("\n");
282
318
  };
@@ -291,8 +327,8 @@ try {
291
327
  "nothing wired → ⚠ not wired, with the remedy",
292
328
  /⚠ not wired — none of the 3 hooks/.test(doc({})),
293
329
  );
294
- const twice = JSON.parse(JSON.stringify(wiredOnce));
295
- twice.hooks.PreToolUse[0].hooks.push({
330
+ const twice = PreToolUseSettings.parse(structuredClone(wiredOnce));
331
+ twice.hooks.PreToolUse[0]?.hooks.push({
296
332
  type: "command",
297
333
  command: "npx paperlint hook paper-edit-guard",
298
334
  });
@@ -30,6 +30,8 @@ import {
30
30
  BIN_FILE,
31
31
  PACKAGE_NAME,
32
32
  } from "../skills/paper-pipeline/scripts/consumer.mjs";
33
+ import { messageOf } from "./domain/text.ts";
34
+ import { fieldOf, isRecord } from "./domain/record.ts";
33
35
 
34
36
  /** The committed, shared settings file — husky's `.husky/` analogue, except git carries it. */
35
37
  export const SETTINGS_PATH = join(".claude", "settings.json");
@@ -133,17 +135,42 @@ export function hookRun(command: string): {
133
135
  export function commandsIn(
134
136
  settings: Settings,
135
137
  ): { readonly event: string; readonly command: string }[] {
136
- const hooks = settings["hooks"];
137
- if (!hooks || typeof hooks !== "object") return [];
138
138
  const out: { event: string; command: string }[] = [];
139
- for (const [event, entries] of Object.entries(hooks as HooksMap))
140
- for (const entry of Array.isArray(entries) ? entries : [])
141
- for (const h of Array.isArray(entry?.hooks) ? entry.hooks : [])
142
- if (typeof h?.command === "string")
143
- out.push({ event, command: h.command });
139
+ for (const [event, entries] of Object.entries(prop(settings["hooks"]) ?? {}))
140
+ for (const entry of listOf(entries))
141
+ for (const h of listOf(prop(entry)?.["hooks"])) {
142
+ const command = prop(h)?.["command"];
143
+ if (typeof command === "string") out.push({ event, command });
144
+ }
144
145
  return out;
145
146
  }
146
147
 
148
+ /** The settings file is the user's: any level may be missing or of the wrong kind. */
149
+ const listOf = (v: unknown): readonly unknown[] => (Array.isArray(v) ? v : []);
150
+ const prop = (v: unknown): Readonly<Record<string, unknown>> | undefined =>
151
+ typeof v === "object" && v !== null
152
+ ? Object.fromEntries(Object.entries(v))
153
+ : undefined;
154
+
155
+ const isHookCommand = (v: unknown): v is HookCommand =>
156
+ typeof fieldOf(v, "type") === "string" &&
157
+ typeof fieldOf(v, "command") === "string";
158
+
159
+ function isHookEntry(v: unknown): v is HookEntry {
160
+ const matcher = fieldOf(v, "matcher");
161
+ const hooks = fieldOf(v, "hooks");
162
+ return (
163
+ ["undefined", "string"].includes(typeof matcher) &&
164
+ Array.isArray(hooks) &&
165
+ hooks.every(isHookCommand)
166
+ );
167
+ }
168
+
169
+ /** A `hooks` map of the shape Claude Code reads: event → entries → commands. */
170
+ const isHooksMap = (v: unknown): v is HooksMap =>
171
+ isRecord(v) &&
172
+ Object.values(v).every((es) => Array.isArray(es) && es.every(isHookEntry));
173
+
147
174
  export interface Wiring {
148
175
  readonly compiled: HooksMap;
149
176
  /** Derived from the commands, in order. Never empty — an empty set throws. */
@@ -155,8 +182,12 @@ export interface Wiring {
155
182
  * check that silently wired nothing would report "✓ wired" over an empty set.
156
183
  */
157
184
  export function shippedWiring(file: string = WIRING_FILE): Wiring {
158
- const json = JSON.parse(readFileSync(file, "utf8")) as { hooks?: HooksMap };
159
- const compiled = json.hooks ?? {};
185
+ const json: unknown = JSON.parse(readFileSync(file, "utf8"));
186
+ const compiled = fieldOf(json, "hooks") ?? {};
187
+ if (!isHooksMap(compiled))
188
+ throw new Error(
189
+ `${file}: "hooks" is not a map of event → [{ matcher?, hooks: [{ type, command }] }]`,
190
+ );
160
191
  const names = commandsIn({ hooks: compiled })
161
192
  .map((c) => hookRun(c.command)?.name)
162
193
  .filter((n): n is string => typeof n === "string");
@@ -195,7 +226,11 @@ export function wiredCounts(
195
226
  const total = (c: HookCount): number => c.ours + c.other;
196
227
 
197
228
  export type SettingsRead =
198
- | { readonly status: "absent"; readonly path: string; readonly settings: {} }
229
+ | {
230
+ readonly status: "absent";
231
+ readonly path: string;
232
+ readonly settings: Settings;
233
+ }
199
234
  | {
200
235
  readonly status: "read";
201
236
  readonly path: string;
@@ -214,11 +249,11 @@ export function readSettings(root: string): SettingsRead {
214
249
  const raw = readFileSync(path, "utf8");
215
250
  try {
216
251
  const parsed: unknown = JSON.parse(raw);
217
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
252
+ if (!isRecord(parsed))
218
253
  return { status: "unparsable", path, reason: "not a JSON object" };
219
- return { status: "read", path, settings: parsed as Settings, raw };
254
+ return { status: "read", path, settings: parsed, raw };
220
255
  } catch (e) {
221
- return { status: "unparsable", path, reason: (e as Error).message };
256
+ return { status: "unparsable", path, reason: messageOf(e) };
222
257
  }
223
258
  }
224
259