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
@@ -62,10 +62,12 @@ type Resolved = Required<Omit<EngineOptions, "install">> & {
62
62
  };
63
63
 
64
64
  /** The options a caller actually set — `undefined` must still get its default, so no plain spread. */
65
- const defined = <T extends object>(o: T): Partial<T> =>
66
- Object.fromEntries(
67
- Object.entries(o).filter(([, v]) => v !== undefined),
68
- ) as Partial<T>;
65
+ function defined<T extends object>(o: T): Partial<T> {
66
+ const out: Partial<T> = {};
67
+ // `for…in` hands out `o`'s keys typed as its keys; an options literal has no inherited ones.
68
+ for (const k in o) if (o[k] !== undefined) out[k] = o[k];
69
+ return out;
70
+ }
69
71
 
70
72
  function withDefaults(o: EngineOptions): Resolved {
71
73
  const r = {
@@ -133,7 +135,10 @@ const names = (missing: readonly string[]): string =>
133
135
  `${String(missing.length)} package(s): ${missing.join(", ")}`;
134
136
 
135
137
  /** The one line a run without a human gets. It names the command and what is missing. */
136
- export function refusal(d: EngineDecision, facts: EngineFacts): string {
138
+ export function refusal(
139
+ d: EngineDecision,
140
+ facts: Pick<EngineFacts, "system">,
141
+ ): string {
137
142
  const missing = d.kind === "refuse" || d.kind === "ask" ? d.missing : [];
138
143
  const sys =
139
144
  facts.system && facts.system.missing.length
@@ -4,7 +4,7 @@
4
4
  * The process runner is replaced by a fake that writes what pdflatex and bibtex would write, so
5
5
  * these assertions check the SHELL's decisions: what is run, with which environment, in which
6
6
  * order, and what is left on disk. Whether a real pdflatex produces a real PDF is the other half,
7
- * `test/e2e/build.ts`.
7
+ * `test/e2e/tex/build.e2e.ts`.
8
8
  *
9
9
  * 🔴 THE TWO THINGS PINNED DOWN HERE THAT A RETURNED OBJECT CANNOT SHOW: a paper-supplied
10
10
  * `build.sh` is never executed (checked by the trace it would leave on disk and by the list of
@@ -40,9 +40,7 @@ const {
40
40
  STEPS,
41
41
  IGNORED_SCRIPTS,
42
42
  } = await import(join(HERE, "build.ts"));
43
- const { packageVenuesDir } = await import(
44
- join(HERE, "..", "skills", "paper-pipeline", "scripts", "consumer.mjs")
45
- );
43
+ const { texInputsDir } = await import(join(HERE, "package-dirs.ts"));
46
44
 
47
45
  const check = createChecker();
48
46
 
@@ -137,12 +135,12 @@ const fakeRead = async (pdf) => {
137
135
  const CLEAN_TEX = "\\documentclass{article}\\begin{document}x\\end{document}";
138
136
 
139
137
  try {
140
- // ── inputs: paperlint's own venues directory, no configuration ───────────────────────────────
138
+ // ── inputs: paperlint's own TeX inputs directory, no configuration ────────────────────────────
141
139
  // First: the facts below resolve a paper's venue preset in this same directory, so a wrong one
142
140
  // must be named HERE, not as a missing venue label two checks later.
143
- const venues = packageVenuesDir();
141
+ const venues = texInputsDir();
144
142
  check(
145
- "the venues directory is paperlint's own and holds paper-guards.tex",
143
+ "the TeX inputs directory is paperlint's own and holds paper-guards.tex",
146
144
  existsSync(join(venues, "paper-guards.tex")),
147
145
  );
148
146
  check(
package/src/build.test.ts CHANGED
@@ -8,7 +8,7 @@ import assert from "node:assert/strict";
8
8
  import { existsSync, writeFileSync } from "node:fs";
9
9
  import { join } from "node:path";
10
10
  import { test } from "vitest";
11
- import { useTempDir, writeTree } from "../test/support.ts";
11
+ import { present, useTempDir, writeTree } from "../test/support.ts";
12
12
  import {
13
13
  bibInput,
14
14
  buildPaper,
@@ -64,16 +64,16 @@ function bibtexRun(f: Fake, cwd: string) {
64
64
  }
65
65
  function fakeRun(f: Fake): NonNullable<BuildOptions["run"]> {
66
66
  let pass = 0;
67
- return ((bin: string, _args: string[], opts: { cwd: string }) => {
67
+ return (bin, _args, opts) => {
68
68
  if (bin === f.missing)
69
69
  return {
70
70
  error: Object.assign(new Error("spawn ENOENT"), { code: "ENOENT" }),
71
71
  status: null,
72
72
  };
73
- return bin === "pdflatex"
74
- ? latexPass(f, opts.cwd, ++pass)
75
- : bibtexRun(f, opts.cwd);
76
- }) as never;
73
+ // The build always runs a pass in the paper's directory.
74
+ const cwd = present(opts.cwd, "the pass's cwd");
75
+ return bin === "pdflatex" ? latexPass(f, cwd, ++pass) : bibtexRun(f, cwd);
76
+ };
77
77
  }
78
78
 
79
79
  const NO_GEOMETRY: Geometry = {
@@ -146,11 +146,10 @@ test("documentclass: an empty name is no class; a macro inside the name contribu
146
146
  // `\documentclass` came back bare has no name to read, and is no class.
147
147
  assert.equal(
148
148
  parseDocumentclass("\\documentclass", {
149
- parse: () =>
150
- ({
151
- type: "root",
152
- content: [{ type: "macro", content: "documentclass" }],
153
- }) as never,
149
+ parse: () => ({
150
+ type: "root",
151
+ content: [{ type: "macro", content: "documentclass" }],
152
+ }),
154
153
  }),
155
154
  null,
156
155
  );
package/src/build.ts CHANGED
@@ -34,7 +34,7 @@
34
34
  * The decision of the LaTeX loop is `latex-loop.ts`, reading the log is `latex-log.ts`; both are
35
35
  * pure. This file is the shell: it runs processes through the injected `run` (the existing port —
36
36
  * `spawnSync` by default) and reads the files a pass left behind. Where paperlint's own files live is
37
- * answered by `consumer.mjs`, the one module allowed to know it (rule 10).
37
+ * answered by `package-dirs.ts`, the one module allowed to know it (rule 10).
38
38
  */
39
39
  // eslint-disable-next-line boundaries/dependencies -- legacy I/O, moves behind a port in #76
40
40
  import { existsSync, readdirSync, readFileSync, rmSync } from "node:fs";
@@ -44,7 +44,7 @@ import { createHash } from "node:crypto";
44
44
  import { delimiter, join, relative } from "node:path";
45
45
  // eslint-disable-next-line boundaries/dependencies -- legacy layer, moves behind a port in #76
46
46
  import { getParser } from "@unified-latex/unified-latex-util-parse";
47
- import { packageVenuesDir } from "../skills/paper-pipeline/scripts/consumer.mjs";
47
+ import { texInputsDir } from "./package-dirs.ts";
48
48
  import { CONFIG_FILE } from "#lib/paper-config";
49
49
  import {
50
50
  declaredVenue,
@@ -61,7 +61,8 @@ import { banalMeasurer, parseBanalSettings } from "./adapters/banal/index.ts";
61
61
  // eslint-disable-next-line boundaries/dependencies -- legacy layer, moves behind a port in #76
62
62
  import { hostDirs, nodeAdapters, nodeFiles } from "./adapters/node/index.ts";
63
63
  import { whyNoGeometry } from "./domain/geometry.ts";
64
- import type { AbsolutePath } from "./domain/paths.ts";
64
+ import { callerPath } from "./caller-path.ts";
65
+ import type { Runner } from "./engine.ts";
65
66
  import type { Files } from "./ports/files.ts";
66
67
  import type { MeasureGeometry } from "./ports/measure-geometry.ts";
67
68
  import type { CheckReferences } from "./ports/check-references.ts";
@@ -76,7 +77,6 @@ import {
76
77
  import {
77
78
  nextStep,
78
79
  summarize,
79
- TRACKED,
80
80
  type BibInput,
81
81
  type Hashes,
82
82
  type Observation,
@@ -128,7 +128,7 @@ export interface PaperFacts {
128
128
  }
129
129
 
130
130
  /** A process runner with `spawnSync`'s shape — the port the harness replaces. */
131
- export type Runner = typeof spawnSync;
131
+ export type { Runner };
132
132
 
133
133
  /** What a step's `run` receives. `env` is the environment accumulated by the steps before it. */
134
134
  export interface BuildContext {
@@ -268,11 +268,11 @@ export const inputsStep: BuildStep = {
268
268
  required: false,
269
269
  applies: () => ({
270
270
  yes: true,
271
- why: `TEXINPUTS += ${packageVenuesDir()}`,
271
+ why: `TEXINPUTS += ${texInputsDir()}`,
272
272
  }),
273
273
  run: (ctx) => ({
274
274
  ok: true,
275
- env: withTexInputs(ctx.env, [packageVenuesDir()]),
275
+ env: withTexInputs(ctx.env, [texInputsDir()]),
276
276
  }),
277
277
  };
278
278
 
@@ -286,9 +286,8 @@ function hashes(paperDir: string): Hashes {
286
286
  const p = join(paperDir, `${JOB}.${ext}`);
287
287
  return existsSync(p) ? sha(readFileSync(p)) : null;
288
288
  };
289
- const out: Record<string, string | null> = {};
290
- for (const t of TRACKED) out[t] = h(t);
291
- return out as Hashes;
289
+ // A literal of the mapped type: a new tracked extension without a hash here is a compile error.
290
+ return { aux: h("aux"), toc: h("toc"), out: h("out"), bbl: h("bbl") };
292
291
  }
293
292
 
294
293
  const readOr = (path: string, enc: BufferEncoding): string | null =>
@@ -387,8 +386,8 @@ function latexPass(
387
386
 
388
387
  /** The two streams of a finished run, as they really arrive (see `printed`). */
389
388
  interface Streams {
390
- readonly stdout: string | undefined;
391
- readonly stderr: string | undefined;
389
+ readonly stdout?: string | undefined;
390
+ readonly stderr?: string | undefined;
392
391
  }
393
392
 
394
393
  /**
@@ -694,7 +693,7 @@ function defaultMeasurer(
694
693
  return banalMeasurer(
695
694
  nodeAdapters({ tmpDir: dirs.tmp }),
696
695
  parseBanalSettings(b.env, dirs),
697
- b.projectRoot as AbsolutePath,
696
+ callerPath(b.projectRoot),
698
697
  );
699
698
  }
700
699
 
@@ -0,0 +1,13 @@
1
+ /**
2
+ * A path as a caller gave it, made absolute where it enters a `Files` call.
3
+ *
4
+ * The app's use cases take paper directories and files as strings, and some callers pass them as
5
+ * typed on a command line (`skills/render-paper/extract-pdf-facts.mjs <paper.pdf>`). A relative one
6
+ * has always meant the process's cwd — `node:fs` resolved it there. Resolving it HERE, the same way,
7
+ * makes the `AbsolutePath` brand a checked fact rather than an assertion: the value handed to the port
8
+ * is absolute, and nothing downstream has to wonder.
9
+ */
10
+ import { resolve } from "node:path";
11
+ import { absolutePath, type AbsolutePath } from "./domain/paths.ts";
12
+
13
+ export const callerPath = (p: string): AbsolutePath => absolutePath(resolve(p));
@@ -1148,7 +1148,7 @@ check(
1148
1148
  // pnpm: node_modules/vigiles/dist/cli.js DOES NOT
1149
1149
  // The old wiring addressed the runtime from the project root and did not resolve under pnpm,
1150
1150
  // and `|| exit 2` on PreToolUse(Bash) turned that into a block on ANY command. The end-to-end
1151
- // half (both installs, real processes) lives in `test/e2e/install.ts`; here — the verdicts.
1151
+ // half (both installs, real processes) lives in `test/e2e/install/install.e2e.ts`; here — the verdicts.
1152
1152
  {
1153
1153
  const calls = [];
1154
1154
  const fake = (code) => (bin, args, opts) => {
@@ -1711,8 +1711,6 @@ console.log(
1711
1711
  typo.out.includes('paperlint.json: unknown key "typographyDept"'),
1712
1712
  );
1713
1713
  settings({
1714
- ledger: "x.jsonl",
1715
- citeChecks: "c",
1716
1714
  timezone: "UTC",
1717
1715
  contactEmail: "a@b.c",
1718
1716
  triggerCases: "t.mjs",
package/src/cli.test.ts CHANGED
@@ -10,6 +10,7 @@ import { existsSync } from "node:fs";
10
10
  import { dirname, join } from "node:path";
11
11
  import { test } from "vitest";
12
12
  import { runNode, useTempDir, writeTree } from "../test/support.ts";
13
+ import type { runToolchain } from "./toolchain.ts";
13
14
  import {
14
15
  chooseVenue,
15
16
  initTexLive,
@@ -22,6 +23,7 @@ import {
22
23
  silentOptionalRules,
23
24
  toolchainTex,
24
25
  } from "./cli.ts";
26
+ import { texInputsDir } from "./package-dirs.ts";
25
27
 
26
28
  const root = useTempDir("cli-test-");
27
29
 
@@ -57,6 +59,18 @@ test("parseArgs: the boolean flags of build and toolchain, and a value flag with
57
59
  );
58
60
  });
59
61
 
62
+ test("🔴 parseSettings refuses a mistyped `structure` instead of letting `lint` crash on it", () => {
63
+ // Guards: `markers: 5` used to pass the boundary, and `checkStructure` then threw
64
+ // "rules.markers.some is not a function" from inside `paperlint lint`.
65
+ for (const structure of [{ markers: 5 }, "yes", { requireOneOf: ["a"] }]) {
66
+ const r = parseSettings({ structure }, "paperlint.json", "/r");
67
+ assert.equal(r.ok, false, JSON.stringify(structure));
68
+ assert.match(r.error, /^paperlint\.json: "structure" must be false/);
69
+ }
70
+ const off = parseSettings({ structure: false }, "paperlint.json", "/r");
71
+ assert.equal(off.ok && off.value.structure, false);
72
+ });
73
+
60
74
  test("parseSettings refuses a non-object, names one unknown key or several, and a mistyped default", () => {
61
75
  const error = (json: unknown) => {
62
76
  const r = parseSettings(json, "paperlint.json", "/r");
@@ -146,7 +160,7 @@ test("runHook: a runtime with no cli.js beside it, and a hook killed by a signal
146
160
  });
147
161
  const killed = runHook("paper-edit-guard", {
148
162
  err: (m: string) => messages.push(m),
149
- run: (() => ({ status: null, signal: "SIGKILL" })) as never,
163
+ run: () => ({ status: null, signal: "SIGKILL" }),
150
164
  });
151
165
  assert.deepEqual(
152
166
  { noCli, killed, messages: messages.map((m) => m.split(" from ")[0]) },
@@ -235,14 +249,7 @@ writeTree(noTexDir, {
235
249
  "papers/tex/paper.tex":
236
250
  "\\documentclass{article}\\begin{document}x\\end{document}\n",
237
251
  });
238
- const venues = join(
239
- import.meta.dirname,
240
- "..",
241
- "skills",
242
- "submit-paper",
243
- "references",
244
- "venues",
245
- );
252
+ const venues = texInputsDir();
246
253
 
247
254
  test("build: a markdown-only paper is refused", async () => {
248
255
  const md = await withoutTex(noTexDir, () =>
@@ -363,7 +370,8 @@ test("a paper's paperlint.json that does not parse stops the lint with its path"
363
370
  });
364
371
 
365
372
  test("silentOptionalRules: a config with no rules turns nothing on", async () => {
366
- assert.deepEqual(await silentOptionalRules({} as never, [], {}), []);
373
+ const eslint = { calculateConfigForFile: () => Promise.resolve(undefined) };
374
+ assert.deepEqual(await silentOptionalRules(eslint, [], {}), []);
367
375
  });
368
376
 
369
377
  test("init with no path and --format md sets up the current directory", async () => {
@@ -538,8 +546,8 @@ test("ownVersion: the manifest's version; an unreadable manifest or a non-string
538
546
 
539
547
  test("init's TeX Live install runs the toolchain over the project's papers, never a check", () => {
540
548
  const dir = writeTree(join(root, "init-tex"), { "package.json": "{}" });
541
- const calls: unknown[] = [];
542
- const fake = ((o: unknown) => (calls.push(o), 0)) as never;
549
+ const calls: Parameters<typeof runToolchain>[0][] = [];
550
+ const fake: typeof runToolchain = (o) => (calls.push(o), 0);
543
551
  const log = () => {};
544
552
  const here = initTexLive(
545
553
  parseArgs(["init"]),
@@ -553,10 +561,7 @@ test("init's TeX Live install runs the toolchain over the project's papers, neve
553
561
  );
554
562
  assert.deepEqual([here.install(), there.install()], [0, 0]);
555
563
  assert.deepEqual(
556
- calls.map((c) => {
557
- const o = c as { check: boolean; tex: unknown; banal: unknown };
558
- return [o.check, o.tex, typeof o.banal];
559
- }),
564
+ calls.map((c) => [c.check, c.tex, typeof c.banal]),
560
565
  [
561
566
  [false, toolchainTex(dir), "object"],
562
567
  [false, toolchainTex(join(dir, "sub")), "object"],
package/src/cli.ts CHANGED
@@ -30,11 +30,10 @@ import { spawnSync } from "node:child_process";
30
30
  import { fileURLToPath } from "node:url";
31
31
  import { join, dirname, resolve, relative, basename, sep } from "node:path";
32
32
  import markdown from "@eslint/markdown";
33
+ import { z } from "zod";
33
34
  // Types come from consumer.d.mts beside it.
34
- import {
35
- isMain,
36
- packageVenuesDir,
37
- } from "../skills/paper-pipeline/scripts/consumer.mjs";
35
+ import { isMain } from "../skills/paper-pipeline/scripts/consumer.mjs";
36
+ import { presetsDir } from "./package-dirs.ts";
38
37
  export { isMain };
39
38
  import type { Args, PaperlintConfig, ConfigRead } from "./types.ts";
40
39
  import {
@@ -47,8 +46,10 @@ import { prepareEngine } from "./build-engine.ts";
47
46
  import { cacheRoot, cachedTree, runToolchain } from "./toolchain.ts";
48
47
  import { banalInstaller, parseBanalSettings } from "./adapters/banal/index.ts";
49
48
  import { curlDownload } from "./adapters/curl/index.ts";
49
+ import { latexReader } from "./adapters/latex/index.ts";
50
50
  import { hostDirs, nodeAdapters, nodeFiles } from "./adapters/node/index.ts";
51
51
  import { VENUE_RULE_LEVELS, venueRules } from "./venue-rules.ts";
52
+ import { TEX_VENUE_RULE_LEVELS, texVenueRules } from "./tex-venue-rules.ts";
52
53
  import {
53
54
  paperRules,
54
55
  stringFields,
@@ -71,6 +72,7 @@ import {
71
72
  shippedPresets,
72
73
  SHIPPED_PREFIX,
73
74
  type PaperPreset,
75
+ type PresetTemplate,
74
76
  } from "./presets.ts";
75
77
  import type { ToolInstaller } from "./ports/tool-installer.ts";
76
78
  import {
@@ -112,6 +114,7 @@ import {
112
114
  shippedRuleIds,
113
115
  unknownKeys,
114
116
  type Parsed,
117
+ type ConfigBlock,
115
118
  type RuleBlock,
116
119
  type RuleEntry,
117
120
  } from "./rules-config.ts";
@@ -127,9 +130,10 @@ import reviewFrontmatter from "#eslint-rules/review-frontmatter";
127
130
  import siblingFrontmatter from "#eslint-rules/sibling-frontmatter";
128
131
  import pdfRules from "#eslint-rules/pdf-last-page-balance";
129
132
  import { messageOf } from "./domain/text.ts";
133
+ import { isRecord } from "./domain/record.ts";
130
134
 
131
135
  /** The shipped venue presets, read from the package's venues directory — the one list. */
132
- const SHIPPED_VENUES = (): string[] => shippedPresets(packageVenuesDir());
136
+ const SHIPPED_VENUES = (): string[] => shippedPresets(presetsDir());
133
137
 
134
138
  const USAGE = `paperlint — machine-checkable gates for a paper kept in git
135
139
 
@@ -220,7 +224,7 @@ settings — paperlint.json, at two levels, one schema. Both are optional.
220
224
  export function buildConfig(
221
225
  opts: PaperlintConfig = {},
222
226
  texLanguage: unknown,
223
- ): unknown[] {
227
+ ): ConfigBlock[] {
224
228
  const paperRules = { ...researchQuestion.rules, ...typography.rules };
225
229
  // The reference rules judge `_build/references.json`, and only on `paper.tex`.
226
230
  const texPaperRules = {
@@ -238,7 +242,7 @@ export function buildConfig(
238
242
  languageOptions: { frontmatter: "yaml" },
239
243
  };
240
244
 
241
- const cfg: unknown[] = [
245
+ const cfg: ConfigBlock[] = [
242
246
  // 🔴 THE PROJECT'S PAPER TEMPLATE IS NOT A PAPER. `paperlint new` reads `<papers>/.template/`, and
243
247
  // its files carry every marker a paper does. Flat config does NOT ignore dot-directories by
244
248
  // default (only `node_modules/` and `.git/`), so without this block `paperlint lint` would lint the
@@ -254,7 +258,7 @@ export function buildConfig(
254
258
  pdf: {
255
259
  rules: {
256
260
  ...pdfRules.rules,
257
- ...venueRules({ files: nodeFiles, venuesDir: packageVenuesDir() }),
261
+ ...venueRules({ files: nodeFiles, venuesDir: presetsDir() }),
258
262
  },
259
263
  },
260
264
  },
@@ -299,7 +303,17 @@ export function buildConfig(
299
303
  cfg.push({
300
304
  files: ["**/paper.tex"],
301
305
  plugins: {
302
- tex: { languages: { latex: texLanguage }, rules: texBuild },
306
+ tex: {
307
+ languages: { latex: texLanguage },
308
+ rules: {
309
+ ...texBuild,
310
+ ...texVenueRules({
311
+ files: nodeFiles,
312
+ venuesDir: presetsDir(),
313
+ latex: latexReader,
314
+ }),
315
+ },
316
+ },
303
317
  paper: { rules: texPaperRules },
304
318
  bib: bibReachable,
305
319
  },
@@ -313,6 +327,7 @@ export function buildConfig(
313
327
  "tex/acm-frontmatter-override": "error",
314
328
  // Silent for a paper whose paperlint.json names no venue (src/venue-rules.ts).
315
329
  ...VENUE_RULE_LEVELS,
330
+ ...TEX_VENUE_RULE_LEVELS,
316
331
  },
317
332
  });
318
333
  // 🔴 ONLY THE FILES THESE BLOCKS CLAIM ARE LINTED (src/paper-files.ts). Without this block ESLint's
@@ -329,6 +344,21 @@ export function buildConfig(
329
344
  return cfg;
330
345
  }
331
346
 
347
+ /**
348
+ * paperlint's blocks as ESLint's config type — the one place the two meet. ESLint types a plugin's
349
+ * rule as one that runs on ANY language; paperlint's run on the language they were written for, so
350
+ * no static proof that a block fits exists (`ConfigBlock`, rules-config.ts). ESLint checks every block
351
+ * when it loads the config and throws on one it cannot use; what is left to check here is that each
352
+ * is an object.
353
+ */
354
+ const isEslintBlock = (b: unknown): b is Linter.Config =>
355
+ typeof b === "object" && b !== null;
356
+
357
+ export function eslintConfig(blocks: readonly ConfigBlock[]): Linter.Config[] {
358
+ const loose: readonly unknown[] = blocks;
359
+ return loose.filter(isEslintBlock);
360
+ }
361
+
332
362
  /**
333
363
  * The rule ids a consumer may name in `rules`: every rule paperlint's own config defines, read off that
334
364
  * config rather than listed again. `@eslint/markdown` is a dependency's plugin, not paperlint's.
@@ -357,9 +387,7 @@ export interface RulePlugin {
357
387
  export function rulePlugins(texLanguage?: unknown): Record<string, RulePlugin> {
358
388
  const out: Record<string, { rules: Record<string, unknown> }> = {};
359
389
  for (const block of buildConfig({}, texLanguage ?? { sentinel: "tex" }))
360
- for (const [name, plugin] of Object.entries(
361
- (block as { plugins?: Record<string, RulePlugin> }).plugins ?? {},
362
- ))
390
+ for (const [name, plugin] of Object.entries(block.plugins ?? {}))
363
391
  if (plugin !== markdown)
364
392
  out[name] = { rules: { ...out[name]?.rules, ...plugin.rules } };
365
393
  if (out["tex"] && texLanguage !== undefined)
@@ -382,12 +410,10 @@ export function rulePlugins(texLanguage?: unknown): Record<string, RulePlugin> {
382
410
  * switched off in THIS run — `paperlint lint` still reports a directive that silences nothing.
383
411
  */
384
412
  export function rulesOff(texLanguage?: unknown): Linter.Config[] {
385
- return [
413
+ return eslintConfig([
386
414
  {
387
415
  name: "paperlint/rules-off",
388
- plugins: rulePlugins(texLanguage) as NonNullable<
389
- Linter.Config["plugins"]
390
- >,
416
+ plugins: rulePlugins(texLanguage),
391
417
  rules: Object.fromEntries([...SHIPPED_RULES].map((id) => [id, "off"])),
392
418
  },
393
419
  {
@@ -395,7 +421,7 @@ export function rulesOff(texLanguage?: unknown): Linter.Config[] {
395
421
  files: PAPER_FILE_PATTERNS,
396
422
  linterOptions: { reportUnusedDisableDirectives: "off" },
397
423
  },
398
- ];
424
+ ]);
399
425
  }
400
426
 
401
427
  /** Whether a rule entry (`"error"`, `2`, `["warn", {…}]`) turns the rule on. */
@@ -412,7 +438,7 @@ export const OPTIONAL_RULES: ReadonlySet<string> = new Set(
412
438
  [...SHIPPED_RULES].filter(
413
439
  (id) =>
414
440
  !buildConfig({}, { sentinel: "tex language" }).some((b) =>
415
- isOn((b as { rules?: Record<string, unknown> }).rules?.[id]),
441
+ isOn(b.rules?.[id]),
416
442
  ),
417
443
  ),
418
444
  );
@@ -429,8 +455,13 @@ export const OPTIONAL_RULES: ReadonlySet<string> = new Set(
429
455
  * papers a and b (#103) — a block that reaches a paper outside this run is not dead. A glob that
430
456
  * reaches no paper of the project at all still fails, from a subset as from the whole.
431
457
  */
458
+ /** What `calculateConfigForFile` answers, read for its rules: undefined for a file outside ESLint's cwd or scope. */
459
+ const ComputedConfig = z
460
+ .object({ rules: z.record(z.string(), z.unknown()).optional() })
461
+ .optional();
462
+
432
463
  export async function silentOptionalRules(
433
- eslint: ESLint,
464
+ eslint: Pick<ESLint, "calculateConfigForFile">,
434
465
  files: readonly string[],
435
466
  opts: PaperlintConfig,
436
467
  ): Promise<string[]> {
@@ -444,8 +475,7 @@ export async function silentOptionalRules(
444
475
  const reached = new Set<string>();
445
476
  for (const f of new Set(files.filter((p) => basename(p) === MAIN))) {
446
477
  // Undefined for a file outside ESLint's cwd or scope: it reaches nothing.
447
- const cfg = (await eslint.calculateConfigForFile(f)) as
448
- { rules?: Record<string, unknown> } | undefined;
478
+ const cfg = ComputedConfig.parse(await eslint.calculateConfigForFile(f));
449
479
  for (const id of turnedOn) if (isOn(cfg?.rules?.[id])) reached.add(id);
450
480
  }
451
481
  return [...turnedOn].filter((id) => !reached.has(id));
@@ -543,6 +573,20 @@ const PAPER_FILE_PATTERNS: string[] = ownedPatterns(
543
573
  buildConfig({}, { sentinel: "tex language" }),
544
574
  );
545
575
 
576
+ /** `structure` as `checkStructure` reads it: off, or lists of file names over the defaults. */
577
+ const Names = z.array(z.string()).exactOptional();
578
+ const Structure = z
579
+ .union([
580
+ z.literal(false),
581
+ z.object({
582
+ markers: Names,
583
+ require: Names,
584
+ requireOneOf: z.array(z.array(z.string())).exactOptional(),
585
+ ignore: Names,
586
+ }),
587
+ ])
588
+ .optional();
589
+
546
590
  /**
547
591
  * The root `paperlint.json` after the boundary: an unknown key is refused by name, the paper
548
592
  * defaults (`extends`, `kind`, `pdf`) are checked the way a paper's own are, and `rules` becomes
@@ -553,10 +597,9 @@ export function parseSettings(
553
597
  where: string,
554
598
  baseDir: string,
555
599
  ): Parsed<PaperlintConfig> {
556
- if (typeof json !== "object" || json === null || Array.isArray(json))
600
+ if (!isRecord(json))
557
601
  return { ok: false, error: `${where}: must be a JSON object` };
558
- const raw = json as Record<string, unknown>;
559
- const opts = raw as PaperlintConfig;
602
+ const raw = json;
560
603
  const unknown = unknownKeys(raw);
561
604
  if (unknown.length > 0)
562
605
  return {
@@ -567,9 +610,22 @@ export function parseSettings(
567
610
  };
568
611
  const defaults = stringFields(raw);
569
612
  if (!defaults.ok) return { ok: false, error: `${where}: ${defaults.error}` };
613
+ const structure = Structure.safeParse(raw["structure"]);
614
+ if (!structure.success)
615
+ return {
616
+ ok: false,
617
+ error: `${where}: "structure" must be false, or an object whose markers, require and ignore are lists of file names and requireOneOf a list of such lists`,
618
+ };
570
619
  const rules = parseRuleBlocks(raw["rules"], where, SHIPPED_RULES, baseDir);
571
620
  if (!rules.ok) return rules;
572
- return { ok: true, value: { ...opts, rules: rules.value } };
621
+ return {
622
+ ok: true,
623
+ value: {
624
+ papersDir: raw[PAPERS_DIR_FIELD],
625
+ structure: structure.data,
626
+ rules: rules.value,
627
+ },
628
+ };
573
629
  }
574
630
 
575
631
  /**
@@ -770,7 +826,7 @@ export function readConfig(
770
826
 
771
827
  /** The papers directory field of the settings, read by its one declared name; the default when absent. */
772
828
  export function papersDirOf(opts: PaperlintConfig): unknown {
773
- const declared = (opts as Record<string, unknown>)[PAPERS_DIR_FIELD];
829
+ const declared = opts.papersDir;
774
830
  return declared === undefined ? DEFAULT_PAPERS_ROOT : declared;
775
831
  }
776
832
 
@@ -839,7 +895,12 @@ export function runHook(
839
895
  createRequire(import.meta.url).resolve(spec),
840
896
  }: {
841
897
  err?: typeof console.error;
842
- run?: typeof spawnSync;
898
+ /** Runs the hook with the terminal's streams: `spawnSync` is one, and only its status is read. */
899
+ run?: (
900
+ file: string,
901
+ args: readonly string[],
902
+ options: { readonly stdio: "inherit" },
903
+ ) => { readonly status: number | null };
843
904
  resolve?: (spec: string) => string;
844
905
  } = {},
845
906
  ): number {
@@ -988,12 +1049,25 @@ export async function chooseVenue(
988
1049
  value: {
989
1050
  extends: spec.value,
990
1051
  kind: kind !== null && kinds.includes(kind) ? kind : null,
1052
+ setClass: classSetter(r.value.template),
991
1053
  label,
992
1054
  kinds,
993
1055
  },
994
1056
  };
995
1057
  }
996
1058
 
1059
+ /**
1060
+ * What `paperlint new` does to the template's paper.tex for a venue: set it in the class the
1061
+ * preset's template names. Null when the preset names no template, or one the reader cannot read
1062
+ * (then `tex/template` says so on the new paper).
1063
+ */
1064
+ function classSetter(
1065
+ t: PresetTemplate | null,
1066
+ ): ((paperTex: string) => string) | null {
1067
+ const cls = t === null ? null : latexReader.template(t.text);
1068
+ return cls === null ? null : (src) => latexReader.withDocumentClass(src, cls);
1069
+ }
1070
+
997
1071
  const bad = (error: string): { ok: false; error: string } => ({
998
1072
  ok: false,
999
1073
  error,
@@ -1168,7 +1242,7 @@ async function runBuild(
1168
1242
  }
1169
1243
 
1170
1244
  /** The presets' deps, wired to the disk and the package's own venues directory. */
1171
- const PRESET_DEPS = { files: nodeFiles, venuesDir: packageVenuesDir() };
1245
+ const PRESET_DEPS = { files: nodeFiles, venuesDir: presetsDir() };
1172
1246
 
1173
1247
  /**
1174
1248
  * What one paper needs from TeX Live: the base set plus its preset chain's `tex`. A paper whose
@@ -1466,7 +1540,7 @@ export async function run(
1466
1540
  const eslint = new ESLint({
1467
1541
  cwd: lintRoot(root, paths),
1468
1542
  overrideConfigFile: true,
1469
- overrideConfig: buildConfig(withPapers, texLanguage) as Linter.Config[],
1543
+ overrideConfig: eslintConfig(buildConfig(withPapers, texLanguage)),
1470
1544
  fix: a.fix,
1471
1545
  });
1472
1546
 
@@ -1543,12 +1617,20 @@ async function firstUnownedFile(
1543
1617
  * is outside paperlint's scope (`all-matched-files-ignored` — a papers directory holding only
1544
1618
  * vendored scripts). Both mean "nothing was linted", which the caller reports itself.
1545
1619
  */
1620
+ /** What a thrown ESLint error says about itself; anything else thrown says nothing. */
1621
+ const EslintFailure = z
1622
+ .object({
1623
+ messageTemplate: z.string().optional(),
1624
+ message: z.string().optional(),
1625
+ })
1626
+ .catch({});
1627
+
1546
1628
  const isEmptySet = (e: unknown): boolean => {
1547
- const fail = e as { messageTemplate?: string; message?: string } | null;
1629
+ const fail = EslintFailure.parse(e);
1548
1630
  return (
1549
- fail?.messageTemplate === "file-not-found" ||
1550
- fail?.messageTemplate === "all-matched-files-ignored" ||
1551
- /No files matching/i.test(String(fail?.message))
1631
+ fail.messageTemplate === "file-not-found" ||
1632
+ fail.messageTemplate === "all-matched-files-ignored" ||
1633
+ /No files matching/i.test(String(fail.message))
1552
1634
  );
1553
1635
  };
1554
1636