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
@@ -4,21 +4,10 @@ description: Verify every citation is a real work with correct metadata, and tha
4
4
  allowed-tools: [Read, Write, Edit, Grep, Glob, Bash, WebSearch, WebFetch, Agent]
5
5
  ---
6
6
 
7
- <!-- vigiles:sha256:3a7e10662d0084c4 compiled from skills/verify-citations/SKILL.md.spec.ts -->
7
+ <!-- vigiles:sha256:fe3634eb63db5c0d compiled from skills/verify-citations/SKILL.md.spec.ts -->
8
8
 
9
9
  # verify-citations — every cite real, the delta explicit
10
10
 
11
- ## Run me
12
-
13
- 🔴 FIRST, before any other step:
14
-
15
- ```
16
- node .claude/skills/paper-pipeline/scripts/announce.mjs verify-citations <paper-dir>
17
- ```
18
-
19
- An advisory pass cannot be observed failing — silence is both its error state and its normal
20
- state — so starting is an event, and events get written down.
21
-
22
11
  Reviewers reject on a single fabricated or wrong citation — it reads as either sloppiness or a
23
12
  hallucinated bibliography, and either one is fatal. And they reject on "this was already done by ___" if
24
13
  your delta over the nearest prior work isn't spelled out. This skill closes both holes before submit.
@@ -30,8 +19,7 @@ bibliography changes, including the edit you make on the last day. Scorecard row
30
19
  `<paper-dir>/PIPELINE-STATUS.md`; see the CONTINUOUS section of `paper-pipeline`.
31
20
 
32
21
  - **A pass dated before the current text is STALE, and stale reads as green.** The date in the `cites`
33
- row is compared against the paper's own mtime, and `../paper-pipeline/scripts/pipeline-check.mjs` reports the
34
- mismatch — because a `☑` from three days and forty edits ago is worse than a `☐`: it looks closed.
22
+ row is compared against the paper's own mtime — because a `☑` from three days and forty edits ago is worse than a `☐`: it looks closed.
35
23
  - **"We checked last cycle" does not count.** Say it plainly, because it is the exact sentence that
36
24
  ships a fabricated cite: a review round adds text, added text adds citations, and the run that
37
25
  cleared the bibliography never saw them.
@@ -294,30 +282,6 @@ therefore being checked for the first time.
294
282
  fails CI if any line or branch of them goes unexercised. Run `npx vitest run src/references.test.ts`
295
283
  after any edit to the rules.
296
284
 
297
- ## 6. Record the verdict
298
-
299
- 🔴 LAST step, once the deliverable exists:
300
-
301
- ```
302
- node .claude/skills/paper-pipeline/scripts/ledger.mjs record verify-citations <paper-dir> FINDING <count> <report-path>
303
- node .claude/skills/paper-pipeline/scripts/ledger.mjs record verify-citations <paper-dir> ABSTAINED <reason> "<one line>"
304
- ```
305
-
306
- **FINDING** — `<count>` is the number of cites still `% VERIFY` or `unresolvable`; `<report-path>`
307
- lists them. Add `--blocking` for a cite an authoritative source disproves, or a missing delta
308
- sentence; either is a submit blocker.
309
- **ABSTAINED** — `no-witness`: every cite resolved with correct metadata and the delta sentence is in
310
- the prose. `crashed`: the registries were unreachable, so nothing was verified — which is emphatically
311
- not the same as everything checking out, and under the old vocabulary both came back `PASS`.
312
-
313
- 🔴 **There is no PASS.** `no-witness` here means every cite the paper makes was looked up and none
314
- of them came back wrong. It does not mean the bibliography is complete: this check cannot see a work
315
- that should have been cited and is not. That gap belongs to `map-prior-work`, and no row here covers it.
316
-
317
- Because this check is CONTINUOUS, it writes a row per run and never a final one. That is the point:
318
- the ledger compares the row against the paper's current bytes, so a pass that predates the last three
319
- `\cite` additions reports as STALE-PAPER instead of reading as green.
320
-
321
285
  ## Rules
322
286
  - **Re-run on every `\cite` added or moved.** A `cites` row older than the paper's text is a stale pass,
323
287
  not a green one — "we checked last cycle" is not a verification.
@@ -24,17 +24,6 @@ export default experimental_skill({
24
24
  body: `
25
25
  # verify-citations — every cite real, the delta explicit
26
26
 
27
- ## Run me
28
-
29
- 🔴 FIRST, before any other step:
30
-
31
- \`\`\`
32
- node .claude/skills/paper-pipeline/scripts/announce.mjs verify-citations <paper-dir>
33
- \`\`\`
34
-
35
- An advisory pass cannot be observed failing — silence is both its error state and its normal
36
- state — so starting is an event, and events get written down.
37
-
38
27
  Reviewers reject on a single fabricated or wrong citation — it reads as either sloppiness or a
39
28
  hallucinated bibliography, and either one is fatal. And they reject on "this was already done by ___" if
40
29
  your delta over the nearest prior work isn't spelled out. This skill closes both holes before submit.
@@ -46,8 +35,7 @@ bibliography changes, including the edit you make on the last day. Scorecard row
46
35
  \`<paper-dir>/PIPELINE-STATUS.md\`; see the CONTINUOUS section of \`paper-pipeline\`.
47
36
 
48
37
  - **A pass dated before the current text is STALE, and stale reads as green.** The date in the \`cites\`
49
- row is compared against the paper's own mtime, and \`../paper-pipeline/scripts/pipeline-check.mjs\` reports the
50
- mismatch — because a \`☑\` from three days and forty edits ago is worse than a \`☐\`: it looks closed.
38
+ row is compared against the paper's own mtime — because a \`☑\` from three days and forty edits ago is worse than a \`☐\`: it looks closed.
51
39
  - **"We checked last cycle" does not count.** Say it plainly, because it is the exact sentence that
52
40
  ships a fabricated cite: a review round adds text, added text adds citations, and the run that
53
41
  cleared the bibliography never saw them.
@@ -310,30 +298,6 @@ therefore being checked for the first time.
310
298
  fails CI if any line or branch of them goes unexercised. Run \`npx vitest run src/references.test.ts\`
311
299
  after any edit to the rules.
312
300
 
313
- ## 6. Record the verdict
314
-
315
- 🔴 LAST step, once the deliverable exists:
316
-
317
- \`\`\`
318
- node .claude/skills/paper-pipeline/scripts/ledger.mjs record verify-citations <paper-dir> FINDING <count> <report-path>
319
- node .claude/skills/paper-pipeline/scripts/ledger.mjs record verify-citations <paper-dir> ABSTAINED <reason> "<one line>"
320
- \`\`\`
321
-
322
- **FINDING** — \`<count>\` is the number of cites still \`% VERIFY\` or \`unresolvable\`; \`<report-path>\`
323
- lists them. Add \`--blocking\` for a cite an authoritative source disproves, or a missing delta
324
- sentence; either is a submit blocker.
325
- **ABSTAINED** — \`no-witness\`: every cite resolved with correct metadata and the delta sentence is in
326
- the prose. \`crashed\`: the registries were unreachable, so nothing was verified — which is emphatically
327
- not the same as everything checking out, and under the old vocabulary both came back \`PASS\`.
328
-
329
- 🔴 **There is no PASS.** \`no-witness\` here means every cite the paper makes was looked up and none
330
- of them came back wrong. It does not mean the bibliography is complete: this check cannot see a work
331
- that should have been cited and is not. That gap belongs to \`map-prior-work\`, and no row here covers it.
332
-
333
- Because this check is CONTINUOUS, it writes a row per run and never a final one. That is the point:
334
- the ledger compares the row against the paper's current bytes, so a pass that predates the last three
335
- \`\\cite\` additions reports as STALE-PAPER instead of reading as green.
336
-
337
301
  ## Rules
338
302
  - **Re-run on every \`\\cite\` added or moved.** A \`cites\` row older than the paper's text is a stale pass,
339
303
  not a green one — "we checked last cycle" is not a verification.
@@ -59,9 +59,8 @@ function missing(m: BanalMissing): string {
59
59
 
60
60
  type Kind = BanalFailure["kind"];
61
61
  /** One sentence-writer per variant. A mapped type over `kind`, so a new variant without one is a compile error. */
62
- type Describers = {
63
- readonly [K in Kind]: (f: Extract<BanalFailure, { kind: K }>) => Lines;
64
- };
62
+ type FailureOf = { readonly [K in Kind]: Extract<BanalFailure, { kind: K }> };
63
+ type Describers = { readonly [K in Kind]: (f: FailureOf[K]) => Lines };
65
64
 
66
65
  const orNone = (s: string): string => s || "no output";
67
66
 
@@ -101,15 +100,15 @@ const DESCRIBE: Describers = {
101
100
  };
102
101
 
103
102
  /**
104
- * What to tell a person. Most failures are one line; a sha256 mismatch shows both hashes.
105
- *
106
- * The one assertion below narrows the table's entry for `f.kind` to a function of `f`: the mapped
107
- * type already pairs each kind with its own variant, and TypeScript cannot correlate the index with
108
- * the argument on its own.
103
+ * The describer for `kind`, applied to its own variant. Generic over the kind, so the table's entry
104
+ * and the argument are correlated through `FailureOf[K]` and need no assertion (TypeScript#47109).
109
105
  */
106
+ const describeKind = <K extends Kind>(kind: K, f: FailureOf[K]): Lines =>
107
+ DESCRIBE[kind](f);
108
+
109
+ /** What to tell a person. Most failures are one line; a sha256 mismatch shows both hashes. */
110
110
  export function describe(f: BanalFailure): Lines {
111
- const write = DESCRIBE[f.kind] as (f: BanalFailure) => Lines;
112
- return write(f);
111
+ return describeKind(f.kind, f);
113
112
  }
114
113
 
115
114
  /** `describe` as one line, for a caller that has a single line to fill. */
@@ -2,12 +2,12 @@
2
2
  * `adapters/banal/index.ts` — banal as the two ports the app sees. What `run.test.ts` checks in
3
3
  * banal's own terms is checked here as the domain receives it: a `Geometry` whose provenance names
4
4
  * banal and the rule that found it, a reason that is one line of text, a `Ready` that says what was
5
- * verified. In-memory ports throughout; the real banal is `test/e2e/banal.ts`.
5
+ * verified. In-memory ports throughout; the real banal is `test/e2e/tex/banal.e2e.ts`.
6
6
  */
7
7
  import assert from "node:assert/strict";
8
8
  import { test } from "vitest";
9
9
  import { whyNoGeometry } from "../../domain/geometry.ts";
10
- import type { AbsolutePath } from "../../domain/paths.ts";
10
+ import { absolutePath } from "../../domain/paths.ts";
11
11
  import type { ProcessExit } from "../../ports/process.ts";
12
12
  import { sha256Hex } from "../../domain/sha256.ts";
13
13
  import {
@@ -21,7 +21,7 @@ import type { Ready } from "../../ports/tool-installer.ts";
21
21
  import { banalInstaller, banalMeasurer, parseBanalSettings } from "./index.ts";
22
22
 
23
23
  const dirs = { home: "/h", tmp: "/t", cwd: "/r" };
24
- const project = "/r" as AbsolutePath;
24
+ const project = absolutePath("/r");
25
25
  const MEASURED =
26
26
  '{"papersize":[792,612],"columns":2,"bodyfontsize":9,"pages":[{},{"type":"bib","reffontsize":7}]}';
27
27
 
@@ -14,7 +14,8 @@ import type { Geometry, Provenance } from "../../domain/geometry.ts";
14
14
  import type { AbsolutePath } from "../../domain/paths.ts";
15
15
  import { err, ok } from "../../domain/result.ts";
16
16
  import type { MeasureGeometry } from "../../ports/measure-geometry.ts";
17
- import type { Ready, ToolInstaller } from "../../ports/tool-installer.ts";
17
+ import { ready, type Ready } from "../../domain/ready.ts";
18
+ import type { ToolInstaller } from "../../ports/tool-installer.ts";
18
19
  import { describe } from "./failure.ts";
19
20
  import { provenanceLabel, type BanalCandidate } from "./locate.ts";
20
21
  import { pinLabel, type BanalSource } from "./pin.ts";
@@ -65,15 +66,12 @@ export function banalMeasurer(
65
66
  }
66
67
 
67
68
  /** The one place banal's `Ready` is minted: from an `Installed`, whose `PinnedBanal` only run.ts mints. */
68
- const ready = (i: Installed): Ready => {
69
- // Widened by annotation: the brand of the path is not part of `Ready`'s shape.
70
- const where: string = i.banal.path;
71
- return {
72
- where,
69
+ const readyOf = (i: Installed): Ready =>
70
+ ready({
71
+ where: i.banal.path,
73
72
  fresh: i.fresh,
74
73
  verified: "sha256 verified, and it measured a probe page",
75
- } as Ready;
76
- };
74
+ });
77
75
 
78
76
  /**
79
77
  * banal as the `ToolInstaller` port: download at the pin, check the sha256, accept by measuring a
@@ -91,12 +89,12 @@ export function banalInstaller(
91
89
  ...(source ? { source } : {}),
92
90
  onDownload: report,
93
91
  });
94
- return r.ok ? ok(ready(r.value)) : err(describe(r.error));
92
+ return r.ok ? ok(readyOf(r.value)) : err(describe(r.error));
95
93
  },
96
94
  check() {
97
95
  const r = checkBanal(d, s, source);
98
96
  return r.ok
99
- ? ok(ready({ banal: r.value, fresh: false }))
97
+ ? ok(readyOf({ banal: r.value, fresh: false }))
100
98
  : err(describe(r.error));
101
99
  },
102
100
  };
@@ -15,14 +15,19 @@ export interface ShaMismatch {
15
15
  readonly got: Sha256;
16
16
  }
17
17
 
18
+ // The brand's one owner: the predicate IS the pin check, so no assertion is needed to mint the type.
19
+ const isPinned = (
20
+ bytes: Uint8Array,
21
+ source: BanalSource,
22
+ ): bytes is PinnedBytes => sha256Hex(bytes) === source.sha256;
23
+
18
24
  export function verifyPin(
19
25
  bytes: Uint8Array,
20
26
  source: BanalSource,
21
27
  ): Result<PinnedBytes, ShaMismatch> {
22
- const got = sha256Hex(bytes);
23
- return got === source.sha256
24
- ? ok(bytes as PinnedBytes)
25
- : err({ expected: source.sha256, got });
28
+ return isPinned(bytes, source)
29
+ ? ok(bytes)
30
+ : err({ expected: source.sha256, got: sha256Hex(bytes) });
26
31
  }
27
32
 
28
33
  /** What is installed, judged against the pin. */
@@ -11,15 +11,15 @@ import { tmpdir } from "node:os";
11
11
  import { join } from "node:path";
12
12
  import { afterAll as after, test } from "vitest";
13
13
  import { nodeAdapters } from "../node/index.ts";
14
- import type { AbsolutePath } from "../../domain/paths.ts";
14
+ import { absolutePath } from "../../domain/paths.ts";
15
+ import type { Scratch } from "../../ports/workspace.ts";
15
16
  import {
16
17
  banalCommand,
18
+ parseShQuoted,
17
19
  shQuote,
18
20
  stage,
19
21
  stageBanalInput,
20
- type StagedInput,
21
22
  } from "./invocation.ts";
22
- import type { LocatedBanal } from "./locate.ts";
23
23
  import { XML_DIALECT } from "./xml.ts";
24
24
 
25
25
  test("the staging holds BOTH files: the XML and the -v stub answering the dialect paperlint writes", () => {
@@ -32,11 +32,14 @@ test("the staging holds BOTH files: the XML and the -v stub answering the dialec
32
32
  });
33
33
 
34
34
  test("banalCommand: perl runs banal on the staged .xml, with $PDFTOHTML quoted", () => {
35
- const banal = {
36
- path: "/b/banal" as AbsolutePath,
37
- provenance: { kind: "cache" },
38
- } as LocatedBanal;
39
- const staged = { xml: "/s/paper.xml", stub: "/s d/pdftohtml" } as StagedInput;
35
+ // A scratch that places the two files in different directories, one with a space.
36
+ const scratch: Scratch = {
37
+ dir: absolutePath("/s"),
38
+ write: (name) =>
39
+ absolutePath(name === "paper.xml" ? "/s/paper.xml" : "/s d/pdftohtml"),
40
+ };
41
+ const staged = stage(scratch, stageBanalInput([]));
42
+ const banal = { path: absolutePath("/b/banal") };
40
43
  const c = banalCommand(banal, staged, { PATH: "/bin" });
41
44
  assert.deepEqual(
42
45
  [c.file, ...c.args],
@@ -50,6 +53,15 @@ test("shQuote survives a single quote", () => {
50
53
  assert.equal(shQuote("it's"), `'it'"'"'s'`);
51
54
  });
52
55
 
56
+ test("parseShQuoted refuses a word the shell would split or expand", () => {
57
+ assert.equal(parseShQuoted(`'a b'`), `'a b'`);
58
+ assert.throws(() => parseShQuoted("a b"), /not one single-quoted shell word/);
59
+ assert.throws(
60
+ () => parseShQuoted(`'a'b'`),
61
+ /not one single-quoted shell word/,
62
+ );
63
+ });
64
+
53
65
  const root = realpathSync(mkdtempSync(join(tmpdir(), "paperlint-stage-test-")));
54
66
  after(() => {
55
67
  rmSync(root, { recursive: true, force: true });
@@ -7,13 +7,13 @@
7
7
  * (`Error: Failed to run pdftohtml`, measured); told a version below 0.85 it applies a larger
8
8
  * correction and every font size moves. So `$PDFTOHTML` (banal's own override, line 166) points at a
9
9
  * stub that answers `-v` with `XML_DIALECT.version` — the pdftohtml whose XML `xml.ts` writes — and
10
- * refuses anything else. `test/e2e/banal.ts` shows both failures with the real banal.
10
+ * refuses anything else. `test/e2e/tex/banal.e2e.ts` shows both failures with the real banal.
11
11
  */
12
12
  import type { Opaque } from "ts-essentials";
13
13
  import type { AbsolutePath } from "../../domain/paths.ts";
14
14
  import type { Command } from "../../ports/process.ts";
15
15
  import type { Scratch } from "../../ports/workspace.ts";
16
- import type { LocatedBanal } from "./locate.ts";
16
+ import type { BanalCandidate } from "./locate.ts";
17
17
  import type { PageLayout } from "../../domain/page-layout.ts";
18
18
  import { pdf2xml, XML_DIALECT } from "./xml.ts";
19
19
 
@@ -23,9 +23,19 @@ export const BANAL_RUN_MS = 120_000;
23
23
  /** A path quoted for `/bin/sh`. Minted by `shQuote` only. */
24
24
  export type ShQuoted = Opaque<string, "ShQuoted">;
25
25
 
26
+ // One single-quoted word: any run of non-quotes, each quote spelled `'"'"'`. What `shQuote` writes.
27
+ const SH_QUOTED = /^'(?:[^']|'"'"')*'$/;
28
+ const isShQuoted = (s: string): s is ShQuoted => SH_QUOTED.test(s);
29
+
30
+ /** A word already quoted for `/bin/sh`, checked; anything else throws. */
31
+ export function parseShQuoted(s: string): ShQuoted {
32
+ if (!isShQuoted(s)) throw new Error(`not one single-quoted shell word: ${s}`);
33
+ return s;
34
+ }
35
+
26
36
  /** banal interpolates `$PDFTOHTML` into a shell command unquoted — so the value is quoted here. */
27
37
  export const shQuote = (s: string): ShQuoted =>
28
- `'${s.replace(/'/g, `'"'"'`)}'` as ShQuoted;
38
+ parseShQuoted(`'${s.replace(/'/g, `'"'"'`)}'`);
29
39
 
30
40
  /** The stub standing in for `pdftohtml`: it answers `-v` and refuses to convert anything. */
31
41
  export const PDFTOHTML_STUB = [
@@ -64,11 +74,15 @@ export const stageBanalInput = (
64
74
  ],
65
75
  });
66
76
 
77
+ // Not exported: only `stage` can write the property, so only it can make a StagedInput.
78
+ const STAGED = Symbol("StagedInput");
79
+
67
80
  /** The staging written to disk. Minted by `stage` only: both files written, or nothing returned. */
68
- export type StagedInput = Opaque<
69
- { readonly xml: AbsolutePath; readonly stub: AbsolutePath },
70
- "StagedInput"
71
- >;
81
+ export interface StagedInput {
82
+ readonly xml: AbsolutePath;
83
+ readonly stub: AbsolutePath;
84
+ readonly [STAGED]: true;
85
+ }
72
86
 
73
87
  /** Write a staging into a scratch directory, the stub executable. The only minter of `StagedInput`. */
74
88
  export function stage(s: Scratch, staging: BanalStaging): StagedInput {
@@ -76,12 +90,13 @@ export function stage(s: Scratch, staging: BanalStaging): StagedInput {
76
90
  return {
77
91
  xml: s.write(xml.name, xml.content, xml.mode),
78
92
  stub: s.write(stub.name, stub.content, stub.mode),
79
- } as StagedInput;
93
+ [STAGED]: true,
94
+ };
80
95
  }
81
96
 
82
97
  /** `perl banal -no-time -json <xml>`, with `$PDFTOHTML` quoted by construction. */
83
98
  export function banalCommand(
84
- banal: LocatedBanal,
99
+ banal: Pick<BanalCandidate, "path">,
85
100
  staged: StagedInput,
86
101
  baseEnv: Readonly<Record<string, string>>,
87
102
  ): Command {
@@ -1,13 +1,13 @@
1
1
  import assert from "node:assert/strict";
2
2
  import { test } from "vitest";
3
- import type { AbsolutePath } from "../../domain/paths.ts";
3
+ import { absolutePath } from "../../domain/paths.ts";
4
4
  import { err } from "../../domain/result.ts";
5
5
  import { installedBanal, lookupOrder, pickBanal } from "./locate.ts";
6
6
  import { BANAL_PIN } from "./pin.ts";
7
7
  import { parseBanalSettings } from "./settings.ts";
8
8
 
9
9
  const dirs = { home: "/h", tmp: "/t", cwd: "/w" };
10
- const project = "/p" as AbsolutePath;
10
+ const project = absolutePath("/p");
11
11
  const cache = `/h/.cache/paperlint/banal/${BANAL_PIN.commit.slice(0, 12)}/banal`;
12
12
  const on =
13
13
  (...files: string[]) =>
@@ -3,9 +3,7 @@
3
3
  * `$BANAL` (an explicit choice — if it names nothing, there is NO fallback), `<project>/vendor/banal`
4
4
  * (a project that vendors its own copy), then the one `paperlint toolchain` installed.
5
5
  */
6
- import { join } from "node:path";
7
- import type { Opaque } from "ts-essentials";
8
- import type { AbsolutePath } from "../../domain/paths.ts";
6
+ import { joinPath, type AbsolutePath } from "../../domain/paths.ts";
9
7
  import { err, ok, type Result } from "../../domain/result.ts";
10
8
  import { BANAL_PIN } from "./pin.ts";
11
9
  import type { BanalSettings } from "./settings.ts";
@@ -20,14 +18,20 @@ export interface BanalCandidate {
20
18
  readonly provenance: Provenance;
21
19
  }
22
20
 
23
- /** A candidate the `Files` port confirmed is a file. Minted by `pickBanal` only. */
24
- export type LocatedBanal = Opaque<BanalCandidate, "LocatedBanal">;
21
+ // Not exported: only this module can write the property, so only `locate` can make a LocatedBanal.
22
+ const LOCATED = Symbol("LocatedBanal");
25
23
 
26
- /**
27
- * `paperlint toolchain`'s banal after its bytes hashed to the pin AND it measured the probe page. Minted by
28
- * `ensureBanal` / `checkBanal` only (`./index.ts`): a "ready" line cannot be printed without one.
29
- */
30
- export type PinnedBanal = Opaque<BanalCandidate, "PinnedBanal">;
24
+ /** A candidate the `Files` port confirmed is a file. Minted by `pickBanal` (through `locate`) only. */
25
+ export type LocatedBanal = BanalCandidate & { readonly [LOCATED]: true };
26
+
27
+ /** `path` as a banal to run — when the `Files` port says it is a file, else null. */
28
+ function locate(
29
+ path: AbsolutePath,
30
+ kind: Provenance["kind"],
31
+ isFile: (p: AbsolutePath) => boolean,
32
+ ): LocatedBanal | null {
33
+ return isFile(path) ? { path, provenance: { kind }, [LOCATED]: true } : null;
34
+ }
31
35
 
32
36
  /** Why there is no banal to run. */
33
37
  export type BanalMissing =
@@ -47,7 +51,7 @@ export type LookupOrder =
47
51
 
48
52
  /** Where `paperlint toolchain` puts the pinned banal: one directory per HotCRP commit. */
49
53
  export const installedBanal = (s: BanalSettings): AbsolutePath =>
50
- join(s.cacheDir, BANAL_PIN.commit.slice(0, 12), "banal") as AbsolutePath;
54
+ joinPath(s.cacheDir, BANAL_PIN.commit.slice(0, 12), "banal");
51
55
 
52
56
  export function lookupOrder(
53
57
  s: BanalSettings,
@@ -56,25 +60,27 @@ export function lookupOrder(
56
60
  if (s.explicit) return { kind: "explicit", path: s.explicit };
57
61
  return {
58
62
  kind: "search",
59
- vendor: join(projectRoot, "vendor", "banal") as AbsolutePath,
63
+ vendor: joinPath(projectRoot, "vendor", "banal"),
60
64
  cache: installedBanal(s),
61
65
  };
62
66
  }
63
67
 
64
- const located = (path: AbsolutePath, kind: Provenance["kind"]) =>
65
- ok({ path, provenance: { kind } } as LocatedBanal);
66
-
67
68
  export function pickBanal(
68
69
  order: LookupOrder,
69
70
  isFile: (p: AbsolutePath) => boolean,
70
71
  ): Result<LocatedBanal, BanalMissing> {
71
- if (order.kind === "explicit")
72
- return isFile(order.path)
73
- ? located(order.path, "env")
72
+ if (order.kind === "explicit") {
73
+ const own = locate(order.path, "env", isFile);
74
+ return own
75
+ ? ok(own)
74
76
  : err({ kind: "explicit-not-found", path: order.path });
75
- if (isFile(order.vendor)) return located(order.vendor, "vendor");
76
- if (isFile(order.cache)) return located(order.cache, "cache");
77
- return err({ kind: "not-installed", installed: order.cache });
77
+ }
78
+ const found =
79
+ locate(order.vendor, "vendor", isFile) ??
80
+ locate(order.cache, "cache", isFile);
81
+ return found
82
+ ? ok(found)
83
+ : err({ kind: "not-installed", installed: order.cache });
78
84
  }
79
85
 
80
86
  /** Which rule found it, as a person reads it. */
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * `output.ts` — every way a banal run can end, as a table of `ProcessExit` → `Result`, and the
3
- * measurement → geometry projection. Pure: no perl, no disk. The real banal is `test/e2e/banal.ts`.
3
+ * measurement → geometry projection. Pure: no perl, no disk. The real banal is `test/e2e/tex/banal.e2e.ts`.
4
4
  */
5
5
  import assert from "node:assert/strict";
6
6
  import { test } from "vitest";
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * `adapters/banal/run.ts` — running and installing banal, on in-memory ports: which `Command` it builds, what it stages,
3
3
  * what it downloads and writes, and what it concludes. No perl, no disk, no network: the adapters
4
- * record, and each assertion reads the record. Real perl and the real banal are `test/e2e/banal.ts`;
4
+ * record, and each assertion reads the record. Real perl and the real banal are `test/e2e/tex/banal.e2e.ts`;
5
5
  * the adapters' own behaviour is `src/adapters/node/*.test.ts`.
6
6
  */
7
7
  import assert from "node:assert/strict";
@@ -18,7 +18,7 @@ import { checkBanal, ensureBanal, measureGeometry } from "./run.ts";
18
18
  import { sha256Hex } from "../../domain/sha256.ts";
19
19
  import { installedBanal } from "./locate.ts";
20
20
  import { parseBanalSettings } from "./settings.ts";
21
- import type { AbsolutePath } from "../../domain/paths.ts";
21
+ import { absolutePath } from "../../domain/paths.ts";
22
22
  import type { Command, ProcessExit } from "../../ports/process.ts";
23
23
 
24
24
  const s = parseBanalSettings(
@@ -29,7 +29,7 @@ const noExplicit = parseBanalSettings(
29
29
  { PATH: "/bin" },
30
30
  { home: "/h", tmp: "/t", cwd: "/w" },
31
31
  );
32
- const project = "/p" as AbsolutePath;
32
+ const project = absolutePath("/p");
33
33
  /** What banal prints for the one-page probe. */
34
34
  const MEASURED = '{"bodyfontsize": 10.3, "columns": 1, "pages": [{}]}';
35
35
  const banalAnswers = (e: ProcessExit) =>
@@ -44,8 +44,6 @@ import {
44
44
  lookupOrder,
45
45
  pickBanal,
46
46
  type BanalCandidate,
47
- type LocatedBanal,
48
- type PinnedBanal,
49
47
  } from "./locate.ts";
50
48
  import {
51
49
  geometryOf,
@@ -89,11 +87,14 @@ export type BanalOutcome =
89
87
  readonly tried: BanalCandidate | null;
90
88
  };
91
89
 
92
- /** Run `banal` on `pages` in a scratch directory that is gone when this returns. */
90
+ /**
91
+ * Run `banal` on `pages` in a scratch directory that is gone when this returns. It needs the path
92
+ * only: `measureGeometry` passes the banal `pickBanal` located, `probe` the one whose bytes it verified.
93
+ */
93
94
  function runBanal(
94
95
  d: BanalDeps,
95
96
  s: BanalSettings,
96
- banal: LocatedBanal,
97
+ banal: Pick<BanalCandidate, "path">,
97
98
  pages: readonly PageLayout[],
98
99
  ): Result<BanalMeasurement, BanalFailure> {
99
100
  return d.workspace.within("paperlint-banal-", (scratch) =>
@@ -148,6 +149,15 @@ function perlRuns(d: BanalDeps, s: BanalSettings): boolean {
148
149
  return r.kind === "exited" && r.status === 0;
149
150
  }
150
151
 
152
+ // Not exported: only `probe` below can write the property, so only it can make a PinnedBanal.
153
+ const PINNED = Symbol("PinnedBanal");
154
+
155
+ /**
156
+ * `paperlint toolchain`'s banal after its bytes hashed to the pin AND it measured the probe page. Minted by
157
+ * `probe` only, for `ensureBanal` / `checkBanal` (`./index.ts`): a "ready" line cannot be printed without one.
158
+ */
159
+ export type PinnedBanal = BanalCandidate & { readonly [PINNED]: true };
160
+
151
161
  /**
152
162
  * Does the installed, pin-verified banal RUN — accepted by measuring the probe page, not by a
153
163
  * download's exit code. The one place a `PinnedBanal` is minted; callers verified the bytes first.
@@ -157,13 +167,9 @@ function probe(
157
167
  s: BanalSettings,
158
168
  path: AbsolutePath,
159
169
  ): Result<PinnedBanal, BanalFailure> {
160
- const at: BanalCandidate = { path, provenance: { kind: "cache" } };
161
- const r = andThen(
162
- runBanal(d, s, at as LocatedBanal, [PROBE_PAGE]),
163
- acceptProbe,
164
- );
170
+ const r = andThen(runBanal(d, s, { path }, [PROBE_PAGE]), acceptProbe);
165
171
  return r.ok
166
- ? ok(at as PinnedBanal)
172
+ ? ok({ path, provenance: { kind: "cache" }, [PINNED]: true })
167
173
  : err({ kind: "does-not-run", path, why: r.error });
168
174
  }
169
175
 
@@ -9,7 +9,7 @@ import {
9
9
  type Environment,
10
10
  type HostDirs,
11
11
  } from "../../domain/host.ts";
12
- import type { AbsolutePath } from "../../domain/paths.ts";
12
+ import { absolutePath, type AbsolutePath } from "../../domain/paths.ts";
13
13
 
14
14
  /** An explicit banal to use instead of paperlint's (a path to the script). */
15
15
  export const BANAL_ENV = "BANAL";
@@ -29,7 +29,7 @@ export interface BanalSettings {
29
29
 
30
30
  /** `resolve` with an absolute base never consults the process's cwd. */
31
31
  const absolute = (cwd: string, p: string): AbsolutePath =>
32
- resolve(cwd, p) as AbsolutePath;
32
+ absolutePath(resolve(cwd, p));
33
33
 
34
34
  /** An unset variable and an empty one mean the same: not set. */
35
35
  const set = (v: string | undefined): string | null => (v ? v : null);
@@ -4,7 +4,7 @@
4
4
  *
5
5
  * Every page here is written by hand. The real-PDF half — pdf.js boxes of a committed fixture,
6
6
  * written as XML, measured by the real banal and compared with banal on real pdftohtml — is
7
- * `test/e2e/banal.ts`, which runs where `paperlint toolchain` has installed banal.
7
+ * `test/e2e/tex/banal.e2e.ts`, which runs where `paperlint toolchain` has installed banal.
8
8
  */
9
9
  import assert from "node:assert/strict";
10
10
  import { test } from "vitest";
@@ -15,18 +15,18 @@ import { tmpdir } from "node:os";
15
15
  import { join } from "node:path";
16
16
  import { pathToFileURL } from "node:url";
17
17
  import { afterAll as after, test } from "vitest";
18
- import type { AbsolutePath } from "../../domain/paths.ts";
18
+ import { absolutePath, joinPath } from "../../domain/paths.ts";
19
19
  import { curlDownload, curlFailure } from "./download.io.ts";
20
20
  import type { ProcessExit } from "../../ports/process.ts";
21
21
  import { spawnProcess } from "../node/index.ts";
22
22
 
23
- const root = realpathSync(
24
- mkdtempSync(join(tmpdir(), "paperlint-download-test-")),
23
+ const root = absolutePath(
24
+ realpathSync(mkdtempSync(join(tmpdir(), "paperlint-download-test-"))),
25
25
  );
26
26
  after(() => {
27
27
  rmSync(root, { recursive: true, force: true });
28
28
  });
29
- const at = (...p: string[]) => join(root, ...p) as AbsolutePath;
29
+ const at = (...p: string[]) => joinPath(root, ...p);
30
30
 
31
31
  const dl = (env: Record<string, string>) => {
32
32
  const tmp = at("dl-tmp");