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
@@ -40,11 +40,11 @@ function page(items: FakeItem[], fonts: Record<string, unknown>) {
40
40
  }),
41
41
  };
42
42
  }
43
- const doc = (pages: ReturnType<typeof page>[]) =>
44
- ({
45
- numPages: pages.length,
46
- getPage: (i: number) => Promise.resolve(pages[i - 1]),
47
- }) as never;
43
+ const doc = (pages: ReturnType<typeof page>[]) => ({
44
+ numPages: pages.length,
45
+ getPage: (i: number) =>
46
+ Promise.resolve(present(pages[i - 1], `page ${String(i)}`)),
47
+ });
48
48
  const item = (
49
49
  fontName: string,
50
50
  transform: number[],
@@ -141,3 +141,14 @@ test("a failure that is not an Error is named `Error` with its text", () => {
141
141
  detail: "Error: boom",
142
142
  });
143
143
  });
144
+
145
+ test("🔴 a rejection with no value at all is a failure too, not a TypeError out of `readPdf`", () => {
146
+ // Guards: `readPdf` ends in `.catch(failureOf)`, and reading `.name` off `null` threw from inside it.
147
+ assert.deepEqual(
148
+ [failureOf(null), failureOf(undefined)],
149
+ [
150
+ { ok: false, reason: "unreadable", detail: "Error: null" },
151
+ { ok: false, reason: "unreadable", detail: "Error: undefined" },
152
+ ],
153
+ );
154
+ });
package/src/pdf-facts.ts CHANGED
@@ -35,12 +35,67 @@ import type { PageLayout, TextBox } from "./domain/page-layout.ts";
35
35
  // eslint-disable-next-line boundaries/dependencies -- legacy layer, moves behind a port in #76
36
36
  import { fillsFor, isUpright } from "./adapters/pdfjs/fill.ts";
37
37
  import { messageOf } from "./domain/text.ts";
38
+ import { fieldOf, isRecord, numbersOf } from "./domain/record.ts";
38
39
 
39
40
  type PdfJs = Awaited<ReturnType<typeof import("unpdf").getResolvedPDFJS>>;
40
- type Doc = Awaited<ReturnType<PdfJs["getDocument"]>["promise"]>;
41
- type Page = Awaited<ReturnType<Doc["getPage"]>>;
42
- type Item = Awaited<ReturnType<Page["getTextContent"]>>["items"][number];
43
- type TextItem = Extract<Item, { str: string }>;
41
+
42
+ /** A text item as this module reads it; pdf.js's `TextItem` is one. */
43
+ interface TextItem {
44
+ readonly str: string;
45
+ readonly fontName: string;
46
+ /** pdf.js types it `any[]`; `numbersOf` reads it. */
47
+ readonly transform: readonly unknown[];
48
+ readonly width: number;
49
+ readonly height: number;
50
+ }
51
+ /** What else pdf.js lists among a page's items: a marked-content boundary, with no text. */
52
+ interface MarkedContent {
53
+ readonly type: string;
54
+ readonly str?: undefined;
55
+ }
56
+ type Item = TextItem | MarkedContent;
57
+
58
+ /** A page's operators, as `fillsFor` walks them; pdf.js's operator list is one. */
59
+ interface OperatorList {
60
+ readonly fnArray: readonly number[];
61
+ readonly argsArray: readonly unknown[];
62
+ }
63
+
64
+ /** A viewport as this module reads it; pdf.js's `PageViewport` is one. */
65
+ interface Viewport {
66
+ readonly width: number;
67
+ readonly height: number;
68
+ readonly transform: readonly number[];
69
+ convertToViewportPoint(x: number, y: number): readonly unknown[];
70
+ }
71
+
72
+ /**
73
+ * A page as this module reads it. pdf.js's `PDFPageProxy` is one, and so is a test's fake page: the
74
+ * module depends on these five members, not on the rest of pdf.js's page.
75
+ */
76
+ interface Page {
77
+ getOperatorList(): Promise<OperatorList>;
78
+ getTextContent(): Promise<{ readonly items: readonly Item[] }>;
79
+ /** pdf.js's `commonObjs`: loaded objects by id, iterable as `[id, object]` pairs. */
80
+ readonly commonObjs: {
81
+ has(id: string): boolean;
82
+ get(id: string): unknown;
83
+ [Symbol.iterator](): Iterator<readonly unknown[]>;
84
+ };
85
+ getViewport(params: { scale: number }): Viewport;
86
+ }
87
+
88
+ /** A document as this module reads it; pdf.js's `PDFDocumentProxy` is one. */
89
+ interface Doc {
90
+ readonly numPages: number;
91
+ getPage(pageNumber: number): Promise<Page>;
92
+ }
93
+
94
+ /** A viewport point, which pdf.js types as `any[]`: the two numbers it holds. */
95
+ const pointOf = (xy: readonly unknown[]): [number, number] => [
96
+ Number(xy[0]),
97
+ Number(xy[1]),
98
+ ];
44
99
 
45
100
  /**
46
101
  * What `readPdf` measured. `last` is the last page's words; `classifyLastPage` reads it. `layout` is
@@ -95,8 +150,8 @@ const fail = (reason: PdfReadFailure, detail: string): PdfRead => ({
95
150
 
96
151
  /** A font object pdf.js left in `commonObjs`, read into a `RawFont`, or null when it is not one. */
97
152
  export function rawFontOf(id: string, obj: unknown): RawFont | null {
98
- if (typeof obj !== "object" || obj === null) return null;
99
- const o = obj as Record<string, unknown>;
153
+ if (!isRecord(obj)) return null;
154
+ const o = obj;
100
155
  if (!("loadedName" in o) && !("isType3Font" in o)) return null;
101
156
  const str = (v: unknown): string | undefined =>
102
157
  typeof v === "string" ? v : undefined;
@@ -111,7 +166,7 @@ export function rawFontOf(id: string, obj: unknown): RawFont | null {
111
166
  }
112
167
 
113
168
  const isText = (it: Item): it is TextItem =>
114
- "str" in it && it.str.trim() !== "";
169
+ typeof it.str === "string" && it.str.trim() !== "";
115
170
 
116
171
  /** The font pdf.js resolved for `id`, if it did. `get` throws on an unresolved id; `has` does not. */
117
172
  function resolved(page: Page, id: string): unknown {
@@ -124,9 +179,10 @@ function resolved(page: Page, id: string): unknown {
124
179
  */
125
180
  function loadedFonts(page: Page): Map<string, RawFont> {
126
181
  const out = new Map<string, RawFont>();
127
- for (const [id, obj] of page.commonObjs as Iterable<[string, unknown]>) {
128
- const f = rawFontOf(id, obj);
129
- if (f) out.set(id, f);
182
+ // pdf.js yields `[id, object]` pairs, typed `any[]`.
183
+ for (const entry of page.commonObjs) {
184
+ const f = rawFontOf(String(entry[0]), entry[1]);
185
+ if (f) out.set(f.id, f);
130
186
  }
131
187
  return out;
132
188
  }
@@ -167,7 +223,7 @@ async function readPage(
167
223
  * Both are far below any balance tolerance, so the simpler rule stays.
168
224
  */
169
225
  function metricsOf(font: unknown): { ascent: number; descent: number } {
170
- const f = (font ?? {}) as Record<string, unknown>;
226
+ const f = isRecord(font) ? font : {};
171
227
  const ascent = f["ascent"];
172
228
  if (typeof ascent !== "number" || !(ascent > 0))
173
229
  return { ascent: DEFAULT_ASCENT, descent: DEFAULT_DESCENT };
@@ -179,8 +235,8 @@ function metricsOf(font: unknown): { ascent: number; descent: number } {
179
235
  function pageText(page: Page, items: readonly TextItem[]): PageText {
180
236
  const vp = page.getViewport({ scale: 1 });
181
237
  const runs: TextRun[] = items.map((it) => {
182
- const [a = 0, b = 0, , d = 0, e = 0, f = 0] = it.transform as number[];
183
- const [x, baseline] = vp.convertToViewportPoint(e, f) as [number, number];
238
+ const [a = 0, b = 0, , d = 0, e = 0, f = 0] = numbersOf(it.transform);
239
+ const [x, baseline] = pointOf(vp.convertToViewportPoint(e, f));
184
240
  const size = Math.hypot(a, b) || it.height || Math.abs(d);
185
241
  return {
186
242
  text: it.str,
@@ -200,7 +256,7 @@ function pageText(page: Page, items: readonly TextItem[]): PageText {
200
256
 
201
257
  /** The font name pdf.js resolved for an item, else its internal id. */
202
258
  function fontNameOf(font: unknown, id: string): string {
203
- const name = (font as { name?: unknown } | null)?.name;
259
+ const name = fieldOf(font, "name");
204
260
  return typeof name === "string" && name ? name : id;
205
261
  }
206
262
 
@@ -211,12 +267,12 @@ function fontNameOf(font: unknown, id: string): string {
211
267
  */
212
268
  function boxOf(
213
269
  page: Page,
214
- vp: ReturnType<Page["getViewport"]>,
270
+ vp: Viewport,
215
271
  it: TextItem,
216
272
  lib: PdfJs,
217
273
  ): Omit<TextBox, "fill"> {
218
- const [a = 0, b = 0, c = 0, d = 0, e = 0, f = 0] = it.transform as number[];
219
- const [x, baseline] = vp.convertToViewportPoint(e, f) as [number, number];
274
+ const [a = 0, b = 0, c = 0, d = 0, e = 0, f = 0] = numbersOf(it.transform);
275
+ const [x, baseline] = pointOf(vp.convertToViewportPoint(e, f));
220
276
  const size = Math.hypot(c, d) || Math.hypot(a, b) || it.height;
221
277
  const font = resolved(page, it.fontName);
222
278
  const m = metricsOf(font);
@@ -229,7 +285,7 @@ function boxOf(
229
285
  font: fontNameOf(font, it.fontName),
230
286
  text: it.str,
231
287
  upright: isUpright(
232
- lib.Util.transform(vp.transform, it.transform) as number[],
288
+ numbersOf(lib.Util.transform(vp.transform, it.transform)),
233
289
  ),
234
290
  };
235
291
  }
@@ -238,7 +294,7 @@ function boxOf(
238
294
  function layoutOf(
239
295
  page: Page,
240
296
  items: readonly TextItem[],
241
- ops: Awaited<ReturnType<Page["getOperatorList"]>>,
297
+ ops: OperatorList,
242
298
  lib: PdfJs,
243
299
  ): PageLayout {
244
300
  const vp = page.getViewport({ scale: 1 });
@@ -290,10 +346,11 @@ export async function factsOf(doc: Doc, lib: PdfJs): Promise<PdfRead> {
290
346
 
291
347
  /** pdf.js's own exception names for the two conditions a caller can act on. */
292
348
  export function failureOf(e: unknown): PdfRead {
293
- const err = e as { name?: string; message?: string };
294
- const detail = `${err.name ?? "Error"}: ${err.message ?? String(e)}`;
349
+ const name = fieldOf(e, "name");
350
+ const message = fieldOf(e, "message");
351
+ const detail = `${typeof name === "string" ? name : "Error"}: ${typeof message === "string" ? message : String(e)}`;
295
352
  return fail(
296
- err.name === "PasswordException" ? "encrypted" : "unreadable",
353
+ name === "PasswordException" ? "encrypted" : "unreadable",
297
354
  detail,
298
355
  );
299
356
  }
@@ -160,6 +160,10 @@ export type FontProgram =
160
160
  /** A program pdf.js named but this module does not know — kept verbatim, never guessed. */
161
161
  export type UnknownProgram = `unknown:${string}`;
162
162
 
163
+ const isUnknownProgram = (
164
+ p: FontProgram | UnknownProgram,
165
+ ): p is UnknownProgram => p.startsWith("unknown:");
166
+
163
167
  /**
164
168
  * One font the PDF draws text with.
165
169
  *
@@ -296,7 +300,5 @@ export const POPPLER_TYPE: Readonly<Record<FontProgram, string>> = {
296
300
  /** The type column for any fact, the unknown ones verbatim. */
297
301
  export function popplerType(f: FontFact): string {
298
302
  if (f.kind === "type3") return POPPLER_TYPE.Type3;
299
- return f.program.startsWith("unknown:")
300
- ? f.program
301
- : POPPLER_TYPE[f.program as FontProgram];
303
+ return isUnknownProgram(f.program) ? f.program : POPPLER_TYPE[f.program];
302
304
  }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * What paperlint needs from a LaTeX source, shaped by its rules and `paperlint new` — not by the
3
+ * parser. The one implementation is `src/adapters/latex/` (unified-latex); `src/cli.ts` wires it.
4
+ */
5
+ import type {
6
+ ClassLine,
7
+ DocumentClass,
8
+ Outline,
9
+ TextRun,
10
+ } from "../domain/tex-document.ts";
11
+
12
+ export interface LatexReader {
13
+ /** The source's `\documentclass`, as one of its states. */
14
+ readonly documentClass: (src: string) => ClassLine;
15
+ /** Its sections, where the back matter begins, and where the document ends. */
16
+ readonly outline: (src: string) => Outline;
17
+ /** The text a reader sees: prose, the author block, footnotes — not comments, keys, math, code. */
18
+ readonly renderedRuns: (src: string) => readonly TextRun[];
19
+ /**
20
+ * A preset's `template` → the class it names: a whole `\documentclass[…]{…}` line, or a bare
21
+ * class name (no option). Null for anything else.
22
+ */
23
+ readonly template: (text: string) => DocumentClass | null;
24
+ /**
25
+ * `src` with its `\documentclass` set to `want` — unless it already is `want`'s class with every
26
+ * option `want` names (the author's own options stay), or it has none.
27
+ */
28
+ readonly withDocumentClass: (src: string, want: DocumentClass) => string;
29
+ }
@@ -2,26 +2,11 @@
2
2
  * The port `paperlint toolchain` installs and checks an external program through. One port, many
3
3
  * programs: banal implements it now, TeX Live in #76.
4
4
  */
5
- import type { Opaque } from "ts-essentials";
6
5
  import type { Result } from "../domain/result.ts";
7
6
  import type { Lines } from "../domain/text.ts";
7
+ import type { Ready } from "../domain/ready.ts";
8
8
 
9
- /**
10
- * Present, verified, running. BRANDED: a literal `{ where, fresh, verified }` is not a `Ready`, so
11
- * only an installer's adapter — one minting function each (banal's is `ready` in
12
- * `adapters/banal/index.ts`) — can make one, and a ready line cannot be printed without it.
13
- */
14
- export type Ready = Opaque<
15
- {
16
- /** Where it is: the program's path or its bin directory. */
17
- readonly where: string;
18
- /** This run installed it. */
19
- readonly fresh: boolean;
20
- /** What was checked before calling it ready, as a person reads it. */
21
- readonly verified: string;
22
- },
23
- "Ready"
24
- >;
9
+ export type { Ready };
25
10
 
26
11
  export interface ToolInstaller {
27
12
  /** The program as a person reads it: `banal 1.2 (HotCRP f3e4352)`. */
@@ -10,15 +10,18 @@ import { readdirSync, readFileSync } from "node:fs";
10
10
  import { join } from "node:path";
11
11
  import { describe, expect, it } from "vitest";
12
12
  import { memoryFiles } from "./adapters/memory/index.ts";
13
- import { packageVenuesDir } from "../skills/paper-pipeline/scripts/consumer.mjs";
13
+ import { presetsDir } from "./package-dirs.ts";
14
+ import { parsePreset } from "./tex-requirements.ts";
14
15
  import {
15
16
  MAX_PRESET_DEPTH,
17
+ SHIPPED_PREFIX,
18
+ shippedPresets,
16
19
  labelOf,
17
20
  presetProblemText,
18
21
  resolvePreset,
19
22
  } from "./presets.ts";
20
23
 
21
- const VENUES = packageVenuesDir();
24
+ const VENUES = presetsDir();
22
25
  const shipped = Object.fromEntries(
23
26
  readdirSync(VENUES)
24
27
  .filter((f) => f.endsWith(".jsonc") || f.endsWith(".json"))
@@ -44,7 +47,11 @@ describe("shipped presets", () => {
44
47
  expect(r.value.format.columns).toBe(2);
45
48
  expect(r.value.format.fontsText).toBe("LinLibertine");
46
49
  expect([...r.value.format.kinds.keys()]).toEqual(["short", "full", "demo"]);
47
- expect(r.value.template).toBe("acmart");
50
+ // The family's template, as its file spells it: the child names none.
51
+ expect(r.value.template).toEqual({
52
+ text: "\\documentclass[sigconf]{acmart}",
53
+ file: join(VENUES, "acm-sigconf.jsonc"),
54
+ });
48
55
  expect("acmart" in r.value.tex.packages).toBe(true);
49
56
  expect(r.value.label).toBe("agenticdev");
50
57
  });
@@ -70,6 +77,45 @@ describe("shipped presets", () => {
70
77
  });
71
78
  });
72
79
 
80
+ describe("the naming convention of shipped presets (docs/rules.md)", () => {
81
+ // Each shipped preset, parsed: its file name and what it extends. The families are DERIVED — a
82
+ // shipped preset another shipped preset extends — so a new family is checked the day it ships.
83
+ const presets = shippedPresets(VENUES).map((name) => {
84
+ const file = join(VENUES, `${name}.jsonc`);
85
+ return {
86
+ name,
87
+ extends: parsePreset(readFileSync(file, "utf8"), file, VENUES).extends,
88
+ };
89
+ });
90
+ const families = presets.filter((p) =>
91
+ presets.some((q) => q.extends === `${SHIPPED_PREFIX}${p.name}`),
92
+ );
93
+ const publishers = families.map((f) => f.name.slice(0, f.name.indexOf("-")));
94
+
95
+ it("there are families to check (else the checks below see nothing)", () => {
96
+ expect(families.map((f) => f.name).sort()).toEqual([
97
+ "acm-sigconf",
98
+ "ieee-conference",
99
+ ]);
100
+ });
101
+
102
+ it.each(families.map((f) => [f.name]))(
103
+ "the family %s is <publisher>-<template variant>",
104
+ (name) => {
105
+ expect(name).toMatch(/^[a-z0-9]+-[a-z0-9-]+$/);
106
+ },
107
+ );
108
+
109
+ it.each(presets.filter((p) => p.extends !== null).map((p) => [p.name]))(
110
+ "the venue %s carries no publisher prefix — its template is said by `extends`",
111
+ (name) => {
112
+ expect(publishers.filter((pub) => name.startsWith(`${pub}-`))).toEqual(
113
+ [],
114
+ );
115
+ },
116
+ );
117
+ });
118
+
73
119
  describe("a project's own preset, by relative path", () => {
74
120
  const usenix = JSON.stringify({
75
121
  extends: "paperlint:acm-sigconf",
@@ -149,7 +195,7 @@ describe("what does not resolve, and says why", () => {
149
195
  "a shipped name with a typo",
150
196
  "paperlint:agenticdve",
151
197
  "not-found",
152
- /acm-sigconf, agenticdev, aisec, realm/,
198
+ /acm-sigconf, agenticdev, aidc, aisec, ieee-conference, realm/,
153
199
  ],
154
200
  ["the base TeX set", "paperlint:tex-base", "not-found", /agenticdev/],
155
201
  [
@@ -225,6 +271,21 @@ describe("chains that do not resolve", () => {
225
271
  });
226
272
  });
227
273
 
274
+ describe("a preset's template: kept as its file spells it, with that file", () => {
275
+ it("a child's template replaces its parent's, and names its own file", () => {
276
+ const r = resolve("./own.jsonc", {
277
+ "/work/papers/p/own.jsonc": JSON.stringify({
278
+ extends: "paperlint:aidc",
279
+ template: "\\documentclass[conference]{IEEEtran}",
280
+ }),
281
+ });
282
+ expect(r.ok && r.value.template).toEqual({
283
+ text: "\\documentclass[conference]{IEEEtran}",
284
+ file: "/work/papers/p/own.jsonc",
285
+ });
286
+ });
287
+ });
288
+
228
289
  describe("labelOf — display only, never used to resolve", () => {
229
290
  it.each([
230
291
  ["paperlint:agenticdev", "agenticdev"],
package/src/presets.ts CHANGED
@@ -19,7 +19,8 @@
19
19
  * tex union — a child never removes a package its parent needs
20
20
  * format per key, the child wins; `kinds` by kind name, a child's kind replaces that kind
21
21
  * rules per rule id, the child wins
22
- * template the child wins; so does `name`
22
+ * template the child wins; so does `name`, and `required_sections` (the whole list)
23
+ * aliases union, with every `name` — what the venue is called along the chain
23
24
  *
24
25
  * ── THE LABEL ─────────────────────────────────────────────────────────────────────
25
26
  * Messages, the facts file and the build plan need a word for the venue. It is the most derived
@@ -36,10 +37,11 @@ import {
36
37
  profileFileOf,
37
38
  venueNames,
38
39
  type PresetFile,
40
+ type RequiredSection,
39
41
  type TexRequirements,
40
42
  type VenueFormat,
41
43
  } from "./tex-requirements.ts";
42
- import type { AbsolutePath } from "./domain/paths.ts";
44
+ import { callerPath } from "./caller-path.ts";
43
45
  import { err, ok, type Result } from "./domain/result.ts";
44
46
  import type { Files } from "./ports/files.ts";
45
47
  import {
@@ -61,13 +63,27 @@ export interface PresetDeps {
61
63
  readonly venuesDir: string;
62
64
  }
63
65
 
66
+ /** A preset's `template`, as its file spells it. */
67
+ export interface PresetTemplate {
68
+ readonly text: string;
69
+ readonly file: string;
70
+ }
71
+
64
72
  /** A resolved chain, merged. */
65
73
  export interface Preset {
66
74
  /** The word for this venue in messages. */
67
75
  readonly label: string;
68
76
  /** Every file of the chain, root first. */
69
77
  readonly chain: readonly string[];
70
- readonly template: string | null;
78
+ /**
79
+ * The `\\documentclass` the venue's template uses, as the preset spells it, and the file that
80
+ * spells it; null when no file of the chain names one. Read by the rules and `paperlint new`,
81
+ * through the LaTeX reader.
82
+ */
83
+ readonly template: PresetTemplate | null;
84
+ /** Every `name` and `aliases` along the chain: what this venue is called in a paper's text. */
85
+ readonly aliases: readonly string[];
86
+ readonly requiredSections: readonly RequiredSection[];
71
87
  readonly tex: TexRequirements;
72
88
  readonly format: VenueFormat;
73
89
  readonly rules: Readonly<Record<string, unknown>>;
@@ -120,7 +136,7 @@ function readPreset(
120
136
  ): Result<{ file: string; preset: PresetFile }, PresetProblem> {
121
137
  const where = fileOf(spec, from, deps.venuesDir);
122
138
  if (!where.ok) return where;
123
- const bytes = deps.files.readBytes(where.value as AbsolutePath);
139
+ const bytes = deps.files.readBytes(callerPath(where.value));
124
140
  if (bytes === null)
125
141
  return err({ kind: "not-found", spec, file: where.value, shipped: [] });
126
142
  try {
@@ -166,33 +182,58 @@ export function mergeFormat(
166
182
  parent: VenueFormat,
167
183
  child: VenueFormat,
168
184
  ): VenueFormat {
169
- const keys = Object.keys(NO_FORMAT).filter((k) => k !== "kinds") as Exclude<
170
- keyof VenueFormat,
171
- "kinds"
172
- >[];
173
- const scalars = Object.fromEntries(
174
- keys.map((k) => [k, child[k] ?? parent[k]]),
175
- ) as Omit<VenueFormat, "kinds">;
176
- return { ...scalars, kinds: new Map([...parent.kinds, ...child.kinds]) };
185
+ // Every field listed: a VenueFormat literal missing one is a compile error.
186
+ const pick = <K extends Exclude<keyof VenueFormat, "kinds">>(
187
+ k: K,
188
+ ): VenueFormat[K] => child[k] ?? parent[k];
189
+ return {
190
+ pageWidthIn: pick("pageWidthIn"),
191
+ pageHeightIn: pick("pageHeightIn"),
192
+ columns: pick("columns"),
193
+ bodyPt: pick("bodyPt"),
194
+ bodyPtTol: pick("bodyPtTol"),
195
+ refPtMin: pick("refPtMin"),
196
+ refPtMax: pick("refPtMax"),
197
+ fontsText: pick("fontsText"),
198
+ fontsTitle: pick("fontsTitle"),
199
+ kinds: new Map([...parent.kinds, ...child.kinds]),
200
+ };
177
201
  }
178
202
 
179
203
  /** The chain, root first, merged into one preset. Pure. */
180
204
  function merged(
181
205
  spec: string,
182
- rootFirst: readonly PresetFile[],
183
- files: readonly string[],
206
+ rootFirst: readonly { readonly preset: PresetFile; readonly file: string }[],
184
207
  ): Preset {
185
- const base = {
186
- name: null as string | null,
187
- template: null as string | null,
208
+ const base: {
209
+ name: string | null;
210
+ template: PresetTemplate | null;
211
+ aliases: readonly string[];
212
+ requiredSections: readonly RequiredSection[];
213
+ tex: TexRequirements;
214
+ format: VenueFormat;
215
+ rules: Readonly<Record<string, unknown>>;
216
+ } = {
217
+ name: null,
218
+ template: null,
219
+ aliases: [],
220
+ requiredSections: [],
188
221
  tex: NO_REQUIREMENTS,
189
222
  format: NO_FORMAT,
190
- rules: {} as Readonly<Record<string, unknown>>,
223
+ rules: {},
191
224
  };
192
225
  const m = rootFirst.reduce(
193
- (acc, p) => ({
226
+ (acc, { preset: p, file }) => ({
194
227
  name: p.name ?? acc.name,
195
- template: p.template ?? acc.template,
228
+ template: p.template === null ? acc.template : { text: p.template, file },
229
+ aliases: [
230
+ ...new Set([
231
+ ...acc.aliases,
232
+ ...(p.name === null ? [] : [p.name]),
233
+ ...p.aliases,
234
+ ]),
235
+ ],
236
+ requiredSections: p.requiredSections ?? acc.requiredSections,
196
237
  tex: p.tex ? mergeRequirements(acc.tex, p.tex) : acc.tex,
197
238
  format: mergeFormat(acc.format, p.format),
198
239
  rules: { ...acc.rules, ...p.rules },
@@ -201,8 +242,10 @@ function merged(
201
242
  );
202
243
  return {
203
244
  label: m.name ?? labelOf(spec),
204
- chain: files,
245
+ chain: rootFirst.map((x) => x.file),
205
246
  template: m.template,
247
+ aliases: m.aliases,
248
+ requiredSections: m.requiredSections,
206
249
  tex: m.tex,
207
250
  format: m.format,
208
251
  rules: m.rules,
@@ -224,13 +267,7 @@ export function resolvePreset(
224
267
  ? err({ ...chain.error, shipped: shippedPresets(deps.venuesDir) })
225
268
  : chain;
226
269
  const rootFirst = [...chain.value].reverse();
227
- return ok(
228
- merged(
229
- spec,
230
- rootFirst.map((x) => x.preset),
231
- rootFirst.map((x) => x.file),
232
- ),
233
- );
270
+ return ok(merged(spec, rootFirst));
234
271
  }
235
272
 
236
273
  /** The shipped presets' names (each is spelled `paperlint:<name>`). */
@@ -9,10 +9,11 @@
9
9
  */
10
10
  import { describe, expect, it } from "vitest";
11
11
  import { ESLint } from "eslint";
12
+ import { eslintConfig } from "./cli.ts";
12
13
  import { join } from "node:path";
13
14
  import { memoryFiles } from "./adapters/memory/index.ts";
14
15
  import { ok } from "./domain/result.ts";
15
- import type { AbsolutePath } from "./domain/paths.ts";
16
+ import { absolutePath } from "./domain/paths.ts";
16
17
  import type {
17
18
  CheckReferences,
18
19
  EntryVerdict,
@@ -78,9 +79,13 @@ const offline: CheckReferences = (_bib, cache) =>
78
79
  const ctx = (files: ReturnType<typeof memoryFiles>) => ({
79
80
  paperDir: PAPER,
80
81
  env: {},
81
- run: (() => ({})) as never,
82
- readPdf: (() => null) as never,
83
- measure: (() => null) as never,
82
+ run: () => ({ status: 0 }),
83
+ readPdf: () => Promise.reject(new Error("the references step reads no PDF")),
84
+ measure: {
85
+ measure: () => {
86
+ throw new Error("the references step measures nothing");
87
+ },
88
+ },
84
89
  files,
85
90
  });
86
91
 
@@ -94,32 +99,26 @@ async function build(tex: string, check: CheckReferences) {
94
99
  return { files, out };
95
100
  }
96
101
 
97
- /**
98
- * The rules as an ESLint plugin. Their context type is the narrow one they read (a custom
99
- * language's SourceCode), which ESLint's JS-centred plugin type does not describe.
100
- */
101
- const asPlugin = (rules: object): ESLint.Plugin => ({ rules }) as ESLint.Plugin;
102
-
103
102
  /** Lint `tex` as the paper's paper.tex against what `files` recorded. */
104
103
  async function lint(files: ReturnType<typeof memoryFiles>, tex: string) {
105
104
  files.writeAtomic(
106
- `${PAPER}/paper.tex` as AbsolutePath,
105
+ absolutePath(`${PAPER}/paper.tex`),
107
106
  new TextEncoder().encode(tex),
108
107
  );
109
108
  const eslint = new ESLint({
110
109
  cwd: "/work",
111
110
  overrideConfigFile: true,
112
- overrideConfig: [
111
+ overrideConfig: eslintConfig([
113
112
  {
114
113
  files: ["**/paper.tex"],
115
114
  plugins: {
116
115
  tex: { languages: { latex: texLanguage } },
117
- paper: asPlugin(referenceRules({ files })),
116
+ paper: { rules: referenceRules({ files }) },
118
117
  },
119
118
  language: "tex/latex",
120
119
  rules: REFERENCE_RULE_LEVELS,
121
120
  },
122
- ],
121
+ ]),
123
122
  });
124
123
  const [res] = await eslint.lintText(tex, {
125
124
  filePath: join(PAPER, "paper.tex"),
@@ -186,7 +185,7 @@ function fetching(key: string) {
186
185
  return { check, seen };
187
186
  }
188
187
  const cacheText = (files: ReturnType<typeof memoryFiles>): string | null => {
189
- const b = files.readBytes(lookupCachePath(PAPER) as AbsolutePath);
188
+ const b = files.readBytes(absolutePath(lookupCachePath(PAPER)));
190
189
  return b === null ? null : new TextDecoder().decode(b);
191
190
  };
192
191
 
@@ -363,7 +362,7 @@ const record = (
363
362
  ) => {
364
363
  const bib = present(bibliographyOf(files, PAPER), "the bibliography");
365
364
  files.writeAtomic(
366
- referencesPath(PAPER) as AbsolutePath,
365
+ absolutePath(referencesPath(PAPER)),
367
366
  new TextEncoder().encode(
368
367
  JSON.stringify({
369
368
  schema: 1,
@@ -424,9 +423,16 @@ describe("the reference rules over refs.bib, and on other files", () => {
424
423
  const files = memoryFiles({});
425
424
  const rules = referenceRules({ files });
426
425
  for (const rule of Object.values(rules))
427
- expect(rule.create({ filename: `${PAPER}/notes.tex` } as never)).toEqual(
428
- {},
429
- );
426
+ expect(
427
+ rule.create({
428
+ filename: `${PAPER}/notes.tex`,
429
+ cwd: "/work",
430
+ sourceCode: { getLocFromIndex: () => ({ line: 1, column: 0 }) },
431
+ report: () => {
432
+ throw new Error("a file that is not paper.tex is never reported");
433
+ },
434
+ }),
435
+ ).toEqual({});
430
436
  });
431
437
  });
432
438