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
package/src/references.ts CHANGED
@@ -28,7 +28,7 @@
28
28
  * a pass would be the counter that counts what it never looked at.
29
29
  */
30
30
  import { join } from "node:path";
31
- import type { AbsolutePath } from "./domain/paths.ts";
31
+ import { callerPath } from "./caller-path.ts";
32
32
  import { sha256Hex } from "./domain/sha256.ts";
33
33
  import type { Files } from "./ports/files.ts";
34
34
  import {
@@ -60,9 +60,8 @@ export const lookupCachePath = (paperDir: string): string =>
60
60
  export const referencesPath = (paperDir: string): string =>
61
61
  join(paperDir, "_build", REFERENCES_FILE);
62
62
 
63
- const at = (p: string): AbsolutePath => p as AbsolutePath;
64
63
  const text = (files: Files, p: string): string | null => {
65
- const b = files.readBytes(at(p));
64
+ const b = files.readBytes(callerPath(p));
66
65
  return b === null ? null : new TextDecoder().decode(b);
67
66
  };
68
67
 
@@ -186,7 +185,7 @@ export async function recordReferences(
186
185
  const record = (c: ReferencesCheck): ReferencesDocument => {
187
186
  const doc = documentOf(bib, c);
188
187
  files.writeAtomic(
189
- at(referencesPath(paperDir)),
188
+ callerPath(referencesPath(paperDir)),
190
189
  new TextEncoder().encode(`${JSON.stringify(doc, null, 2)}\n`),
191
190
  );
192
191
  return doc;
@@ -206,7 +205,7 @@ export async function recordReferences(
206
205
  const fetched = newAnswers(cache.value, result.cache);
207
206
  if (fetched > 0)
208
207
  files.writeAtomic(
209
- at(lookupCachePath(paperDir)),
208
+ callerPath(lookupCachePath(paperDir)),
210
209
  new TextEncoder().encode(serializeLookupCache(result.cache)),
211
210
  );
212
211
  const bad = doc.entries.filter(
@@ -0,0 +1,474 @@
1
+ /**
2
+ * Every rule paperlint registers runs through REAL ESLint, the way `paperlint lint` runs it: a small
3
+ * paper tree in a temp directory, `run(["lint", "--json"])` — the CLI's own config, the `tex/latex`
4
+ * language and its `raw`, the `root` visitors, `loc`, message templating, the `files` wiring. The
5
+ * unit tests beside each rule call its `create` directly and cannot see that boundary; the TeX e2e
6
+ * sees it only for the papers a real pdflatex can build.
7
+ *
8
+ * The rule set is read off the registry (`rulePlugins`, as `rule-docs.test.ts` does). Every rule
9
+ * needs a case that REPORTS (its id, severity and line) and one that stays SILENT, or an entry in
10
+ * `EXEMPT` with the reason; a case for a rule that is not registered fails too. A `_build/` file a
11
+ * rule reads is planted as the build would write it — no pdflatex, no network.
12
+ */
13
+ import { readFileSync } from "node:fs";
14
+ import { dirname, join } from "node:path";
15
+ import { fileURLToPath } from "node:url";
16
+ import { describe, expect, it } from "vitest";
17
+ import { z } from "zod";
18
+ import { run, rulePlugins } from "./cli.ts";
19
+ import { sha256Hex } from "./domain/sha256.ts";
20
+ import { bibHash } from "./references.ts";
21
+ import { useTempDir, writeTree } from "../test/support.ts";
22
+
23
+ const ROOT = dirname(dirname(fileURLToPath(import.meta.url)));
24
+ const fixture = (p: string): string =>
25
+ readFileSync(join(ROOT, "fixtures", p), "utf8");
26
+
27
+ const P = "papers/p";
28
+ const STATUS = "---\nstages: []\n---\n# PIPELINE-STATUS\n";
29
+ const tex = (body: string, cls = "\\documentclass{article}"): string =>
30
+ `${cls}\n\\begin{document}\n${body}\n\\end{document}\n`;
31
+
32
+ /** A paper: its scorecard and paper.tex, and whatever else the case adds. */
33
+ const paper = (
34
+ paperTex: string,
35
+ extra: Readonly<Record<string, string>> = {},
36
+ ): Record<string, string> => ({
37
+ [`${P}/PIPELINE-STATUS.md`]: STATUS,
38
+ [`${P}/paper.tex`]: paperTex,
39
+ ...extra,
40
+ });
41
+
42
+ // ── the venue rules' inputs: a PDF and the facts `paperlint build` would write about it ─────────
43
+
44
+ const PDF = "%PDF-1.5 a stand-in for the built PDF";
45
+ const AGENTICDEV = { extends: "paperlint:agenticdev", kind: "short" };
46
+ const ACM_TEX = tex("Text.", "\\documentclass[sigconf]{acmart}");
47
+
48
+ /** The facts of a short agenticdev paper that meets every number in its preset. */
49
+ const goodFacts = (): Record<string, unknown> => ({
50
+ schema: 2,
51
+ pdf: "paper.pdf",
52
+ pdf_sha256: sha256Hex(new TextEncoder().encode(PDF)),
53
+ venue: "agenticdev",
54
+ kind: "short",
55
+ npages: 7,
56
+ fonts_source: "pdfjs-drawn",
57
+ fonts: ["LinLibertineT", "LinBiolinumTB"].map((name) => ({
58
+ name,
59
+ type: "Type 1",
60
+ embedded: true,
61
+ program: "Type1",
62
+ })),
63
+ last_page: { kind: "measured", columns_pt: [600, 590] },
64
+ last_page_cols_pt: [600, 590],
65
+ geometry_source: "banal",
66
+ page_w_in: 8.5,
67
+ page_h_in: 11,
68
+ columns: 2,
69
+ body_pt: 9.3,
70
+ ref_pt: 7.3,
71
+ body_pages: 5,
72
+ ref_pages: 2,
73
+ appendix_pages: 0,
74
+ pages_by_type: { body: 5, bib: 2 },
75
+ });
76
+
77
+ /** An agenticdev paper, built: its settings, the PDF, and facts with `patch` applied. */
78
+ const built = (
79
+ patch: Record<string, unknown> = {},
80
+ settings: Record<string, unknown> = AGENTICDEV,
81
+ ): Record<string, string> =>
82
+ paper(ACM_TEX, {
83
+ [`${P}/paperlint.json`]: JSON.stringify(settings),
84
+ [`${P}/paper.pdf`]: PDF,
85
+ [`${P}/_build/paper.facts.json`]: JSON.stringify({
86
+ ...goodFacts(),
87
+ ...patch,
88
+ }),
89
+ });
90
+
91
+ // ── the reference rules' input: refs.bib, and the verdicts `paperlint build` would record ───────
92
+
93
+ const BIB =
94
+ "@article{a,\n title = {A},\n author = {Doe, J.},\n year = {2026},\n doi = {10.1/x}\n}\n";
95
+ const sha = bibHash({ source: "refs.bib", text: BIB, offset: 0 });
96
+ /** A paper citing refs.bib, with its references record (`status`, `entries`) or none. */
97
+ const cited = (
98
+ body: Record<string, unknown> | null,
99
+ bibSha = sha,
100
+ ): Record<string, string> =>
101
+ paper(tex("See~\\cite{a}.\n\\bibliography{refs}"), {
102
+ [`${P}/refs.bib`]: BIB,
103
+ ...(body === null
104
+ ? {}
105
+ : {
106
+ [`${P}/_build/references.json`]: JSON.stringify({
107
+ schema: 1,
108
+ bib: { source: "refs.bib", sha256: bibSha },
109
+ ...body,
110
+ }),
111
+ }),
112
+ });
113
+ /** A paper whose bibliography is written inline, by `filecontents*`, as `bib/reachable-entry` reads it. */
114
+ const inlineBib = (entry: string): string =>
115
+ `\\documentclass{article}\n\\begin{filecontents*}{refs.bib}\n${entry}\n\\end{filecontents*}\n\\begin{document}\nSee~\\cite{a}.\n\\bibliography{refs}\n\\end{document}\n`;
116
+ const checked = (exists: string, authors: string) => ({
117
+ status: "checked",
118
+ entries: [{ key: "a", exists, authors }],
119
+ });
120
+
121
+ // ── an AIDC paper, for the LaTeX venue rules ──────────────────────────────────────────────────
122
+
123
+ const AIDC = {
124
+ [`${P}/paperlint.json`]: JSON.stringify({
125
+ extends: "paperlint:aidc",
126
+ kind: "short",
127
+ }),
128
+ };
129
+ const IEEE = "\\documentclass[conference,compsoc]{IEEEtran}";
130
+ const STATEMENT = "\\section*{LLM Usage Statement}\nNone.";
131
+ const aidc = (body: string, cls = IEEE) =>
132
+ paper(tex(`\\section{Introduction}\n${body}\n${STATEMENT}`, cls), AIDC);
133
+
134
+ // ── the case table ─────────────────────────────────────────────────────────────────────────────
135
+
136
+ interface Reports {
137
+ readonly tree: Record<string, string>;
138
+ /** The file the finding is on, relative to the project. */
139
+ readonly file: string;
140
+ readonly severity: 1 | 2;
141
+ readonly line: number;
142
+ }
143
+ interface RuleCases {
144
+ readonly reports: Reports;
145
+ /** A tree on which the rule says nothing, on any file. */
146
+ readonly silent: Record<string, string>;
147
+ }
148
+
149
+ const TEX_FILE = `${P}/paper.tex`;
150
+ const STATUS_FILE = `${P}/PIPELINE-STATUS.md`;
151
+ const SHIPPED_STATUS =
152
+ "---\nstages:\n - stage: submitted\n date: 2026-07-22\n pdf: versions/a.pdf\n bytes: 1\n---\n# PIPELINE-STATUS\n";
153
+ const onPdf = (tree: Record<string, string>, line = 1) => ({
154
+ tree,
155
+ file: TEX_FILE,
156
+ severity: 2 as const,
157
+ line,
158
+ });
159
+
160
+ const CASES: Readonly<Record<string, RuleCases>> = {
161
+ "paper/stages": {
162
+ reports: {
163
+ tree: { ...paper(tex("x")), [STATUS_FILE]: SHIPPED_STATUS },
164
+ file: STATUS_FILE,
165
+ severity: 2,
166
+ line: 1,
167
+ },
168
+ silent: paper(tex("x")),
169
+ },
170
+ "paper/source": {
171
+ reports: {
172
+ tree: { ...paper(tex("x")), [STATUS_FILE]: SHIPPED_STATUS },
173
+ file: STATUS_FILE,
174
+ severity: 2,
175
+ line: 1,
176
+ },
177
+ silent: paper(tex("x")),
178
+ },
179
+ "paper/research-question": {
180
+ reports: {
181
+ tree: {
182
+ [STATUS_FILE]: fixture(
183
+ "paper-research-question/shipped-no-rq/PIPELINE-STATUS.md",
184
+ ),
185
+ [TEX_FILE]: fixture("paper-research-question/shipped-no-rq/paper.tex"),
186
+ },
187
+ file: TEX_FILE,
188
+ severity: 1,
189
+ line: 1,
190
+ },
191
+ silent: {
192
+ [STATUS_FILE]: fixture(
193
+ "paper-research-question/shipped-with-rq/PIPELINE-STATUS.md",
194
+ ),
195
+ [TEX_FILE]: fixture("paper-research-question/shipped-with-rq/paper.tex"),
196
+ },
197
+ },
198
+ "paper/section-word": {
199
+ reports: {
200
+ tree: paper(tex("As in §2.")),
201
+ file: TEX_FILE,
202
+ severity: 1,
203
+ line: 3,
204
+ },
205
+ silent: paper(tex("As in Section~2.")),
206
+ },
207
+ "paper/leading-zero": {
208
+ reports: {
209
+ tree: paper(tex("We found $p < .05$.")),
210
+ file: TEX_FILE,
211
+ severity: 1,
212
+ line: 3,
213
+ },
214
+ silent: paper(tex("We found $p < 0.05$.")),
215
+ },
216
+ "paper/figure-ref-style": {
217
+ reports: {
218
+ tree: paper(tex("Fig.~\\ref{f1} and Figure~\\ref{f2}.")),
219
+ file: TEX_FILE,
220
+ severity: 1,
221
+ line: 3,
222
+ },
223
+ silent: paper(tex("Figure~\\ref{f1} and Figure~\\ref{f2}.")),
224
+ },
225
+ "paper/author-list": {
226
+ reports: {
227
+ tree: cited(checked("true", "mismatch")),
228
+ file: TEX_FILE,
229
+ severity: 2,
230
+ line: 1,
231
+ },
232
+ silent: cited(checked("true", "match")),
233
+ },
234
+ "paper/cite-exists": {
235
+ reports: {
236
+ tree: cited(checked("false", "match")),
237
+ file: TEX_FILE,
238
+ severity: 2,
239
+ line: 1,
240
+ },
241
+ silent: cited(checked("true", "match")),
242
+ },
243
+ "paper/refs-checked": {
244
+ reports: { tree: cited(null), file: TEX_FILE, severity: 1, line: 1 },
245
+ silent: cited(checked("true", "match")),
246
+ },
247
+ "paper/refs-fresh": {
248
+ reports: {
249
+ tree: cited(checked("true", "match"), "0".repeat(64)),
250
+ file: TEX_FILE,
251
+ severity: 2,
252
+ line: 1,
253
+ },
254
+ silent: cited(checked("true", "match")),
255
+ },
256
+ "review/frontmatter": {
257
+ reports: {
258
+ tree: paper(tex("x"), {
259
+ [`${P}/reviews/r1.md`]:
260
+ "---\nfindings:\n - id: 1\n status: open\n---\n# Review\n",
261
+ }),
262
+ file: `${P}/reviews/r1.md`,
263
+ severity: 2,
264
+ line: 1,
265
+ },
266
+ silent: paper(tex("x"), {
267
+ [`${P}/reviews/r1.md`]:
268
+ "---\nfindings:\n - id: 1\n status: fixed\n---\n# Review\n",
269
+ }),
270
+ },
271
+ "sibling/frontmatter": {
272
+ reports: {
273
+ tree: paper(tex("x"), {
274
+ [`${P}/siblings/s.md`]: "---\nread: skimmed\n---\n# A sibling\n",
275
+ }),
276
+ file: `${P}/siblings/s.md`,
277
+ severity: 1,
278
+ line: 1,
279
+ },
280
+ silent: paper(tex("x"), {
281
+ [`${P}/siblings/s.md`]: "---\nread: full\n---\n# A sibling\n",
282
+ }),
283
+ },
284
+ "tex/future-promise": {
285
+ reports: {
286
+ tree: paper(fixture("tex-build/defect.tex")),
287
+ file: TEX_FILE,
288
+ severity: 1,
289
+ line: 15,
290
+ },
291
+ silent: paper(fixture("tex-build/clean.tex")),
292
+ },
293
+ "tex/acm-frontmatter-override": {
294
+ reports: {
295
+ tree: paper(fixture("tex-build/frontmatter-defect.tex")),
296
+ file: TEX_FILE,
297
+ severity: 2,
298
+ line: 10,
299
+ },
300
+ silent: paper(fixture("tex-build/frontmatter-clean.tex")),
301
+ },
302
+ "tex/template": {
303
+ reports: {
304
+ tree: aidc("Text.", "\\documentclass[conference]{IEEEtran}"),
305
+ file: TEX_FILE,
306
+ severity: 2,
307
+ line: 1,
308
+ },
309
+ silent: aidc("Text."),
310
+ },
311
+ "tex/required-section": {
312
+ reports: {
313
+ tree: paper(tex("\\section{Introduction}\nText.", IEEE), AIDC),
314
+ file: TEX_FILE,
315
+ severity: 2,
316
+ line: 5,
317
+ },
318
+ silent: aidc("Text."),
319
+ },
320
+ "tex/venue-leftover": {
321
+ reports: {
322
+ tree: aidc("First written for AISec."),
323
+ file: TEX_FILE,
324
+ severity: 1,
325
+ line: 4,
326
+ },
327
+ silent: aidc("Text."),
328
+ },
329
+ "bib/reachable-entry": {
330
+ reports: {
331
+ tree: paper(inlineBib("@book{a,\n title = {A},\n year = {2026}\n}")),
332
+ file: TEX_FILE,
333
+ severity: 1,
334
+ line: 3,
335
+ },
336
+ silent: paper(inlineBib(BIB)),
337
+ },
338
+ "pdf/fresh": {
339
+ reports: onPdf(built({ pdf_sha256: "0".repeat(64) })),
340
+ silent: built(),
341
+ },
342
+ "pdf/profile": {
343
+ reports: onPdf(built({}, { extends: "paperlint:nope" })),
344
+ silent: built(),
345
+ },
346
+ "pdf/measured": {
347
+ reports: { ...onPdf(built({}, { kind: "short" })), severity: 1 },
348
+ silent: built(),
349
+ },
350
+ "pdf/fonts": {
351
+ reports: onPdf(
352
+ built({
353
+ fonts: [
354
+ { name: "CMR10", type: "Type 1", embedded: true, program: "Type1" },
355
+ ],
356
+ }),
357
+ ),
358
+ silent: built(),
359
+ },
360
+ "pdf/geometry": {
361
+ reports: onPdf(built({ page_w_in: 8.264 })),
362
+ silent: built(),
363
+ },
364
+ "pdf/limits": {
365
+ reports: onPdf(
366
+ built({ body_pages: 9, pages_by_type: { body: 9, bib: 2 } }),
367
+ ),
368
+ silent: built(),
369
+ },
370
+ "pdf/body-size": {
371
+ reports: { ...onPdf(built({ body_pt: 12 })), severity: 1 },
372
+ silent: built(),
373
+ },
374
+ "pdf/last-page-balance": {
375
+ reports: onPdf(
376
+ built(
377
+ { last_page: { kind: "measured", columns_pt: [621.5, 264.8] } },
378
+ { ...AGENTICDEV, rules: { "pdf/last-page-balance": "error" } },
379
+ ),
380
+ ),
381
+ silent: built(
382
+ {},
383
+ { ...AGENTICDEV, rules: { "pdf/last-page-balance": "error" } },
384
+ ),
385
+ },
386
+ };
387
+
388
+ /**
389
+ * Rules whose honest case needs what this test cannot give, each with the reason. Checked against
390
+ * the registry like the cases are: an entry for a rule that no longer exists fails.
391
+ */
392
+ const EXEMPT: Readonly<Record<string, string>> = {};
393
+
394
+ // ── the run ─────────────────────────────────────────────────────────────────────────────────────
395
+
396
+ const LintResults = z.array(
397
+ z.looseObject({
398
+ filePath: z.string(),
399
+ messages: z.array(
400
+ z.looseObject({
401
+ ruleId: z.string().nullable(),
402
+ severity: z.number(),
403
+ line: z.number().optional(),
404
+ message: z.string(),
405
+ }),
406
+ ),
407
+ }),
408
+ );
409
+ type Message = z.infer<typeof LintResults>[number]["messages"][number] & {
410
+ readonly file: string;
411
+ };
412
+
413
+ /** `paperlint lint --json` over the tree, in-process through the CLI: every message, by file. */
414
+ async function lintTree(tree: Record<string, string>): Promise<Message[]> {
415
+ const dir = writeTree(useTempDir("paperlint-rule-cases-"), {
416
+ "package.json": '{"name":"consumer","private":true}',
417
+ ...tree,
418
+ });
419
+ const out: string[] = [];
420
+ await run(["lint", "--json"], {
421
+ cwd: dir,
422
+ log: (...a: unknown[]) => out.push(a.join(" ")),
423
+ err: () => undefined,
424
+ });
425
+ return LintResults.parse(JSON.parse(out.join("\n"))).flatMap((r) =>
426
+ r.messages.map((m) => ({ ...m, file: r.filePath.slice(dir.length + 1) })),
427
+ );
428
+ }
429
+
430
+ /** Every rule id the registry holds. */
431
+ const registered = Object.entries(rulePlugins()).flatMap(([plugin, p]) =>
432
+ Object.keys(p.rules ?? {}).map((rule) => `${plugin}/${rule}`),
433
+ );
434
+
435
+ describe("every registered rule has cases, and every case names a registered rule", () => {
436
+ it.each(registered)(
437
+ "%s has a reporting and a silent case, or a reason it cannot",
438
+ (id) => {
439
+ expect(
440
+ id in CASES || id in EXEMPT,
441
+ `${id}: no cases in rule-cases.test.ts`,
442
+ ).toBe(true);
443
+ },
444
+ );
445
+
446
+ it("no case or exemption names a rule the registry does not hold", () => {
447
+ expect(
448
+ [...Object.keys(CASES), ...Object.keys(EXEMPT)].filter(
449
+ (id) => !registered.includes(id),
450
+ ),
451
+ ).toEqual([]);
452
+ });
453
+ });
454
+
455
+ describe.each(Object.entries(CASES))("%s, through real ESLint", (id, c) => {
456
+ it(`reports on ${c.reports.file}:${String(c.reports.line)} at severity ${String(c.reports.severity)}`, async () => {
457
+ const got = (await lintTree(c.reports.tree)).filter((m) => m.ruleId === id);
458
+ expect(
459
+ got.some(
460
+ (m) =>
461
+ m.file === c.reports.file &&
462
+ m.line === c.reports.line &&
463
+ m.severity === c.reports.severity,
464
+ ),
465
+ JSON.stringify(got),
466
+ ).toBe(true);
467
+ });
468
+
469
+ it("stays silent on a conforming paper", async () => {
470
+ expect((await lintTree(c.silent)).filter((m) => m.ruleId === id)).toEqual(
471
+ [],
472
+ );
473
+ });
474
+ });
@@ -0,0 +1,159 @@
1
+ /**
2
+ * Every rule paperlint ships has a page: `meta.docs.url` names `docs/rules/<group>/<rule>.md` on the
3
+ * default branch, and that file exists. The rule set is read off paperlint's own config
4
+ * (`rulePlugins`), so a new rule is checked the day it is registered.
5
+ *
6
+ * The rules older than this convention are listed in `AWAITING_PAGE` until #131 writes their
7
+ * pages. The list only shrinks: a listed rule that now has a page fails here too, so the list is
8
+ * trimmed in the same change.
9
+ */
10
+ import { existsSync } from "node:fs";
11
+ import { dirname, join } from "node:path";
12
+ import { fileURLToPath } from "node:url";
13
+ import { describe, expect, it } from "vitest";
14
+ import { rulePlugins } from "./cli.ts";
15
+ import { fieldOf } from "./domain/record.ts";
16
+
17
+ const ROOT = dirname(dirname(fileURLToPath(import.meta.url)));
18
+ const PAGES = "https://github.com/zernie/paperlint/blob/main/";
19
+
20
+ /** Rules shipped before per-rule pages, each waiting for its page in #131. Only ever shrinks. */
21
+ const AWAITING_PAGE: ReadonlySet<string> = new Set([
22
+ "bib/reachable-entry",
23
+ "paper/author-list",
24
+ "paper/cite-exists",
25
+ "paper/figure-ref-style",
26
+ "paper/leading-zero",
27
+ "paper/refs-checked",
28
+ "paper/refs-fresh",
29
+ "paper/research-question",
30
+ "paper/section-word",
31
+ "paper/source",
32
+ "paper/stages",
33
+ "pdf/body-size",
34
+ "pdf/fonts",
35
+ "pdf/fresh",
36
+ "pdf/geometry",
37
+ "pdf/last-page-balance",
38
+ "pdf/limits",
39
+ "pdf/measured",
40
+ "pdf/profile",
41
+ "review/frontmatter",
42
+ "sibling/frontmatter",
43
+ "tex/acm-frontmatter-override",
44
+ "tex/future-promise",
45
+ ]);
46
+
47
+ /** A rule's `meta.docs.url`, or null when it has none. */
48
+ const urlOf = (rule: unknown): string | null => {
49
+ const url = fieldOf(fieldOf(fieldOf(rule, "meta"), "docs"), "url");
50
+ return typeof url === "string" ? url : null;
51
+ };
52
+
53
+ /**
54
+ * What is wrong with the rules' pages, one line per rule: a page URL that names no file under
55
+ * `docs/rules/`, a rule with no page that is not awaiting one, and one awaiting a page it has.
56
+ */
57
+ export function pageProblems(
58
+ rules: Readonly<Record<string, unknown>>,
59
+ awaiting: ReadonlySet<string>,
60
+ exists: (repoPath: string) => boolean,
61
+ ): string[] {
62
+ return Object.entries(rules).flatMap(([id, rule]) => {
63
+ const url = urlOf(rule);
64
+ if (url === null)
65
+ return awaiting.has(id)
66
+ ? []
67
+ : [
68
+ `${id}: no meta.docs.url — write docs/rules/${id}.md and point at it`,
69
+ ];
70
+ if (awaiting.has(id))
71
+ return [`${id}: has a page now — take it off AWAITING_PAGE`];
72
+ const path = url.startsWith(PAGES) ? url.slice(PAGES.length) : null;
73
+ return path !== null && path.startsWith("docs/rules/") && exists(path)
74
+ ? []
75
+ : [`${id}: meta.docs.url ${url} names no file under docs/rules/`];
76
+ });
77
+ }
78
+
79
+ /** Every rule paperlint's config registers, by `<plugin>/<rule>`. */
80
+ const shipped = (): Record<string, unknown> =>
81
+ Object.fromEntries(
82
+ Object.entries(rulePlugins()).flatMap(([plugin, p]) =>
83
+ Object.entries(p.rules ?? {}).map(([name, rule]) => [
84
+ `${plugin}/${name}`,
85
+ rule,
86
+ ]),
87
+ ),
88
+ );
89
+
90
+ const inRepo = (p: string): boolean => existsSync(join(ROOT, p));
91
+
92
+ describe("every shipped rule has a page", () => {
93
+ it("the rule set is read off the config, and the new rules are in it", () => {
94
+ expect(Object.keys(shipped())).toEqual(
95
+ expect.arrayContaining([
96
+ "tex/template",
97
+ "tex/required-section",
98
+ "tex/venue-leftover",
99
+ ]),
100
+ );
101
+ });
102
+
103
+ it("each page named exists; every rule without one is awaiting it (#131)", () => {
104
+ expect(pageProblems(shipped(), AWAITING_PAGE, inRepo)).toEqual([]);
105
+ });
106
+
107
+ it("every awaiting rule is still shipped — a removed rule leaves the list", () => {
108
+ expect([...AWAITING_PAGE].filter((id) => !(id in shipped()))).toEqual([]);
109
+ });
110
+ });
111
+
112
+ describe("pageProblems — both halves, on planted rules", () => {
113
+ const withPage = (path: string) => ({
114
+ meta: { docs: { url: PAGES + path } },
115
+ });
116
+ const exists = (p: string) => p === "docs/rules/x/ok.md";
117
+
118
+ it("a brand-new rule without a page fails", () => {
119
+ expect(pageProblems({ "x/new": { meta: {} } }, new Set(), exists)).toEqual([
120
+ "x/new: no meta.docs.url — write docs/rules/x/new.md and point at it",
121
+ ]);
122
+ });
123
+
124
+ it("a page that does not exist, outside docs/rules, or not on the default branch fails", () => {
125
+ expect(
126
+ pageProblems(
127
+ {
128
+ "x/gone": withPage("docs/rules/x/gone.md"),
129
+ "x/readme": withPage("README.md"),
130
+ "x/elsewhere": {
131
+ meta: { docs: { url: "https://example.org/x.md" } },
132
+ },
133
+ },
134
+ new Set(),
135
+ exists,
136
+ ),
137
+ ).toHaveLength(3);
138
+ });
139
+
140
+ it("a listed rule that has a page now fails, so the list shrinks", () => {
141
+ expect(
142
+ pageProblems(
143
+ { "x/ok": withPage("docs/rules/x/ok.md") },
144
+ new Set(["x/ok"]),
145
+ exists,
146
+ ),
147
+ ).toEqual(["x/ok: has a page now — take it off AWAITING_PAGE"]);
148
+ });
149
+
150
+ it("a rule with an existing page, and a listed rule without one, pass", () => {
151
+ expect(
152
+ pageProblems(
153
+ { "x/ok": withPage("docs/rules/x/ok.md"), "x/old": {} },
154
+ new Set(["x/old"]),
155
+ exists,
156
+ ),
157
+ ).toEqual([]);
158
+ });
159
+ });