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
@@ -31,6 +31,7 @@ import {
31
31
  PACKAGE_NAME,
32
32
  } from "../skills/paper-pipeline/scripts/consumer.mjs";
33
33
  import { messageOf } from "./domain/text.ts";
34
+ import { fieldOf, isRecord } from "./domain/record.ts";
34
35
 
35
36
  /** The committed, shared settings file — husky's `.husky/` analogue, except git carries it. */
36
37
  export const SETTINGS_PATH = join(".claude", "settings.json");
@@ -151,6 +152,25 @@ const prop = (v: unknown): Readonly<Record<string, unknown>> | undefined =>
151
152
  ? Object.fromEntries(Object.entries(v))
152
153
  : undefined;
153
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
+
154
174
  export interface Wiring {
155
175
  readonly compiled: HooksMap;
156
176
  /** Derived from the commands, in order. Never empty — an empty set throws. */
@@ -162,8 +182,12 @@ export interface Wiring {
162
182
  * check that silently wired nothing would report "✓ wired" over an empty set.
163
183
  */
164
184
  export function shippedWiring(file: string = WIRING_FILE): Wiring {
165
- const json = JSON.parse(readFileSync(file, "utf8")) as { hooks?: HooksMap };
166
- 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
+ );
167
191
  const names = commandsIn({ hooks: compiled })
168
192
  .map((c) => hookRun(c.command)?.name)
169
193
  .filter((n): n is string => typeof n === "string");
@@ -225,9 +249,9 @@ export function readSettings(root: string): SettingsRead {
225
249
  const raw = readFileSync(path, "utf8");
226
250
  try {
227
251
  const parsed: unknown = JSON.parse(raw);
228
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
252
+ if (!isRecord(parsed))
229
253
  return { status: "unparsable", path, reason: "not a JSON object" };
230
- return { status: "read", path, settings: parsed as Settings, raw };
254
+ return { status: "read", path, settings: parsed, raw };
231
255
  } catch (e) {
232
256
  return { status: "unparsable", path, reason: messageOf(e) };
233
257
  }
package/src/init.test.ts CHANGED
@@ -70,7 +70,7 @@ describe("paperlint init — a declared papers directory", () => {
70
70
  err: (s: string) => out.push(s),
71
71
  interactive: false,
72
72
  hooks: false,
73
- run: (() => ({ status: 0 })) as never,
73
+ run: () => ({ status: 0 }),
74
74
  link: () => ({ ok: false, error: "not linked in this test" }),
75
75
  });
76
76
  const text = out.join("\n");
@@ -97,7 +97,7 @@ describe("paperlint init — a declared papers directory", () => {
97
97
  interactive: true,
98
98
  ask: (q) => Promise.resolve((asked.push(q), "n")),
99
99
  hooks: false,
100
- run: (() => ({ status: 0 })) as never,
100
+ run: () => ({ status: 0 }),
101
101
  link: () => ({ ok: false, error: "not linked in this test" }),
102
102
  });
103
103
  expect(asked.filter((q) => /papers roots/.test(q))).toEqual([]);
@@ -116,7 +116,7 @@ describe("paperlint init — writes paperlint.json only when something differs f
116
116
  err: () => {},
117
117
  interactive: false,
118
118
  hooks: false,
119
- run: (() => ({ status: 0 })) as never,
119
+ run: () => ({ status: 0 }),
120
120
  link: () => ({ ok: false as const, error: "not linked in this test" }),
121
121
  };
122
122
  const fresh = (files: Record<string, string>): string => {
@@ -170,7 +170,7 @@ const texRun = async (
170
170
  ask: (q) =>
171
171
  Promise.resolve((asked.push(q), /TeX Live/.test(q) ? answer : "")),
172
172
  hooks: false,
173
- run: (() => ({ status: 0 })) as never,
173
+ run: () => ({ status: 0 }),
174
174
  link: () => ({ ok: false as const, error: "not linked in this test" }),
175
175
  tex,
176
176
  });
@@ -249,7 +249,7 @@ async function initRun(
249
249
  err: (s: string) => err.push(s),
250
250
  interactive: false,
251
251
  hooks: false,
252
- run: (() => ({ status: 0 })) as never,
252
+ run: () => ({ status: 0 }),
253
253
  link: () => ({ ok: false, error: "not linked in this test" }),
254
254
  ...opts,
255
255
  });
@@ -280,6 +280,26 @@ describe("paperlint init — a paperlint.json it cannot use stops it before anyt
280
280
  ]);
281
281
  });
282
282
 
283
+ it("🔴 a paperlint.json holding JSON that is not an object is refused, not read as settings", async () => {
284
+ // Guards: `null` threw a TypeError reading the papers field; `[1]` was spread into a settings
285
+ // object and the consumer's file rewritten as `{ "0": 1, "papersDir": … }`.
286
+ for (const json of ["null\n", "[1]\n"]) {
287
+ const root = tree({
288
+ "paperlint.json": json,
289
+ "drafts/a/paper.md": "# a\n",
290
+ });
291
+ const decl = await declarePapers(root, () =>
292
+ choosePapers(root, { interactive: false }),
293
+ );
294
+ expect(decl).toEqual({
295
+ status: "unparsable",
296
+ path: join(root, "paperlint.json"),
297
+ reason: "not a JSON object",
298
+ });
299
+ expect(readFileSync(join(root, "paperlint.json"), "utf8")).toBe(json);
300
+ }
301
+ });
302
+
283
303
  it("a file without a trailing newline keeps having none", async () => {
284
304
  const root = tree({
285
305
  "paperlint.json": '{"x":1}',
package/src/init.ts CHANGED
@@ -46,7 +46,13 @@ import {
46
46
  // eslint-disable-next-line boundaries/dependencies -- legacy I/O, moves behind a port in #76
47
47
  import { spawnSync } from "node:child_process";
48
48
  import { dirname, join, relative, resolve } from "node:path";
49
- import { doctor, detectPapers, found, PROGRAMS } from "./doctor.ts";
49
+ import {
50
+ doctor,
51
+ detectPapers,
52
+ found,
53
+ PROGRAMS,
54
+ type ProgramProbe,
55
+ } from "./doctor.ts";
50
56
  import { PAPER_MARKERS, papersIn } from "./build.ts";
51
57
  import { linkSkills, SKILLS_HOME, type LinkReport } from "./link-skills.ts";
52
58
  import { actionRef } from "./action-ref.ts";
@@ -73,6 +79,7 @@ import {
73
79
  PAPERS_DIR_FIELD,
74
80
  } from "#lib/paper-config";
75
81
  import { messageOf } from "./domain/text.ts";
82
+ import { isRecord } from "./domain/record.ts";
76
83
 
77
84
  /** How the papers directory was arrived at. Printed, because a guess must not read as a fact. */
78
85
  export type PapersHow =
@@ -227,13 +234,17 @@ export async function declarePapers(
227
234
  ): Promise<DeclarationResult> {
228
235
  const path = join(root, CONFIG_FILE);
229
236
  const raw = existsSync(path) ? readFileSync(path, "utf8") : null;
230
- let settings: Record<string, unknown> = {};
237
+ let settings: Readonly<Record<string, unknown>> = {};
231
238
  if (raw !== null) {
239
+ let parsed: unknown;
232
240
  try {
233
- settings = JSON.parse(raw) as Record<string, unknown>;
241
+ parsed = JSON.parse(raw);
234
242
  } catch (e) {
235
243
  return { status: "unparsable", path, reason: messageOf(e) };
236
244
  }
245
+ if (!isRecord(parsed))
246
+ return { status: "unparsable", path, reason: "not a JSON object" };
247
+ settings = parsed;
237
248
  const existing = settings[PAPERS_DIR_FIELD];
238
249
  if (existing !== undefined)
239
250
  return { status: "kept", path, papers: existing };
@@ -362,7 +373,7 @@ export function reportWorkflow(
362
373
  * passing checker produce the same silence.
363
374
  */
364
375
  export function missingPrograms(
365
- run = spawnSync,
376
+ run: ProgramProbe = spawnSync,
366
377
  ): readonly (typeof PROGRAMS)[number][] {
367
378
  return PROGRAMS.filter((p) => !found(p.bin, run));
368
379
  }
@@ -584,7 +595,7 @@ export interface InitOptions {
584
595
  name: string,
585
596
  format: PaperFormat,
586
597
  ) => Promise<number>;
587
- run?: typeof spawnSync;
598
+ run?: ProgramProbe;
588
599
  /**
589
600
  * What `paperlint lint` would resolve from the declaration, asked of the CLI's OWN reader. A second
590
601
  * implementation here would be a second source of truth — the very defect `doctor` reports.
@@ -749,21 +760,28 @@ export async function init(
749
760
  for (const line of papersLines(decl, why)) log(line);
750
761
  log(``);
751
762
  log(`settings`);
752
- if (decl.status === "written")
763
+ // Every step below uses the DECLARED directory. A kept declaration outranks what init
764
+ // measured or guessed: otherwise the first paper and the workflow would land in the
765
+ // guessed directory while lint and the hooks keep reading the declared one.
766
+ let papersDir: string;
767
+ if (decl.status === "written") {
768
+ papersDir = decl.papers;
753
769
  log(
754
770
  ` ✓ ${here(decl.path)} → { "${PAPERS_DIR_FIELD}": ${JSON.stringify(decl.papers)} }`,
755
771
  );
756
- else if (decl.status === "default")
772
+ } else if (decl.status === "default") {
773
+ papersDir = decl.papers;
757
774
  log(
758
775
  ` ✓ nothing to write — "${decl.papers}" is the default, so no ${CONFIG_FILE} is needed`,
759
776
  );
760
- else if (decl.status === "kept") {
777
+ } else if (decl.status === "kept") {
761
778
  if (typeof decl.papers !== "string") {
762
779
  err(
763
780
  ` ✗ ${here(decl.path)} declares ${PAPERS_DIR_FIELD} = ${JSON.stringify(decl.papers)} — it must be a directory path (a string)`,
764
781
  );
765
782
  return 2;
766
783
  }
784
+ papersDir = decl.papers;
767
785
  log(
768
786
  ` ✓ ${here(decl.path)} already declares ${PAPERS_DIR_FIELD} = ${JSON.stringify(decl.papers)} — kept, nothing overwritten`,
769
787
  );
@@ -777,10 +795,6 @@ export async function init(
777
795
  );
778
796
  return 2;
779
797
  }
780
- // Every step below uses the DECLARED directory. A kept declaration outranks what init
781
- // measured or guessed: otherwise the first paper and the workflow would land in the
782
- // guessed directory while lint and the hooks keep reading the declared one.
783
- const papersDir = decl.papers as string;
784
798
 
785
799
  // ── 3. the skills, linked where Claude Code looks for them ─────────────────────────────
786
800
  for (const line of reportSkillLinks(link(root), here)) log(line);
@@ -6,7 +6,7 @@
6
6
  * A defect in `nextStep` fails in table 1, one in `summarize` in table 2, before table 3 runs.
7
7
  *
8
8
  * The decision is pure, so none of this needs TeX. The real-pdflatex half lives in
9
- * `test/e2e/build.ts`.
9
+ * `test/e2e/tex/build.e2e.ts`.
10
10
  */
11
11
  import { createChecker } from "../lib/check.ts";
12
12
  import type {
@@ -7,9 +7,8 @@
7
7
  * from a missing file into a created one;
8
8
  * - the scorecard carries `researchQuestion` and NO `stages` — a new paper has shipped nothing,
9
9
  * and a placeholder stage would be a red `paper/stages` on the very first run;
10
- * - the scorecard PARSES as a scorecard (`pipeline-check.mjs` finds its four sections). The
11
- * template used to live in a code fence; moved into a file, it must still be the format the
12
- * skills' checker reads;
10
+ * - the scorecard carries the `**Readiness verdict:**` line the status hook surfaces on every
11
+ * paper edit — without it the hook reports that the paper cannot say whether it is ready;
13
12
  * - it never overwrites, and on an existing folder adds only what is missing;
14
13
  * - the project's `<papers>/.template/` wins over the package, file by file.
15
14
  *
@@ -25,7 +24,6 @@ import {
25
24
  rmSync,
26
25
  writeFileSync,
27
26
  } from "node:fs";
28
- import { spawnSync } from "node:child_process";
29
27
  import { tmpdir } from "node:os";
30
28
  import { dirname, join } from "node:path";
31
29
  import { fileURLToPath } from "node:url";
@@ -124,26 +122,12 @@ try {
124
122
  /1 problem \(0 errors, 1 warning\)/.test(l.text),
125
123
  );
126
124
 
127
- // The format contract of the skills' checker: four sections, parsed.
128
- const pc = spawnSync(
129
- process.execPath,
130
- [
131
- join(ROOT, "skills", "paper-pipeline", "scripts", "pipeline-check.mjs"),
132
- join(papers, "demo"),
133
- "--json",
134
- "--today=2026-09-23",
135
- ],
136
- { cwd: work, encoding: "utf8" },
137
- );
138
- let parsed = null;
139
- try {
140
- parsed = JSON.parse(pc.stdout);
141
- } catch {
142
- parsed = null;
143
- }
125
+ // What the status hook reads on every paper edit: the verdict line.
144
126
  check(
145
- "the scorecard PARSES for pipeline-check — its four sections are found (it prints JSON, not the format warning)",
146
- Array.isArray(parsed) && !/expected the four sections/.test(pc.stderr),
127
+ "the scorecard carries the `**Readiness verdict:**` line the status hook surfaces",
128
+ /^\*\*Readiness verdict:\*\*/m.test(
129
+ readFileSync(join(papers, "demo", STATUS_FILE), "utf8"),
130
+ ),
147
131
  );
148
132
 
149
133
  // ── never overwrite; on an existing folder only the missing files ─────────────────────
@@ -19,7 +19,7 @@ import { newPaper, OVERRIDE_DIR, reportNewPaper } from "./new-paper.ts";
19
19
  import { parsePaperSettings } from "./paper-settings.ts";
20
20
  import { chooseVenue, run } from "./cli.ts";
21
21
  import { shippedPresets } from "./presets.ts";
22
- import { packageVenuesDir } from "../skills/paper-pipeline/scripts/consumer.mjs";
22
+ import { presetsDir } from "./package-dirs.ts";
23
23
  import { lintReport } from "../test/lint-report.ts";
24
24
  import { z } from "zod";
25
25
 
@@ -177,6 +177,31 @@ describe("paperlint new --venue / --kind", () => {
177
177
  expect(r.code).toBe(0);
178
178
  });
179
179
 
180
+ it("🔴 the new paper.tex is set in the venue's class, so tex/template passes on what new wrote; the required section is named", async () => {
181
+ const r = await newIn(["--venue", "aidc", "--kind", "regular"]);
182
+ const tex = readFileSync(join(r.dir, "paper.tex"), "utf8");
183
+ expect(tex).toContain("\\documentclass[conference,compsoc]{IEEEtran}\n");
184
+ expect(tex).not.toContain("{article}");
185
+ expect(r.out).not.toMatch(/tex\/template/);
186
+ // What the stub still lacks is what AIDC requires of the text, and lint says so right away.
187
+ expect(r.out).toMatch(/tex\/required-section/);
188
+ expect(r.code).toBe(1);
189
+ });
190
+
191
+ it("a preset that names no template, or one no reader can read, leaves the template's class as it is", async () => {
192
+ for (const template of [undefined, "\\documentclass[a]{}"]) {
193
+ const r = await newIn(["--venue", "./venues/bare.jsonc"], {
194
+ "venues/bare.jsonc": JSON.stringify({
195
+ ...(template === undefined ? {} : { template }),
196
+ tex: { packages: { x: ["x.sty"] } },
197
+ }),
198
+ });
199
+ expect(readFileSync(join(r.dir, "paper.tex"), "utf8")).toContain(
200
+ "\\documentclass{article}",
201
+ );
202
+ }
203
+ });
204
+
180
205
  it("🔴 a path is relative to where you run it, and written relative to the paper's file", async () => {
181
206
  const r = await newIn(
182
207
  ["--venue", "./venues/my-workshop.jsonc", "--kind", "short"],
@@ -197,7 +222,7 @@ describe("paperlint new --venue / --kind", () => {
197
222
  const r = await newIn(["--venue", "icse"]);
198
223
  expect(r.code).toBe(2);
199
224
  // Guards: the list is read from the shipped presets, the same list lint prints.
200
- expect(r.err).toContain(shippedPresets(packageVenuesDir()).join(", "));
225
+ expect(r.err).toContain(shippedPresets(presetsDir()).join(", "));
201
226
  expect(r.err).toMatch(/--venue icse: no such venue preset/);
202
227
  expect(existsSync(r.dir)).toBe(false);
203
228
  });
@@ -277,7 +302,7 @@ describe("chooseVenue — on a terminal", () => {
277
302
  value: { extends: "paperlint:agenticdev", kind: "full" },
278
303
  });
279
304
  expect(t.asked[0]).toContain(
280
- [...shippedPresets(packageVenuesDir()), "none"].join(" / "),
305
+ [...shippedPresets(presetsDir()), "none"].join(" / "),
281
306
  );
282
307
  expect(t.asked[1]).toMatch(/kind: short \/ full \/ demo \/ later/);
283
308
  });
package/src/new-paper.ts CHANGED
@@ -26,6 +26,7 @@ import {
26
26
  import { join } from "node:path";
27
27
  import { fileURLToPath } from "node:url";
28
28
  import { CONFIG_FILE } from "#lib/paper-config";
29
+ import { isRecord } from "./domain/record.ts";
29
30
 
30
31
  export type PaperFormat = "tex" | "md";
31
32
  export const FORMATS: readonly PaperFormat[] = ["tex", "md"];
@@ -103,6 +104,11 @@ export interface VenueSetting {
103
104
  /** `paperlint:<name>`, or a path relative to the paper's `paperlint.json`. */
104
105
  readonly extends: string;
105
106
  readonly kind: string | null;
107
+ /**
108
+ * The new `paper.tex` set in the class the preset's template names, so `tex/template` passes on
109
+ * the paper `new` just wrote. Absent or null: the template's class is kept.
110
+ */
111
+ readonly setClass?: ((paperTex: string) => string) | null;
106
112
  }
107
113
 
108
114
  /**
@@ -115,15 +121,18 @@ function withVenue(
115
121
  src: string,
116
122
  venue: VenueSetting,
117
123
  ): { ok: true; text: string } | { ok: false; reason: string } {
118
- let obj: Record<string, unknown>;
124
+ let obj: unknown;
119
125
  try {
120
- obj = JSON.parse(text) as Record<string, unknown>;
126
+ obj = JSON.parse(text);
121
127
  } catch {
128
+ obj = null;
129
+ }
130
+ // Not JSON, or JSON that is not an object: there is no field to write "extends" into.
131
+ if (!isRecord(obj))
122
132
  return {
123
133
  ok: false,
124
134
  reason: `--venue cannot be written into ${src}: it is not plain JSON — set "extends" by hand`,
125
135
  };
126
- }
127
136
  const out: Record<string, unknown> = { ...obj, extends: venue.extends };
128
137
  delete out["$comment"];
129
138
  if (venue.kind !== null) out["kind"] = venue.kind;
@@ -160,6 +169,9 @@ function fromTemplate(
160
169
  if (!existsSync(src))
161
170
  return { ok: false, reason: `no template for ${file}: ${src} is missing` };
162
171
  const text = readFileSync(src, "utf8").split("{{name}}").join(name);
172
+ const setClass = venue?.setClass ?? null;
173
+ if (file === SOURCE_FILE.tex && setClass !== null)
174
+ return { ok: true, value: { file, text: setClass(text), from } };
163
175
  if (file !== CONFIG_FILE || venue === null)
164
176
  return { ok: true, value: { file, text, from } };
165
177
  const edited = withVenue(text, src, venue);
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Where the package's own data lives, wherever the package is installed (its checkout,
3
+ * `node_modules`, a plugin cache). This file sits one level below the package root both as a source
4
+ * (`src/`) and as a build (`dist/`), so `..` is the root in either.
5
+ *
6
+ * presets/ the venue presets (`paperlint:<name>`) and their JSON Schema
7
+ * presets/tex/ the LaTeX inputs `paperlint build` puts on TEXINPUTS
8
+ */
9
+ import { fileURLToPath } from "node:url";
10
+
11
+ /** The shipped venue presets and `venue-profile.schema.json`. */
12
+ export const presetsDir = (): string =>
13
+ fileURLToPath(new URL("../presets", import.meta.url));
14
+
15
+ /** The LaTeX inputs a paper `\input`s by name (`paper-guards`, a venue's numbers). */
16
+ export const texInputsDir = (): string =>
17
+ fileURLToPath(new URL("../presets/tex", import.meta.url));
@@ -8,7 +8,7 @@
8
8
  * 74 errors — parse errors and "Definition for rule … was not found" — none of them on a paper
9
9
  * file, and the run failed. Nothing in the settings could turn that off.
10
10
  */
11
- import { ESLint, type Linter } from "eslint";
11
+ import { ESLint } from "eslint";
12
12
  import {
13
13
  mkdirSync,
14
14
  mkdtempSync,
@@ -19,7 +19,7 @@ import {
19
19
  import { tmpdir } from "node:os";
20
20
  import { join } from "node:path";
21
21
  import { afterEach, describe, expect, it } from "vitest";
22
- import { buildConfig, run } from "./cli.ts";
22
+ import { buildConfig, eslintConfig, run } from "./cli.ts";
23
23
  import {
24
24
  narrowToOwners,
25
25
  ownedPatterns,
@@ -28,6 +28,10 @@ import {
28
28
  scopeToOwned,
29
29
  } from "./paper-files.ts";
30
30
  import { lintReport } from "../test/lint-report.ts";
31
+ import { z } from "zod";
32
+
33
+ /** What `calculateConfigForFile` answers for a file inside the config's scope, read for its rules. */
34
+ const Computed = z.object({ rules: z.record(z.string(), z.unknown()) });
31
35
 
32
36
  const dirs: string[] = [];
33
37
  afterEach(() => {
@@ -232,25 +236,24 @@ describe("`rules` blocks — where the rule lands", () => {
232
236
  const eslint = new ESLint({
233
237
  cwd: root,
234
238
  overrideConfigFile: true,
235
- overrideConfig: buildConfig(
236
- {
237
- rules: [
238
- {
239
- basePath: root,
240
- files: ["papers/**"],
241
- rules: { "paper/source": "warn", "paper/section-word": "off" },
242
- },
243
- ],
244
- },
245
- null,
246
- ) as Linter.Config[],
239
+ overrideConfig: eslintConfig(
240
+ buildConfig(
241
+ {
242
+ rules: [
243
+ {
244
+ basePath: root,
245
+ files: ["papers/**"],
246
+ rules: { "paper/source": "warn", "paper/section-word": "off" },
247
+ },
248
+ ],
249
+ },
250
+ null,
251
+ ),
252
+ ),
247
253
  });
248
254
  const on = async (file: string) =>
249
- (
250
- (await eslint.calculateConfigForFile(join(root, file))) as {
251
- rules: Record<string, unknown>;
252
- }
253
- ).rules;
255
+ Computed.parse(await eslint.calculateConfigForFile(join(root, file)))
256
+ .rules;
254
257
  const status = await on("papers/a/PIPELINE-STATUS.md");
255
258
  expect(status["paper/source"]).toEqual([1]);
256
259
  expect(status["paper/section-word"]).toBeUndefined();
@@ -40,16 +40,7 @@
40
40
  * - later global-ignore patterns win, across blocks, in config order;
41
41
  * - `files: [["a", "b"]]` matches a file only when BOTH patterns match.
42
42
  */
43
- import type { RuleBlock, RuleEntry } from "./rules-config.ts";
44
-
45
- /** What this module reads of a config block. */
46
- interface Block {
47
- readonly files?: readonly unknown[];
48
- readonly ignores?: readonly unknown[];
49
- readonly plugins?: Readonly<
50
- Record<string, { readonly rules?: Readonly<Record<string, unknown>> }>
51
- >;
52
- }
43
+ import type { ConfigBlock, RuleBlock, RuleEntry } from "./rules-config.ts";
53
44
 
54
45
  /**
55
46
  * A set of files paperlint lints: an own block's globs and the exceptions that block makes. A file is
@@ -99,20 +90,18 @@ const merged = (scopes: readonly OwnedScope[]): OwnedScope[] => {
99
90
  * THE FILES PAPERLINT LINTS: every own block that names `files`, with its `ignores`. A block without
100
91
  * `files` (a global ignore, or the block registering `pdf` for every file) claims nothing.
101
92
  */
102
- export function ownedScopes(own: readonly unknown[]): OwnedScope[] {
93
+ export function ownedScopes(own: readonly ConfigBlock[]): OwnedScope[] {
103
94
  return merged(
104
- own
105
- .map((raw) => raw as Block)
106
- .flatMap((b) =>
107
- b.files === undefined
108
- ? []
109
- : [{ files: globsOf(b.files), ignores: stringsOf(b.ignores) }],
110
- ),
95
+ own.flatMap((b) =>
96
+ b.files === undefined
97
+ ? []
98
+ : [{ files: globsOf(b.files), ignores: stringsOf(b.ignores) }],
99
+ ),
111
100
  );
112
101
  }
113
102
 
114
103
  /** Every glob paperlint's own blocks lint, once, in config order. */
115
- export function ownedPatterns(own: readonly unknown[]): string[] {
104
+ export function ownedPatterns(own: readonly ConfigBlock[]): string[] {
116
105
  return [...new Set(ownedScopes(own).flatMap((s) => s.files))];
117
106
  }
118
107
 
@@ -134,7 +123,7 @@ export function scopeToOwned(owned: readonly string[]): {
134
123
  }
135
124
 
136
125
  /** Every `<plugin>/<rule>` a block registers. */
137
- const ruleIdsOf = (b: Block): string[] =>
126
+ const ruleIdsOf = (b: ConfigBlock): string[] =>
138
127
  Object.entries(b.plugins ?? {}).flatMap(([name, plugin]) =>
139
128
  Object.keys(plugin.rules ?? {}).map((rule) => `${name}/${rule}`),
140
129
  );
@@ -145,12 +134,11 @@ const ruleIdsOf = (b: Block): string[] =>
145
134
  * one file more.
146
135
  */
147
136
  export function ruleOwners(
148
- own: readonly unknown[],
137
+ own: readonly ConfigBlock[],
149
138
  ): Map<string, readonly OwnedScope[]> {
150
139
  const all = ownedScopes(own);
151
140
  const found = new Map<string, OwnedScope[]>();
152
- for (const raw of own) {
153
- const b = raw as Block;
141
+ for (const b of own) {
154
142
  const scopes =
155
143
  b.files === undefined
156
144
  ? all
@@ -171,17 +159,18 @@ export function narrowToOwners(
171
159
  block: RuleBlock,
172
160
  owners: ReadonlyMap<string, readonly OwnedScope[]>,
173
161
  ): ScopedBlock[] {
162
+ // Keyed by the owner's JSON, so rules with equal owners share one group of blocks.
174
163
  const groups = new Map<string, Record<string, RuleEntry>>();
164
+ const ownerOf = new Map<string, readonly OwnedScope[]>();
175
165
  for (const [id, entry] of Object.entries(block.rules)) {
176
166
  const owner = owners.get(id);
177
167
  if (owner === undefined) continue;
178
168
  const key = JSON.stringify(owner);
179
169
  groups.set(key, { ...groups.get(key), [id]: entry });
170
+ ownerOf.set(key, owner);
180
171
  }
181
- return [...groups].flatMap(([key, rules]) =>
182
- (JSON.parse(key) as OwnedScope[]).map((scope) =>
183
- within(block, scope, rules),
184
- ),
172
+ return [...ownerOf].flatMap(([key, owner]) =>
173
+ owner.map((scope) => within(block, scope, { ...groups.get(key) })),
185
174
  );
186
175
  }
187
176
 
@@ -232,6 +232,9 @@ describe("paperlint lint — the venue preset's rules", () => {
232
232
  extends: "paperlint:agenticdev",
233
233
  kind: "short",
234
234
  }),
235
+ // In agenticdev's class, or tex/template would be the finding.
236
+ "papers/a/paper.tex":
237
+ "\\documentclass[sigconf]{acmart}\n\\begin{document}x\\end{document}\n",
235
238
  });
236
239
  const r = await lint(root);
237
240
  expect(r.code).toBe(0);
@@ -282,7 +285,9 @@ describe("paperlint lint — the paper over its preset, and the project's own pr
282
285
  });
283
286
  const r = await lint(root);
284
287
  expect(r.code).toBe(1);
285
- expect(r.out).toMatch(/acm-sigconf, agenticdev, aisec, realm/);
288
+ expect(r.out).toMatch(
289
+ /acm-sigconf, agenticdev, aidc, aisec, ieee-conference, realm/,
290
+ );
286
291
  });
287
292
  });
288
293
 
@@ -4,7 +4,7 @@
4
4
  */
5
5
  import { describe, expect, it } from "vitest";
6
6
  import { memoryFiles } from "./adapters/memory/index.ts";
7
- import type { AbsolutePath } from "./domain/paths.ts";
7
+ import { absolutePath } from "./domain/paths.ts";
8
8
  import { findProjectRoot } from "../lib/paper-config.ts";
9
9
  import {
10
10
  paperRules,
@@ -215,7 +215,7 @@ describe("findProjectRoot — where the root paperlint.json is looked for", () =
215
215
  [`${PAPER}/paperlint.json`]: '{"kind":"research"}',
216
216
  [`${ROOT}/paperlint.json`]: '{"extends":"paperlint:aisec"}',
217
217
  });
218
- const isFile = (p: string) => files.isFile(p as AbsolutePath);
218
+ const isFile = (p: string) => files.isFile(absolutePath(p));
219
219
  expect(findProjectRoot(PAPER, isFile)).toBe(ROOT);
220
220
  // No root file anywhere: the package.json directory.
221
221
  expect(findProjectRoot(PAPER, (p) => p === `${ROOT}/package.json`)).toBe(
@@ -22,7 +22,7 @@ import {
22
22
  SETTINGS_KEYS,
23
23
  findProjectRoot,
24
24
  } from "#lib/paper-config";
25
- import type { AbsolutePath } from "./domain/paths.ts";
25
+ import { callerPath } from "./caller-path.ts";
26
26
  import { err, ok, type Result } from "./domain/result.ts";
27
27
  import type { Files } from "./ports/files.ts";
28
28
  import {
@@ -114,14 +114,12 @@ export function parsePaperSettings(
114
114
  });
115
115
  }
116
116
 
117
- const at = (p: string): AbsolutePath => p as AbsolutePath;
118
-
119
117
  /** A JSON file through `files`: undefined when absent, the problem when it does not parse. */
120
118
  function readJson(
121
119
  files: Files,
122
120
  path: string,
123
121
  ): Result<unknown, SettingsProblem> {
124
- const bytes = files.readBytes(at(path));
122
+ const bytes = files.readBytes(callerPath(path));
125
123
  if (bytes === null) return ok(undefined);
126
124
  try {
127
125
  return ok(JSON.parse(new TextDecoder().decode(bytes)));
@@ -139,7 +137,9 @@ function rootDefaults(
139
137
  files: Files,
140
138
  paperDir: string,
141
139
  ): Result<Defaults | null, SettingsProblem> {
142
- const root = findProjectRoot(dirname(paperDir), (p) => files.isFile(at(p)));
140
+ const root = findProjectRoot(dirname(paperDir), (p) =>
141
+ files.isFile(callerPath(p)),
142
+ );
143
143
  const json = readJson(files, join(root, CONFIG_FILE));
144
144
  if (!json.ok) return json;
145
145
  if (!isObject(json.value)) return ok(null);