paperlint 3.2.0 → 4.1.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 (442) hide show
  1. package/.github/workflows/ci.yml +16 -23
  2. package/CLAUDE.md +33 -13
  3. package/CONTRIBUTING.md +23 -7
  4. package/README.md +11 -8
  5. package/dist/adapters/banal/failure.d.ts +1 -7
  6. package/dist/adapters/banal/failure.d.ts.map +1 -1
  7. package/dist/adapters/banal/failure.js +5 -7
  8. package/dist/adapters/banal/failure.js.map +1 -1
  9. package/dist/adapters/banal/index.d.ts.map +1 -1
  10. package/dist/adapters/banal/index.js +8 -11
  11. package/dist/adapters/banal/index.js.map +1 -1
  12. package/dist/adapters/banal/install.d.ts.map +1 -1
  13. package/dist/adapters/banal/install.js +4 -3
  14. package/dist/adapters/banal/install.js.map +1 -1
  15. package/dist/adapters/banal/invocation.d.ts +10 -5
  16. package/dist/adapters/banal/invocation.d.ts.map +1 -1
  17. package/dist/adapters/banal/invocation.js +13 -1
  18. package/dist/adapters/banal/invocation.js.map +1 -1
  19. package/dist/adapters/banal/locate.d.ts +12 -9
  20. package/dist/adapters/banal/locate.d.ts.map +1 -1
  21. package/dist/adapters/banal/locate.js +19 -12
  22. package/dist/adapters/banal/locate.js.map +1 -1
  23. package/dist/adapters/banal/run.d.ts +10 -1
  24. package/dist/adapters/banal/run.d.ts.map +1 -1
  25. package/dist/adapters/banal/run.js +8 -4
  26. package/dist/adapters/banal/run.js.map +1 -1
  27. package/dist/adapters/banal/settings.d.ts +1 -1
  28. package/dist/adapters/banal/settings.d.ts.map +1 -1
  29. package/dist/adapters/banal/settings.js +2 -1
  30. package/dist/adapters/banal/settings.js.map +1 -1
  31. package/dist/adapters/latex/document-class.d.ts +22 -0
  32. package/dist/adapters/latex/document-class.d.ts.map +1 -0
  33. package/dist/adapters/latex/document-class.js +102 -0
  34. package/dist/adapters/latex/document-class.js.map +1 -0
  35. package/dist/adapters/latex/hidden.d.ts +16 -0
  36. package/dist/adapters/latex/hidden.d.ts.map +1 -0
  37. package/dist/adapters/latex/hidden.js +49 -0
  38. package/dist/adapters/latex/hidden.js.map +1 -0
  39. package/dist/adapters/latex/index.d.ts +29 -0
  40. package/dist/adapters/latex/index.d.ts.map +1 -0
  41. package/dist/adapters/latex/index.js +18 -0
  42. package/dist/adapters/latex/index.js.map +1 -0
  43. package/dist/adapters/latex/nodes.d.ts +54 -0
  44. package/dist/adapters/latex/nodes.d.ts.map +1 -0
  45. package/dist/adapters/latex/nodes.js +93 -0
  46. package/dist/adapters/latex/nodes.js.map +1 -0
  47. package/dist/adapters/latex/outline.d.ts +10 -0
  48. package/dist/adapters/latex/outline.d.ts.map +1 -0
  49. package/dist/adapters/latex/outline.js +39 -0
  50. package/dist/adapters/latex/outline.js.map +1 -0
  51. package/dist/adapters/latex/parse.d.ts +42 -0
  52. package/dist/adapters/latex/parse.d.ts.map +1 -0
  53. package/dist/adapters/latex/parse.js +94 -0
  54. package/dist/adapters/latex/parse.js.map +1 -0
  55. package/dist/adapters/latex/rendered.d.ts +10 -0
  56. package/dist/adapters/latex/rendered.d.ts.map +1 -0
  57. package/dist/adapters/latex/rendered.js +39 -0
  58. package/dist/adapters/latex/rendered.js.map +1 -0
  59. package/dist/adapters/memory/index.d.ts.map +1 -1
  60. package/dist/adapters/memory/index.js +8 -2
  61. package/dist/adapters/memory/index.js.map +1 -1
  62. package/dist/adapters/node/process.io.d.ts +6 -3
  63. package/dist/adapters/node/process.io.d.ts.map +1 -1
  64. package/dist/adapters/node/process.io.js.map +1 -1
  65. package/dist/adapters/node/workspace.io.d.ts.map +1 -1
  66. package/dist/adapters/node/workspace.io.js +4 -2
  67. package/dist/adapters/node/workspace.io.js.map +1 -1
  68. package/dist/adapters/pdfjs/fill.d.ts.map +1 -1
  69. package/dist/adapters/pdfjs/fill.js +12 -12
  70. package/dist/adapters/pdfjs/fill.js.map +1 -1
  71. package/dist/adapters/references/index.d.ts.map +1 -1
  72. package/dist/adapters/references/index.js +4 -2
  73. package/dist/adapters/references/index.js.map +1 -1
  74. package/dist/build-engine.d.ts +1 -1
  75. package/dist/build-engine.d.ts.map +1 -1
  76. package/dist/build-engine.js +8 -1
  77. package/dist/build-engine.js.map +1 -1
  78. package/dist/build.d.ts +2 -3
  79. package/dist/build.d.ts.map +1 -1
  80. package/dist/build.js +9 -10
  81. package/dist/build.js.map +1 -1
  82. package/dist/caller-path.d.ts +3 -0
  83. package/dist/caller-path.d.ts.map +1 -0
  84. package/dist/caller-path.js +13 -0
  85. package/dist/caller-path.js.map +1 -0
  86. package/dist/cli.d.ts +10 -17
  87. package/dist/cli.d.ts.map +1 -1
  88. package/dist/cli.js +91 -17
  89. package/dist/cli.js.map +1 -1
  90. package/dist/doctor.d.ts +12 -3
  91. package/dist/doctor.d.ts.map +1 -1
  92. package/dist/doctor.js +2 -1
  93. package/dist/doctor.js.map +1 -1
  94. package/dist/domain/paths.d.ts +8 -2
  95. package/dist/domain/paths.d.ts.map +1 -1
  96. package/dist/domain/paths.js +12 -1
  97. package/dist/domain/paths.js.map +1 -1
  98. package/dist/domain/ready.d.ts +24 -0
  99. package/dist/domain/ready.d.ts.map +1 -0
  100. package/dist/domain/ready.js +11 -0
  101. package/dist/domain/ready.js.map +1 -0
  102. package/dist/domain/record.d.ts +8 -0
  103. package/dist/domain/record.d.ts.map +1 -0
  104. package/dist/domain/record.js +8 -0
  105. package/dist/domain/record.js.map +1 -0
  106. package/dist/domain/sha256.d.ts.map +1 -1
  107. package/dist/domain/sha256.js +4 -2
  108. package/dist/domain/sha256.js.map +1 -1
  109. package/dist/domain/tex-document.d.ts +84 -0
  110. package/dist/domain/tex-document.d.ts.map +1 -0
  111. package/dist/domain/tex-document.js +43 -0
  112. package/dist/domain/tex-document.js.map +1 -0
  113. package/dist/engine.d.ts +23 -22
  114. package/dist/engine.d.ts.map +1 -1
  115. package/dist/engine.js.map +1 -1
  116. package/dist/eslint-rules/bib-reachable-entry.d.ts +1 -1
  117. package/dist/eslint-rules/bib-reachable-entry.js.map +1 -1
  118. package/dist/eslint-rules/install-path-literals.d.ts +2 -2
  119. package/dist/eslint-rules/install-path-literals.js.map +1 -1
  120. package/dist/eslint-rules/paper-research-question.d.ts +1 -1
  121. package/dist/eslint-rules/paper-research-question.js.map +1 -1
  122. package/dist/eslint-rules/paper-stages.d.ts +2 -2
  123. package/dist/eslint-rules/paper-stages.js.map +1 -1
  124. package/dist/eslint-rules/paper-typography.d.ts +6 -6
  125. package/dist/eslint-rules/paper-typography.js.map +1 -1
  126. package/dist/eslint-rules/pdf-last-page-balance.d.ts +1 -1
  127. package/dist/eslint-rules/pdf-last-page-balance.js.map +1 -1
  128. package/dist/eslint-rules/review-frontmatter.d.ts +2 -2
  129. package/dist/eslint-rules/review-frontmatter.js.map +1 -1
  130. package/dist/eslint-rules/sibling-frontmatter.d.ts +1 -1
  131. package/dist/eslint-rules/sibling-frontmatter.d.ts.map +1 -1
  132. package/dist/eslint-rules/temp-root-realpath.d.ts +1 -1
  133. package/dist/eslint-rules/temp-root-realpath.js.map +1 -1
  134. package/dist/eslint-rules/tex-build.d.ts +2 -2
  135. package/dist/eslint-rules/tex-build.js.map +1 -1
  136. package/dist/facts-file.d.ts.map +1 -1
  137. package/dist/facts-file.js +31 -28
  138. package/dist/facts-file.js.map +1 -1
  139. package/dist/hooks-settings.d.ts.map +1 -1
  140. package/dist/hooks-settings.js +17 -2
  141. package/dist/hooks-settings.js.map +1 -1
  142. package/dist/init.d.ts +3 -4
  143. package/dist/init.d.ts.map +1 -1
  144. package/dist/init.js +18 -8
  145. package/dist/init.js.map +1 -1
  146. package/dist/lib/paper-config.d.ts.map +1 -1
  147. package/dist/lib/paper-config.js +1 -5
  148. package/dist/lib/paper-config.js.map +1 -1
  149. package/dist/new-paper.d.ts +5 -0
  150. package/dist/new-paper.d.ts.map +1 -1
  151. package/dist/new-paper.js +8 -1
  152. package/dist/new-paper.js.map +1 -1
  153. package/dist/package-dirs.d.ts +5 -0
  154. package/dist/package-dirs.d.ts.map +1 -0
  155. package/dist/package-dirs.js +14 -0
  156. package/dist/package-dirs.js.map +1 -0
  157. package/dist/paper-files.d.ts +4 -4
  158. package/dist/paper-files.d.ts.map +1 -1
  159. package/dist/paper-files.js +6 -6
  160. package/dist/paper-files.js.map +1 -1
  161. package/dist/paper-settings.js +3 -3
  162. package/dist/paper-settings.js.map +1 -1
  163. package/dist/pdf-facts.d.ts +51 -1
  164. package/dist/pdf-facts.d.ts.map +1 -1
  165. package/dist/pdf-facts.js +23 -15
  166. package/dist/pdf-facts.js.map +1 -1
  167. package/dist/pdf-geometry.d.ts.map +1 -1
  168. package/dist/pdf-geometry.js +2 -3
  169. package/dist/pdf-geometry.js.map +1 -1
  170. package/dist/ports/latex.d.ts +24 -0
  171. package/dist/ports/latex.d.ts.map +1 -0
  172. package/dist/ports/latex.js +2 -0
  173. package/dist/ports/latex.js.map +1 -0
  174. package/dist/ports/tool-installer.d.ts +2 -14
  175. package/dist/ports/tool-installer.d.ts.map +1 -1
  176. package/dist/presets.d.ts +15 -2
  177. package/dist/presets.d.ts.map +1 -1
  178. package/dist/presets.js +35 -10
  179. package/dist/presets.js.map +1 -1
  180. package/dist/references.d.ts.map +1 -1
  181. package/dist/references.js +4 -4
  182. package/dist/references.js.map +1 -1
  183. package/dist/rules-config.d.ts +26 -1
  184. package/dist/rules-config.d.ts.map +1 -1
  185. package/dist/rules-config.js +9 -9
  186. package/dist/rules-config.js.map +1 -1
  187. package/dist/tex-requirements.d.ts +15 -0
  188. package/dist/tex-requirements.d.ts.map +1 -1
  189. package/dist/tex-requirements.js +28 -10
  190. package/dist/tex-requirements.js.map +1 -1
  191. package/dist/tex-venue-rules.d.ts +83 -0
  192. package/dist/tex-venue-rules.d.ts.map +1 -0
  193. package/dist/tex-venue-rules.js +254 -0
  194. package/dist/tex-venue-rules.js.map +1 -0
  195. package/dist/types.d.ts +5 -3
  196. package/dist/types.d.ts.map +1 -1
  197. package/dist/venue-rules.d.ts +1 -1
  198. package/dist/venue-rules.d.ts.map +1 -1
  199. package/dist/venue-rules.js +20 -7
  200. package/dist/venue-rules.js.map +1 -1
  201. package/docs/configuration.md +5 -4
  202. package/docs/e2e.md +69 -40
  203. package/docs/install.md +1 -1
  204. package/docs/prior-art/README.md +1 -1
  205. package/docs/prior-art/package-location.md +5 -5
  206. package/docs/prior-art/repro/README.md +1 -1
  207. package/docs/prior-art/test-tooling.md +21 -10
  208. package/docs/rules/tex/required-section.md +73 -0
  209. package/docs/rules/tex/template.md +62 -0
  210. package/docs/rules/tex/venue-leftover.md +71 -0
  211. package/docs/rules.md +70 -48
  212. package/docs/testing.md +17 -4
  213. package/docs/texlive-install-decision.md +1 -1
  214. package/docs/toolchain.md +2 -2
  215. package/eslint-rules/bib-reachable-entry.ts +1 -1
  216. package/eslint-rules/install-path-literals.ts +2 -2
  217. package/eslint-rules/paper-research-question.ts +1 -1
  218. package/eslint-rules/paper-stages.ts +2 -2
  219. package/eslint-rules/paper-typography.ts +2 -2
  220. package/eslint-rules/pdf-last-page-balance.ts +1 -1
  221. package/eslint-rules/review-frontmatter.ts +1 -1
  222. package/eslint-rules/temp-root-realpath.ts +1 -1
  223. package/eslint-rules/tex-build.ts +2 -2
  224. package/eslint.config.mjs +162 -15
  225. package/fixtures/build-e2e/acmart/expect.json +26 -0
  226. package/fixtures/build-e2e/aidc/PIPELINE-STATUS.md +3 -0
  227. package/fixtures/build-e2e/aidc/expect.json +14 -0
  228. package/fixtures/build-e2e/aidc/paper.tex +46 -0
  229. package/fixtures/build-e2e/aidc/paperlint.json +1 -0
  230. package/fixtures/build-e2e/broken/expect.json +11 -0
  231. package/fixtures/build-e2e/cite/expect.json +27 -0
  232. package/fixtures/build-e2e/empty/expect.json +12 -0
  233. package/fixtures/build-e2e/fallback/expect.json +53 -0
  234. package/fixtures/build-e2e/guards/expect.json +6 -0
  235. package/fixtures/build-e2e/no-source/expect.json +10 -0
  236. package/fixtures/build-e2e/unbalanced/expect.json +60 -0
  237. package/fixtures/install-path-literals/clean.fixture.mjs +1 -1
  238. package/fixtures/install-path-literals/clean.md +2 -2
  239. package/fixtures/install-path-literals/defect.fixture.mjs +1 -1
  240. package/fixtures/install-path-literals/defect.md +2 -2
  241. package/fixtures/pdf-facts/README.md +1 -1
  242. package/fixtures/real-markdown-paper/baseline.ts +1 -1
  243. package/fixtures/render-paper/build-clean.sh +3 -5
  244. package/hooks/paper-status-gates.sh +9 -19
  245. package/lib/paper-config.harness.mjs +1 -1
  246. package/lib/paper-config.ts +1 -5
  247. package/lib/skill-checks.mjs +24 -381
  248. package/lib/skill-checks.test.mjs +63 -115
  249. package/lib/skill-corpus.mjs +10 -251
  250. package/lib/skill-corpus.test.mjs +2 -13
  251. package/lib/skill-trigger-cases.mjs +1 -1
  252. package/package.json +13 -2
  253. package/{skills/submit-paper/references/venues → presets}/acm-sigconf.jsonc +4 -1
  254. package/{skills/submit-paper/references/venues → presets}/agenticdev.jsonc +4 -0
  255. package/presets/aidc.jsonc +73 -0
  256. package/{skills/submit-paper/references/venues → presets}/aisec.jsonc +4 -0
  257. package/presets/ieee-conference.jsonc +66 -0
  258. package/{skills/submit-paper/references/venues → presets}/realm.jsonc +8 -1
  259. package/{skills/submit-paper/references/venues → presets/tex}/paper-guards.tex +1 -1
  260. package/{skills/submit-paper/references/venues → presets}/venue-profile.schema.json +30 -2
  261. package/scripts/check.harness.ts +64 -10
  262. package/scripts/check.test.ts +60 -13
  263. package/scripts/check.ts +83 -30
  264. package/scripts/coverage-config.test.ts +0 -1
  265. package/scripts/harness-api.frozen.json +0 -1
  266. package/scripts/test.test.ts +1 -1
  267. package/scripts/test.ts +1 -1
  268. package/skills/analyze-sibling-paper/SKILL.md +1 -39
  269. package/skills/analyze-sibling-paper/SKILL.md.spec.ts +0 -38
  270. package/skills/argument-arc/SKILL.md +2 -36
  271. package/skills/argument-arc/SKILL.md.spec.ts +1 -43
  272. package/skills/build-benchmark/SKILL.md +1 -41
  273. package/skills/build-benchmark/SKILL.md.spec.ts +0 -40
  274. package/skills/camera-ready/SKILL.md +1 -34
  275. package/skills/camera-ready/SKILL.md.spec.ts +0 -33
  276. package/skills/cold-read-diff/SKILL.md +1 -35
  277. package/skills/cold-read-diff/SKILL.md.spec.ts +0 -34
  278. package/skills/draft-paper/SKILL.md +2 -41
  279. package/skills/draft-paper/SKILL.md.spec.ts +1 -50
  280. package/skills/extend-paper/SKILL.md +2 -33
  281. package/skills/extend-paper/SKILL.md.spec.ts +1 -42
  282. package/skills/find-venue/SKILL.md +2 -38
  283. package/skills/find-venue/SKILL.md.spec.ts +1 -47
  284. package/skills/grade-paper-writing/SKILL.md +3 -38
  285. package/skills/grade-paper-writing/SKILL.md.spec.ts +2 -47
  286. package/skills/grade-paper-writing/prose-lint.mjs +2 -3
  287. package/skills/grade-paper-writing/prose-lint.test.ts +1 -1
  288. package/skills/harden-paper/SKILL.md +1 -39
  289. package/skills/harden-paper/SKILL.md.spec.ts +0 -38
  290. package/skills/map-prior-work/SKILL.md +1 -35
  291. package/skills/map-prior-work/SKILL.md.spec.ts +0 -34
  292. package/skills/osf-artifact-upload/osf-artifact-upload.harness.ts +3 -4
  293. package/skills/paper-adversarial-review/SKILL.md +2 -35
  294. package/skills/paper-adversarial-review/SKILL.md.spec.ts +1 -43
  295. package/skills/paper-pipeline/PIPELINE-MAP.md +2 -2
  296. package/skills/paper-pipeline/SKILL.md +3 -29
  297. package/skills/paper-pipeline/SKILL.md.spec.ts +2 -28
  298. package/skills/paper-pipeline/grade-paper-writing-ablation.eval.mjs +1 -1
  299. package/skills/paper-pipeline/paper-pipeline.harness.mjs +25 -40
  300. package/skills/paper-pipeline/references/acceptance-gate.md +1 -6
  301. package/skills/paper-pipeline/references/pipeline-status-template.md +8 -35
  302. package/skills/paper-pipeline/scripts/README.md +30 -225
  303. package/skills/paper-pipeline/scripts/artifact-coverage.harness.mjs +4 -8
  304. package/skills/paper-pipeline/scripts/consumer.d.mts +0 -2
  305. package/skills/paper-pipeline/scripts/consumer.harness.mjs +6 -104
  306. package/skills/paper-pipeline/scripts/consumer.mjs +24 -119
  307. package/skills/paper-pipeline/scripts/generated-code.harness.mjs +2 -3
  308. package/skills/paper-pipeline/scripts/population-map.harness.mjs +3 -3
  309. package/skills/paper-status/SKILL.md +2 -42
  310. package/skills/paper-status/SKILL.md.spec.ts +1 -41
  311. package/skills/pc-panel-review/SKILL.md +2 -46
  312. package/skills/pc-panel-review/SKILL.md.spec.ts +1 -45
  313. package/skills/plan-paper-timeline/SKILL.md +2 -36
  314. package/skills/plan-paper-timeline/SKILL.md.spec.ts +0 -36
  315. package/skills/render-paper/SKILL.md +2 -38
  316. package/skills/render-paper/SKILL.md.spec.ts +1 -37
  317. package/skills/render-paper/check-render.sh +16 -63
  318. package/skills/render-paper/render-paper.harness.mjs +17 -59
  319. package/skills/research-ideate/SKILL.md +2 -39
  320. package/skills/research-ideate/SKILL.md.spec.ts +1 -47
  321. package/skills/study-accepted-papers/SKILL.md +1 -34
  322. package/skills/study-accepted-papers/SKILL.md.spec.ts +0 -33
  323. package/skills/submit-paper/SKILL.md +1 -33
  324. package/skills/submit-paper/SKILL.md.spec.ts +0 -32
  325. package/skills/sweep-design-space/SKILL.md +1 -38
  326. package/skills/sweep-design-space/SKILL.md.spec.ts +0 -37
  327. package/skills/tighten-paper/SKILL.md +2 -39
  328. package/skills/tighten-paper/SKILL.md.spec.ts +1 -47
  329. package/skills/tighten-paper/structure.mjs +4 -11
  330. package/skills/verify-citations/SKILL.md +2 -38
  331. package/skills/verify-citations/SKILL.md.spec.ts +1 -37
  332. package/src/adapters/banal/failure.ts +9 -10
  333. package/src/adapters/banal/index.test.ts +3 -3
  334. package/src/adapters/banal/index.ts +8 -10
  335. package/src/adapters/banal/install.ts +9 -4
  336. package/src/adapters/banal/invocation.test.ts +20 -8
  337. package/src/adapters/banal/invocation.ts +24 -9
  338. package/src/adapters/banal/locate.test.ts +2 -2
  339. package/src/adapters/banal/locate.ts +27 -21
  340. package/src/adapters/banal/output.test.ts +1 -1
  341. package/src/adapters/banal/run.test.ts +3 -3
  342. package/src/adapters/banal/run.ts +16 -10
  343. package/src/adapters/banal/settings.ts +2 -2
  344. package/src/adapters/banal/xml.test.ts +1 -1
  345. package/src/adapters/curl/download.test.ts +4 -4
  346. package/src/adapters/latex/document-class.ts +147 -0
  347. package/src/adapters/latex/hidden.ts +70 -0
  348. package/src/adapters/latex/index.test.ts +361 -0
  349. package/src/adapters/latex/index.ts +55 -0
  350. package/src/adapters/latex/nodes.ts +141 -0
  351. package/src/adapters/latex/outline.ts +64 -0
  352. package/src/adapters/latex/parse.ts +136 -0
  353. package/src/adapters/latex/rendered.ts +59 -0
  354. package/src/adapters/memory/index.ts +4 -3
  355. package/src/adapters/node/files.test.ts +5 -3
  356. package/src/adapters/node/process.io.ts +9 -2
  357. package/src/adapters/node/process.test.ts +4 -4
  358. package/src/adapters/node/workspace.io.ts +9 -4
  359. package/src/adapters/pdfjs/fill.test.ts +1 -1
  360. package/src/adapters/pdfjs/fill.ts +18 -10
  361. package/src/adapters/references/index.ts +7 -2
  362. package/src/build-engine.test.ts +11 -6
  363. package/src/build-engine.ts +10 -5
  364. package/src/build.harness.mjs +5 -7
  365. package/src/build.test.ts +10 -11
  366. package/src/build.ts +12 -13
  367. package/src/caller-path.ts +13 -0
  368. package/src/cli.harness.mjs +1 -3
  369. package/src/cli.test.ts +21 -16
  370. package/src/cli.ts +115 -33
  371. package/src/doctor.test.ts +1 -1
  372. package/src/doctor.ts +14 -5
  373. package/src/domain/paths.test.ts +15 -0
  374. package/src/domain/paths.ts +20 -1
  375. package/src/domain/ready.ts +31 -0
  376. package/src/domain/record.test.ts +20 -0
  377. package/src/domain/record.ts +12 -0
  378. package/src/domain/sha256.ts +5 -3
  379. package/src/domain/tex-document.test.ts +80 -0
  380. package/src/domain/tex-document.ts +110 -0
  381. package/src/engine.harness.mjs +1 -1
  382. package/src/engine.ts +30 -2
  383. package/src/facts-file.test.ts +4 -7
  384. package/src/facts-file.ts +45 -30
  385. package/src/hooks-settings.harness.ts +17 -0
  386. package/src/hooks-settings.ts +28 -4
  387. package/src/init.test.ts +25 -5
  388. package/src/init.ts +26 -12
  389. package/src/latex-loop.harness.ts +1 -1
  390. package/src/new-paper.harness.mjs +7 -23
  391. package/src/new-paper.test.ts +28 -3
  392. package/src/new-paper.ts +15 -3
  393. package/src/package-dirs.ts +17 -0
  394. package/src/paper-files.test.ts +22 -19
  395. package/src/paper-files.ts +16 -27
  396. package/src/paper-settings-commands.test.ts +6 -1
  397. package/src/paper-settings.test.ts +2 -2
  398. package/src/paper-settings.ts +5 -5
  399. package/src/pdf-facts.test.ts +16 -5
  400. package/src/pdf-facts.ts +79 -22
  401. package/src/pdf-geometry.ts +5 -3
  402. package/src/ports/latex.ts +29 -0
  403. package/src/ports/tool-installer.ts +2 -17
  404. package/src/presets.test.ts +65 -4
  405. package/src/presets.ts +65 -28
  406. package/src/references.test.ts +25 -19
  407. package/src/references.ts +4 -5
  408. package/src/rule-cases.test.ts +474 -0
  409. package/src/rule-docs.test.ts +159 -0
  410. package/src/rules-config.ts +40 -11
  411. package/src/tex-requirements.harness.ts +13 -23
  412. package/src/tex-requirements.test.ts +21 -0
  413. package/src/tex-requirements.ts +64 -14
  414. package/src/tex-venue-rules.test.ts +659 -0
  415. package/src/tex-venue-rules.ts +429 -0
  416. package/src/toolchain.harness.mjs +1 -1
  417. package/src/toolchain.test.ts +9 -5
  418. package/src/toolchain.ts +2 -2
  419. package/src/types.ts +5 -3
  420. package/src/venue-rules.test.ts +104 -8
  421. package/src/venue-rules.ts +22 -12
  422. package/templates/paper/paper.tex +2 -1
  423. package/skills/paper-pipeline/scripts/announce.mjs +0 -67
  424. package/skills/paper-pipeline/scripts/announce.test.mjs +0 -102
  425. package/skills/paper-pipeline/scripts/fixtures/clean/PIPELINE-STATUS.md +0 -51
  426. package/skills/paper-pipeline/scripts/fixtures/dirty/PIPELINE-STATUS.md +0 -52
  427. package/skills/paper-pipeline/scripts/fixtures/dirty/paper.md +0 -6
  428. package/skills/paper-pipeline/scripts/fixtures/scorecard.ts +0 -100
  429. package/skills/paper-pipeline/scripts/ledger.cli.test.mjs +0 -302
  430. package/skills/paper-pipeline/scripts/ledger.mjs +0 -623
  431. package/skills/paper-pipeline/scripts/ledger.test.mjs +0 -254
  432. package/skills/paper-pipeline/scripts/pipeline-check.cli.test.mjs +0 -474
  433. package/skills/paper-pipeline/scripts/pipeline-check.harness.mjs +0 -328
  434. package/skills/paper-pipeline/scripts/pipeline-check.mjs +0 -750
  435. package/skills/paper-pipeline/scripts/pipeline-edges.mjs +0 -169
  436. package/skills/paper-pipeline/scripts/pipeline-edges.test.mjs +0 -73
  437. package/skills/paper-pipeline/scripts/run-mechanical.mjs +0 -648
  438. package/skills/paper-pipeline/scripts/run-mechanical.test.mjs +0 -468
  439. package/skills/paper-pipeline/scripts/status.mjs +0 -295
  440. package/skills/paper-pipeline/scripts/status.test.mjs +0 -232
  441. /package/{skills/submit-paper/references/venues → presets/tex}/agenticdev.tex +0 -0
  442. /package/{skills/submit-paper/references/venues → presets}/tex-base.jsonc +0 -0
@@ -44,7 +44,7 @@ test("doctor: a settings file silent on papersDir, an occupied entry, an unreada
44
44
  const code = doctor({
45
45
  cwd: root,
46
46
  log: (s: string) => lines.push(s),
47
- run: (() => ({ status: 0 })) as never,
47
+ run: () => ({ status: 0 }),
48
48
  cliPapers: join(root, "papers"),
49
49
  skillLinks: () => ({
50
50
  ok: true,
package/src/doctor.ts CHANGED
@@ -37,6 +37,7 @@ import { PAPER_MARKERS } from "./build.ts";
37
37
  import { linkSkills, SKILLS_HOME, type LinkReport } from "./link-skills.ts";
38
38
  import { doctorHooks } from "./hooks-settings.ts";
39
39
  import { messageOf } from "./domain/text.ts";
40
+ import { fieldOf } from "./domain/record.ts";
40
41
 
41
42
  export interface Program {
42
43
  readonly bin: string;
@@ -99,7 +100,17 @@ export const PROGRAMS: readonly Program[] = [
99
100
  },
100
101
  ];
101
102
 
102
- export const found = (bin: string, run = spawnSync): boolean =>
103
+ /**
104
+ * Asks the shell whether a program is on PATH. `spawnSync` is one; a fake answers with a status
105
+ * alone, because a status is all `found` reads.
106
+ */
107
+ export type ProgramProbe = (
108
+ file: string,
109
+ args: readonly string[],
110
+ options: { readonly shell: true; readonly stdio: "ignore" },
111
+ ) => { readonly status: number | null };
112
+
113
+ export const found = (bin: string, run: ProgramProbe = spawnSync): boolean =>
103
114
  run("command", ["-v", bin], { shell: true, stdio: "ignore" }).status === 0;
104
115
 
105
116
  /** Lists a directory's entries; the disk by default, a test stages an unreadable child. */
@@ -176,9 +187,7 @@ function declarationVerdict(raw: string | null): {
176
187
  bad: 1,
177
188
  };
178
189
  }
179
- const declared = (settings as Record<string, unknown> | null)?.[
180
- PAPERS_DIR_FIELD
181
- ];
190
+ const declared = fieldOf(settings, PAPERS_DIR_FIELD);
182
191
  return {
183
192
  lines: [
184
193
  declared === undefined
@@ -234,7 +243,7 @@ export interface DoctorOptions {
234
243
  cwd?: string;
235
244
  /** The project root the hooks are anchored to. Claude Code sets this; it defaults to `cwd`. */
236
245
  projectDir?: string;
237
- run?: typeof spawnSync;
246
+ run?: ProgramProbe;
238
247
  /** The directory the CLI resolved from its own config, so both sides can be compared. */
239
248
  cliPapers?: string | null;
240
249
  /** Reads the skill links without writing any. Injected only so a test can fake the install. */
@@ -0,0 +1,15 @@
1
+ import assert from "node:assert/strict";
2
+ import { test } from "vitest";
3
+ import { absolutePath, joinPath } from "./paths.ts";
4
+
5
+ test("absolutePath keeps an absolute path as it is", () => {
6
+ assert.equal(absolutePath("/a/b"), "/a/b");
7
+ });
8
+
9
+ test("absolutePath refuses a relative path instead of letting the process's cwd decide it", () => {
10
+ assert.throws(() => absolutePath("a/b"), /not an absolute path: a\/b/);
11
+ });
12
+
13
+ test("joinPath stays under its absolute base", () => {
14
+ assert.equal(joinPath(absolutePath("/a"), "b", "c"), "/a/b/c");
15
+ });
@@ -1,5 +1,24 @@
1
1
  /** Paths as the domain holds them. */
2
+ import { isAbsolute, join } from "node:path";
2
3
  import type { Opaque } from "ts-essentials";
3
4
 
4
- /** An absolute path. Minted at the composition root (`abs`), so nothing inside resolves a cwd. */
5
+ /**
6
+ * An absolute path. Minted by `absolutePath` (a checked string) or `joinPath` (an absolute base and
7
+ * segments) only, so nothing holding one has to ask whether it is relative.
8
+ */
5
9
  export type AbsolutePath = Opaque<string, "AbsolutePath">;
10
+
11
+ // The brand's one owner: the predicate IS the check, so no assertion is needed to mint the type.
12
+ const isAbsolutePath = (p: string): p is AbsolutePath => isAbsolute(p);
13
+
14
+ /** A path the caller holds as text, checked where it becomes a value. A relative one throws. */
15
+ export function absolutePath(p: string): AbsolutePath {
16
+ if (!isAbsolutePath(p)) throw new Error(`not an absolute path: ${p}`);
17
+ return p;
18
+ }
19
+
20
+ /** `segments` under `base`: absolute because `base` is. */
21
+ export const joinPath = (
22
+ base: AbsolutePath,
23
+ ...segments: string[]
24
+ ): AbsolutePath => absolutePath(join(base, ...segments));
@@ -0,0 +1,31 @@
1
+ /** A program `paperlint toolchain` installed or found: present, verified, running. */
2
+
3
+ // Not exported: a `Ready` carries this property, and the one way to write it is `ready` below.
4
+ const READY = Symbol("Ready");
5
+
6
+ /**
7
+ * Present, verified, running. BRANDED: a literal `{ where, fresh, verified }` is not a `Ready`, so
8
+ * only an installer's adapter — one minting call each (banal's is `ready` in
9
+ * `adapters/banal/index.ts`) — can make one, and a ready line cannot be printed without it.
10
+ */
11
+ export interface Ready {
12
+ /** Where it is: the program's path or its bin directory. */
13
+ readonly where: string;
14
+ /** This run installed it. */
15
+ readonly fresh: boolean;
16
+ /** What was checked before calling it ready, as a person reads it. */
17
+ readonly verified: string;
18
+ readonly [READY]: true;
19
+ }
20
+
21
+ /** An installer's verdict that its program is ready. Called by an installer adapter, after its checks. */
22
+ export const ready = (r: {
23
+ readonly where: string;
24
+ readonly fresh: boolean;
25
+ readonly verified: string;
26
+ }): Ready => ({
27
+ where: r.where,
28
+ fresh: r.fresh,
29
+ verified: r.verified,
30
+ [READY]: true,
31
+ });
@@ -0,0 +1,20 @@
1
+ import assert from "node:assert/strict";
2
+ import { test } from "vitest";
3
+ import { fieldOf, isRecord, numbersOf } from "./record.ts";
4
+
5
+ test("isRecord: an object, and not null, an array or a string", () => {
6
+ assert.deepEqual(
7
+ [isRecord({}), isRecord(null), isRecord([]), isRecord("x")],
8
+ [true, false, false, false],
9
+ );
10
+ });
11
+
12
+ test("fieldOf reads an own or inherited field of a record, and nothing of anything else", () => {
13
+ assert.equal(fieldOf({ a: 1 }, "a"), 1);
14
+ assert.equal(fieldOf(new TypeError("x"), "name"), "TypeError");
15
+ assert.equal(fieldOf("text", "length"), undefined);
16
+ });
17
+
18
+ test("numbersOf keeps numbers as they are", () => {
19
+ assert.deepEqual(numbersOf([1, -0.5, 0]), [1, -0.5, 0]);
20
+ });
@@ -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);
@@ -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,80 @@
1
+ /** The values a LaTeX source is read into: a run's text and the span of a range inside it. */
2
+ import { describe, expect, it } from "vitest";
3
+ import {
4
+ collapse,
5
+ documentClassLine,
6
+ missingOptions,
7
+ runText,
8
+ spanIn,
9
+ venueClass,
10
+ type TextRun,
11
+ } from "./tex-document.ts";
12
+
13
+ // "ab" at 10, then " " at 20 (one character standing for a longer whitespace), then "cd" at 30.
14
+ const RUN: TextRun = {
15
+ segments: [
16
+ { text: "ab", at: 10 },
17
+ { text: " ", at: 20 },
18
+ { text: "cd", at: 30 },
19
+ ],
20
+ };
21
+
22
+ describe("a text run", () => {
23
+ it("its text is its segments' text, in order", () => {
24
+ expect(runText(RUN)).toBe("ab cd");
25
+ });
26
+
27
+ it("spanIn: from the first character's offset to past the last's, across segments", () => {
28
+ expect(spanIn(RUN, 0, 2)).toEqual({ start: 10, end: 12 });
29
+ expect(spanIn(RUN, 1, 4)).toEqual({ start: 11, end: 31 });
30
+ expect(spanIn(RUN, 3, 5)).toEqual({ start: 30, end: 32 });
31
+ });
32
+
33
+ it("🔴 spanIn: an empty range, or one reaching outside the run, has no span — never {0, 0}", () => {
34
+ expect(spanIn(RUN, 2, 2)).toBe(null);
35
+ expect(spanIn(RUN, -1, 2)).toBe(null);
36
+ expect(spanIn(RUN, 3, 9)).toBe(null);
37
+ expect(spanIn(RUN, 9, 10)).toBe(null);
38
+ });
39
+ });
40
+
41
+ describe("a document class", () => {
42
+ it("documentClassLine writes it back; with no options, no brackets", () => {
43
+ expect(
44
+ documentClassLine({
45
+ cls: "IEEEtran",
46
+ options: ["conference", "compsoc"],
47
+ }),
48
+ ).toBe("\\documentclass[conference,compsoc]{IEEEtran}");
49
+ expect(documentClassLine({ cls: "article", options: [] })).toBe(
50
+ "\\documentclass{article}",
51
+ );
52
+ });
53
+
54
+ it("missingOptions: every option of the wanted class the paper lacks, in the wanted order", () => {
55
+ expect(
56
+ missingOptions(
57
+ { cls: "x", options: ["b"] },
58
+ { cls: "x", options: ["a", "b", "c"] },
59
+ ),
60
+ ).toEqual(["a", "c"]);
61
+ });
62
+
63
+ it("venueClass: another class is replaced; the same class gains the missing options, or is kept", () => {
64
+ const want = { cls: "IEEEtran", options: ["conference", "compsoc"] };
65
+ expect(venueClass({ cls: "article", options: ["11pt"] }, want)).toEqual({
66
+ kind: "replace",
67
+ by: want,
68
+ });
69
+ expect(
70
+ venueClass({ cls: "IEEEtran", options: ["review", "conference"] }, want),
71
+ ).toEqual({ kind: "add", options: ["compsoc"] });
72
+ expect(
73
+ venueClass({ cls: "IEEEtran", options: ["compsoc", "conference"] }, want),
74
+ ).toEqual({ kind: "keep" });
75
+ });
76
+
77
+ it("collapse: every run of whitespace, newlines included, and trims", () => {
78
+ expect(collapse(" LLM Usage\n Statement ")).toBe("LLM Usage Statement");
79
+ });
80
+ });
@@ -0,0 +1,110 @@
1
+ /**
2
+ * What a paper's LaTeX source says, in the paper's vocabulary — its document class, its sections,
3
+ * the text a reader sees — as values. No parser here: the LaTeX adapter (`src/adapters/latex/`)
4
+ * produces these, and the rules and `paperlint new` read them through the `LatexReader` port.
5
+ */
6
+
7
+ /** A span of the source: `[start, end)` offsets. */
8
+ export interface Span {
9
+ readonly start: number;
10
+ readonly end: number;
11
+ }
12
+
13
+ /** Where something stands in the source, or that the parser gave it no position. */
14
+ export type Place =
15
+ { readonly kind: "at"; readonly span: Span } | { readonly kind: "unplaced" };
16
+
17
+ /** Whitespace collapsed to single spaces and trimmed — how a title or a class name is compared. */
18
+ export const collapse = (s: string): string =>
19
+ s.split(/\s+/u).filter(Boolean).join(" ");
20
+
21
+ /** A LaTeX document class and the options it is loaded with, in source order. */
22
+ export interface DocumentClass {
23
+ readonly cls: string;
24
+ readonly options: readonly string[];
25
+ }
26
+
27
+ /** A paper's `\documentclass`: none, one that names no class, or a class with its options. */
28
+ export type ClassLine =
29
+ | { readonly kind: "missing" }
30
+ | { readonly kind: "empty"; readonly place: Place }
31
+ | ({ readonly kind: "class"; readonly place: Place } & DocumentClass);
32
+
33
+ /** The options of `want` that `got` does not carry — every one, in `want`'s order. */
34
+ export const missingOptions = (
35
+ got: DocumentClass,
36
+ want: DocumentClass,
37
+ ): readonly string[] => want.options.filter((o) => !got.options.includes(o));
38
+
39
+ /** What a paper's class line needs to satisfy a venue: nothing, options added, or the venue's line. */
40
+ export type ClassChange =
41
+ | { readonly kind: "keep" }
42
+ | { readonly kind: "add"; readonly options: readonly string[] }
43
+ | { readonly kind: "replace"; readonly by: DocumentClass };
44
+
45
+ /**
46
+ * What a paper's class must change to satisfy a venue's `want`: another class is replaced by `want`;
47
+ * the same class keeps its options and gains the ones it lacks, in `want`'s order. This decides WHAT
48
+ * changes; writing it into a source is the LaTeX adapter's.
49
+ */
50
+ export function venueClass(
51
+ have: DocumentClass,
52
+ want: DocumentClass,
53
+ ): ClassChange {
54
+ if (have.cls !== want.cls) return { kind: "replace", by: want };
55
+ const missing = missingOptions(have, want);
56
+ return missing.length === 0
57
+ ? { kind: "keep" }
58
+ : { kind: "add", options: missing };
59
+ }
60
+
61
+ /** A class as a `\documentclass` line, for a message and for the line `paperlint new` writes. */
62
+ export const documentClassLine = (d: DocumentClass): string =>
63
+ `\\documentclass${d.options.length > 0 ? `[${d.options.join(",")}]` : ""}{${d.cls}}`;
64
+
65
+ /** One `\section` or `\section*`, its title's text with whitespace collapsed. */
66
+ export interface Heading {
67
+ readonly title: string;
68
+ readonly place: Place;
69
+ }
70
+
71
+ /** The document's sections in source order, where its back matter begins, and where it ends. */
72
+ export interface Outline {
73
+ readonly sections: readonly Heading[];
74
+ /** The first `\appendix` or bibliography, or null: sections from here on are not body. */
75
+ readonly backMatter: number | null;
76
+ /** Where `\end{document}` stands, or null when there is no document environment. */
77
+ readonly end: number | null;
78
+ }
79
+
80
+ /** Text a reader sees, copied from one place: `text[i]` stands at source offset `at + i`. */
81
+ export interface Segment {
82
+ readonly text: string;
83
+ readonly at: number;
84
+ }
85
+
86
+ /** A run of rendered text: contiguous segments, never empty. */
87
+ export interface TextRun {
88
+ readonly segments: readonly [Segment, ...Segment[]];
89
+ }
90
+
91
+ /** The run's text. */
92
+ export const runText = (run: TextRun): string =>
93
+ run.segments.map((s) => s.text).join("");
94
+
95
+ /** The source offset of the `i`-th character of the text these segments make, or null outside it. */
96
+ function offsetIn(segments: readonly Segment[], i: number): number | null {
97
+ const [s, ...rest] = segments;
98
+ if (s === undefined || i < 0) return null;
99
+ return i < s.text.length ? s.at + i : offsetIn(rest, i - s.text.length);
100
+ }
101
+
102
+ /**
103
+ * The source span of `runText(run).slice(from, to)`: from its first character to past its last.
104
+ * Null when the range is empty or reaches outside the run — never an invented position.
105
+ */
106
+ export function spanIn(run: TextRun, from: number, to: number): Span | null {
107
+ const start = offsetIn(run.segments, from);
108
+ const last = to > from ? offsetIn(run.segments, to - 1) : null;
109
+ return start === null || last === null ? null : { start, end: last + 1 };
110
+ }
@@ -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.ts`.
8
+ * All pure or port-driven: no TeX needed. The real-TeX half is `test/e2e/tex/build.e2e.ts`.
9
9
  */
10
10
  import {
11
11
  chmodSync,
package/src/engine.ts CHANGED
@@ -26,8 +26,36 @@ import { basename, delimiter, join } from "node:path";
26
26
  import type { TexRequirements } from "./tex-requirements.ts";
27
27
  import { printed } from "./domain/text.ts";
28
28
 
29
- /** A process runner with `spawnSync`'s shape — the port the harness replaces. */
30
- 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;
31
59
 
32
60
  /** One TeX Live and what it lacks for the papers at hand. */
33
61
  export interface TreeProbe {
@@ -5,12 +5,12 @@
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
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`.
8
+ * real banal is `test/e2e/tex/banal.e2e.ts`.
9
9
  */
10
10
  import assert from "node:assert/strict";
11
11
  import { readdirSync, readFileSync } from "node:fs";
12
12
  import { join } from "node:path";
13
- import { packageVenuesDir } from "../skills/paper-pipeline/scripts/consumer.mjs";
13
+ import { presetsDir } from "./package-dirs.ts";
14
14
  import { test } from "vitest";
15
15
  import { memoryFiles } from "./adapters/memory/index.ts";
16
16
  import {
@@ -154,12 +154,9 @@ const good: PdfReader = () =>
154
154
  });
155
155
  /** The shipped presets at their real paths: `extends` resolves through the same Files port. */
156
156
  const SHIPPED_PRESETS = Object.fromEntries(
157
- readdirSync(packageVenuesDir())
157
+ readdirSync(presetsDir())
158
158
  .filter((f) => /\.jsonc?$/.test(f))
159
- .map((f) => [
160
- join(packageVenuesDir(), f),
161
- readFileSync(join(packageVenuesDir(), f)),
162
- ]),
159
+ .map((f) => [join(presetsDir(), f), readFileSync(join(presetsDir(), f))]),
163
160
  );
164
161
 
165
162
  /** Options over in-memory files: the PDF and paperlint.json on "disk", a measurer answering `geometry`. */
package/src/facts-file.ts CHANGED
@@ -45,11 +45,11 @@ 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
- import { packageVenuesDir } from "../skills/paper-pipeline/scripts/consumer.mjs";
52
+ import { presetsDir } from "./package-dirs.ts";
53
53
  import { messageOf } from "./domain/text.ts";
54
54
 
55
55
  export const FACTS_SCHEMA = 2;
@@ -60,12 +60,6 @@ export const FACTS_FILE = "paper.facts.json";
60
60
  export const factsPath = (paperDir: string): string =>
61
61
  join(paperDir, FACTS_DIR, FACTS_FILE);
62
62
 
63
- /**
64
- * A path as the caller gave it. The `Files` adapter resolves a relative one against the process's
65
- * cwd, which is what these callers have always meant; the brand is not a claim this code checked it.
66
- */
67
- const at = (p: string): AbsolutePath => p as AbsolutePath;
68
-
69
63
  /** What the facts need from a paper's `paperlint.json`: its venue's label, its kind, where its PDF is. */
70
64
  export interface VenueDecl {
71
65
  /** The venue preset's display label (`agenticdev`), or null when the paper extends none. */
@@ -82,7 +76,7 @@ export interface VenueDecl {
82
76
  export function declaredVenue(
83
77
  files: Files,
84
78
  paperDir: string,
85
- venuesDir: string = packageVenuesDir(),
79
+ venuesDir: string = presetsDir(),
86
80
  ): VenueDecl | null {
87
81
  const p = paperPreset(paperDir, { files, venuesDir });
88
82
  const problem = paperPresetProblem(paperDir, p);
@@ -213,7 +207,7 @@ export async function measurePaper(
213
207
  ): Promise<Result<Measured, string>> {
214
208
  const r = await o.readPdf(pdf);
215
209
  if (!r.ok) return err(describeFailure(r, pdf));
216
- const bytes = o.files.readBytes(at(pdf));
210
+ const bytes = o.files.readBytes(callerPath(pdf));
217
211
  if (bytes === null) return err(`${pdf}: gone after pdf.js read it`);
218
212
  const geometry = o.measure.measure(r.facts.layout);
219
213
  const decl = declaredVenue(o.files, paperDir);
@@ -239,7 +233,7 @@ export function writeFactsFile(
239
233
  ): string {
240
234
  const out = factsPath(paperDir);
241
235
  files.writeAtomic(
242
- at(out),
236
+ callerPath(out),
243
237
  new TextEncoder().encode(`${JSON.stringify(facts, null, 2)}\n`),
244
238
  );
245
239
  return out;
@@ -288,6 +282,34 @@ const MAYBE_NUMBERS = [
288
282
  ] as const;
289
283
  const NUMBERS = ["body_pages", "ref_pages", "appendix_pages"] as const;
290
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
+
291
313
  /** The geometry columns when a measurer ran; the shape error otherwise. */
292
314
  function geometryOf(
293
315
  d: Readonly<Record<string, unknown>>,
@@ -295,24 +317,17 @@ function geometryOf(
295
317
  if (d["geometry_source"] === null) return ok(null);
296
318
  if (typeof d["geometry_source"] !== "string")
297
319
  return err("`geometry_source` is neither a measurer's name nor null");
298
- const bad =
299
- MAYBE_NUMBERS.find((k) => !numOrNull(d[k])) ??
300
- NUMBERS.find((k) => !isNum(d[k]));
301
- if (bad) return err(`\`${bad}\` is not a number`);
302
- const pagesByType = d["pages_by_type"];
303
- if (!isRecord(pagesByType) || !Object.values(pagesByType).every(isNum))
304
- return err("`pages_by_type` is not an object of page counts");
305
- const n = (k: (typeof MAYBE_NUMBERS)[number]) => d[k] as number | null;
320
+ if (!hasGeometryColumns(d)) return err(geometryError(d));
306
321
  return ok({
307
- page_w_in: n("page_w_in"),
308
- page_h_in: n("page_h_in"),
309
- columns: n("columns"),
310
- body_pt: n("body_pt"),
311
- ref_pt: n("ref_pt"),
312
- body_pages: d["body_pages"] as number,
313
- ref_pages: d["ref_pages"] as number,
314
- appendix_pages: d["appendix_pages"] as number,
315
- 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,
316
331
  });
317
332
  }
318
333
 
@@ -348,7 +363,7 @@ export function parseFactsText(text: string): Result<ReadFacts, FactsProblem> {
348
363
  if (!artifact.ok) return err({ kind: "broken", why: artifact.error });
349
364
  const rawFonts = d["fonts"];
350
365
  const fonts = Array.isArray(rawFonts) ? rawFonts.map(fontOf) : null;
351
- if (fonts === null || fonts.some((f) => f === null))
366
+ if (fonts === null || !fonts.every((f) => f !== null))
352
367
  return err({
353
368
  kind: "broken",
354
369
  why: "`fonts` is not a list of { name, type, embedded, program }",
@@ -357,7 +372,7 @@ export function parseFactsText(text: string): Result<ReadFacts, FactsProblem> {
357
372
  if (!geometry.ok) return err({ kind: "broken", why: geometry.error });
358
373
  return ok({
359
374
  ...artifact.value,
360
- fonts: fonts as FontEntry[],
375
+ fonts,
361
376
  geometry: geometry.value,
362
377
  });
363
378
  }
@@ -118,6 +118,23 @@ try {
118
118
  threw,
119
119
  );
120
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
+
121
138
  // ── which spelling runs which hook ─────────────────────────────────────────────────────
122
139
  const ours = `node "\${CLAUDE_PROJECT_DIR}/${MANAGED_BY}" hook paper-edit-guard`;
123
140
  const cases: [string, { name: string; ours: boolean } | null][] = [