paperlint 2.0.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 (762) hide show
  1. package/.github/dependabot.yml +72 -0
  2. package/.github/workflows/ci.yml +297 -0
  3. package/.github/workflows/dependabot-automerge.yml +70 -0
  4. package/.github/workflows/pr-title.yml +59 -0
  5. package/.github/workflows/release.yml +54 -0
  6. package/CLAUDE.md +598 -0
  7. package/CONTRIBUTING.md +159 -0
  8. package/LICENSE +21 -0
  9. package/README.md +240 -0
  10. package/action.harness.mjs +287 -0
  11. package/action.mutations.mjs +162 -0
  12. package/action.yml +138 -0
  13. package/bin/rpp.mjs +43 -0
  14. package/dist/action-ref.d.ts +12 -0
  15. package/dist/action-ref.d.ts.map +1 -0
  16. package/dist/action-ref.js +16 -0
  17. package/dist/action-ref.js.map +1 -0
  18. package/dist/adapters/banal/failure.d.ts +73 -0
  19. package/dist/adapters/banal/failure.d.ts.map +1 -0
  20. package/dist/adapters/banal/failure.js +58 -0
  21. package/dist/adapters/banal/failure.js.map +1 -0
  22. package/dist/adapters/banal/index.d.ts +17 -0
  23. package/dist/adapters/banal/index.d.ts.map +1 -0
  24. package/dist/adapters/banal/index.js +56 -0
  25. package/dist/adapters/banal/index.js.map +1 -0
  26. package/dist/adapters/banal/install.d.ts +26 -0
  27. package/dist/adapters/banal/install.d.ts.map +1 -0
  28. package/dist/adapters/banal/install.js +15 -0
  29. package/dist/adapters/banal/install.js.map +1 -0
  30. package/dist/adapters/banal/invocation.d.ts +48 -0
  31. package/dist/adapters/banal/invocation.d.ts.map +1 -0
  32. package/dist/adapters/banal/invocation.js +43 -0
  33. package/dist/adapters/banal/invocation.js.map +1 -0
  34. package/dist/adapters/banal/locate.d.ts +50 -0
  35. package/dist/adapters/banal/locate.d.ts.map +1 -0
  36. package/dist/adapters/banal/locate.js +34 -0
  37. package/dist/adapters/banal/locate.js.map +1 -0
  38. package/dist/adapters/banal/output.d.ts +27 -0
  39. package/dist/adapters/banal/output.d.ts.map +1 -0
  40. package/dist/adapters/banal/output.js +112 -0
  41. package/dist/adapters/banal/output.js.map +1 -0
  42. package/dist/adapters/banal/pin.d.ts +19 -0
  43. package/dist/adapters/banal/pin.d.ts.map +1 -0
  44. package/dist/adapters/banal/pin.js +15 -0
  45. package/dist/adapters/banal/pin.js.map +1 -0
  46. package/dist/adapters/banal/probe.d.ts +12 -0
  47. package/dist/adapters/banal/probe.d.ts.map +1 -0
  48. package/dist/adapters/banal/probe.js +27 -0
  49. package/dist/adapters/banal/probe.js.map +1 -0
  50. package/dist/adapters/banal/run.d.ts +89 -0
  51. package/dist/adapters/banal/run.d.ts.map +1 -0
  52. package/dist/adapters/banal/run.js +104 -0
  53. package/dist/adapters/banal/run.js.map +1 -0
  54. package/dist/adapters/banal/settings.d.ts +18 -0
  55. package/dist/adapters/banal/settings.d.ts.map +1 -0
  56. package/dist/adapters/banal/settings.js +29 -0
  57. package/dist/adapters/banal/settings.js.map +1 -0
  58. package/dist/adapters/banal/xml.d.ts +48 -0
  59. package/dist/adapters/banal/xml.d.ts.map +1 -0
  60. package/dist/adapters/banal/xml.js +67 -0
  61. package/dist/adapters/banal/xml.js.map +1 -0
  62. package/dist/adapters/curl/download.io.d.ts +14 -0
  63. package/dist/adapters/curl/download.io.d.ts.map +1 -0
  64. package/dist/adapters/curl/download.io.js +69 -0
  65. package/dist/adapters/curl/download.io.js.map +1 -0
  66. package/dist/adapters/curl/index.d.ts +6 -0
  67. package/dist/adapters/curl/index.d.ts.map +1 -0
  68. package/dist/adapters/curl/index.js +6 -0
  69. package/dist/adapters/curl/index.js.map +1 -0
  70. package/dist/adapters/memory/index.d.ts +43 -0
  71. package/dist/adapters/memory/index.d.ts.map +1 -0
  72. package/dist/adapters/memory/index.js +79 -0
  73. package/dist/adapters/memory/index.js.map +1 -0
  74. package/dist/adapters/node/files.io.d.ts +3 -0
  75. package/dist/adapters/node/files.io.d.ts.map +1 -0
  76. package/dist/adapters/node/files.io.js +31 -0
  77. package/dist/adapters/node/files.io.js.map +1 -0
  78. package/dist/adapters/node/host.io.d.ts +3 -0
  79. package/dist/adapters/node/host.io.d.ts.map +1 -0
  80. package/dist/adapters/node/host.io.js +14 -0
  81. package/dist/adapters/node/host.io.js.map +1 -0
  82. package/dist/adapters/node/index.d.ts +25 -0
  83. package/dist/adapters/node/index.d.ts.map +1 -0
  84. package/dist/adapters/node/index.js +14 -0
  85. package/dist/adapters/node/index.js.map +1 -0
  86. package/dist/adapters/node/process.io.d.ts +14 -0
  87. package/dist/adapters/node/process.io.d.ts.map +1 -0
  88. package/dist/adapters/node/process.io.js +41 -0
  89. package/dist/adapters/node/process.io.js.map +1 -0
  90. package/dist/adapters/node/workspace.io.d.ts +4 -0
  91. package/dist/adapters/node/workspace.io.d.ts.map +1 -0
  92. package/dist/adapters/node/workspace.io.js +33 -0
  93. package/dist/adapters/node/workspace.io.js.map +1 -0
  94. package/dist/adapters/pdfjs/fill.d.ts +42 -0
  95. package/dist/adapters/pdfjs/fill.d.ts.map +1 -0
  96. package/dist/adapters/pdfjs/fill.js +91 -0
  97. package/dist/adapters/pdfjs/fill.js.map +1 -0
  98. package/dist/build-engine.d.ts +48 -0
  99. package/dist/build-engine.d.ts.map +1 -0
  100. package/dist/build-engine.js +148 -0
  101. package/dist/build-engine.js.map +1 -0
  102. package/dist/build.d.ts +163 -0
  103. package/dist/build.d.ts.map +1 -0
  104. package/dist/build.js +575 -0
  105. package/dist/build.js.map +1 -0
  106. package/dist/cli.d.ts +151 -0
  107. package/dist/cli.d.ts.map +1 -0
  108. package/dist/cli.js +951 -0
  109. package/dist/cli.js.map +1 -0
  110. package/dist/doctor.d.ts +42 -0
  111. package/dist/doctor.d.ts.map +1 -0
  112. package/dist/doctor.js +280 -0
  113. package/dist/doctor.js.map +1 -0
  114. package/dist/domain/geometry.d.ts +71 -0
  115. package/dist/domain/geometry.d.ts.map +1 -0
  116. package/dist/domain/geometry.js +35 -0
  117. package/dist/domain/geometry.js.map +1 -0
  118. package/dist/domain/host.d.ts +16 -0
  119. package/dist/domain/host.d.ts.map +1 -0
  120. package/dist/domain/host.js +8 -0
  121. package/dist/domain/host.js.map +1 -0
  122. package/dist/domain/page-layout.d.ts +34 -0
  123. package/dist/domain/page-layout.d.ts.map +1 -0
  124. package/dist/domain/page-layout.js +8 -0
  125. package/dist/domain/page-layout.js.map +1 -0
  126. package/dist/domain/paths.d.ts +5 -0
  127. package/dist/domain/paths.d.ts.map +1 -0
  128. package/dist/domain/paths.js +2 -0
  129. package/dist/domain/paths.js.map +1 -0
  130. package/dist/domain/result.d.ts +23 -0
  131. package/dist/domain/result.d.ts.map +1 -0
  132. package/dist/domain/result.js +10 -0
  133. package/dist/domain/result.js.map +1 -0
  134. package/dist/domain/sha256.d.ts +7 -0
  135. package/dist/domain/sha256.d.ts.map +1 -0
  136. package/dist/domain/sha256.js +14 -0
  137. package/dist/domain/sha256.js.map +1 -0
  138. package/dist/domain/text.d.ts +6 -0
  139. package/dist/domain/text.d.ts.map +1 -0
  140. package/dist/domain/text.js +7 -0
  141. package/dist/domain/text.js.map +1 -0
  142. package/dist/engine.d.ts +93 -0
  143. package/dist/engine.d.ts.map +1 -0
  144. package/dist/engine.js +119 -0
  145. package/dist/engine.js.map +1 -0
  146. package/dist/exit-code.d.ts +22 -0
  147. package/dist/exit-code.d.ts.map +1 -0
  148. package/dist/exit-code.js +10 -0
  149. package/dist/exit-code.js.map +1 -0
  150. package/dist/facts-file.d.ts +96 -0
  151. package/dist/facts-file.d.ts.map +1 -0
  152. package/dist/facts-file.js +134 -0
  153. package/dist/facts-file.js.map +1 -0
  154. package/dist/hooks-settings.d.ts +141 -0
  155. package/dist/hooks-settings.d.ts.map +1 -0
  156. package/dist/hooks-settings.js +306 -0
  157. package/dist/hooks-settings.js.map +1 -0
  158. package/dist/init.d.ts +201 -0
  159. package/dist/init.d.ts.map +1 -0
  160. package/dist/init.js +579 -0
  161. package/dist/init.js.map +1 -0
  162. package/dist/latex-log.d.ts +80 -0
  163. package/dist/latex-log.d.ts.map +1 -0
  164. package/dist/latex-log.js +187 -0
  165. package/dist/latex-log.js.map +1 -0
  166. package/dist/latex-loop.d.ts +129 -0
  167. package/dist/latex-loop.d.ts.map +1 -0
  168. package/dist/latex-loop.js +113 -0
  169. package/dist/latex-loop.js.map +1 -0
  170. package/dist/link-skills.d.ts +51 -0
  171. package/dist/link-skills.d.ts.map +1 -0
  172. package/dist/link-skills.js +199 -0
  173. package/dist/link-skills.js.map +1 -0
  174. package/dist/new-paper.d.ts +48 -0
  175. package/dist/new-paper.d.ts.map +1 -0
  176. package/dist/new-paper.js +110 -0
  177. package/dist/new-paper.js.map +1 -0
  178. package/dist/pdf-facts.d.ts +44 -0
  179. package/dist/pdf-facts.d.ts.map +1 -0
  180. package/dist/pdf-facts.js +239 -0
  181. package/dist/pdf-facts.js.map +1 -0
  182. package/dist/pdf-geometry.d.ts +170 -0
  183. package/dist/pdf-geometry.d.ts.map +1 -0
  184. package/dist/pdf-geometry.js +158 -0
  185. package/dist/pdf-geometry.js.map +1 -0
  186. package/dist/ports/download.d.ts +9 -0
  187. package/dist/ports/download.d.ts.map +1 -0
  188. package/dist/ports/download.js +2 -0
  189. package/dist/ports/download.js.map +1 -0
  190. package/dist/ports/files.d.ts +11 -0
  191. package/dist/ports/files.d.ts.map +1 -0
  192. package/dist/ports/files.js +2 -0
  193. package/dist/ports/files.js.map +1 -0
  194. package/dist/ports/measure-geometry.d.ts +8 -0
  195. package/dist/ports/measure-geometry.d.ts.map +1 -0
  196. package/dist/ports/measure-geometry.js +2 -0
  197. package/dist/ports/measure-geometry.js.map +1 -0
  198. package/dist/ports/process.d.ts +45 -0
  199. package/dist/ports/process.d.ts.map +1 -0
  200. package/dist/ports/process.js +2 -0
  201. package/dist/ports/process.js.map +1 -0
  202. package/dist/ports/tool-installer.d.ts +29 -0
  203. package/dist/ports/tool-installer.d.ts.map +1 -0
  204. package/dist/ports/tool-installer.js +2 -0
  205. package/dist/ports/tool-installer.js.map +1 -0
  206. package/dist/ports/workspace.d.ts +18 -0
  207. package/dist/ports/workspace.d.ts.map +1 -0
  208. package/dist/ports/workspace.js +2 -0
  209. package/dist/ports/workspace.js.map +1 -0
  210. package/dist/rules-config.d.ts +34 -0
  211. package/dist/rules-config.d.ts.map +1 -0
  212. package/dist/rules-config.js +132 -0
  213. package/dist/rules-config.js.map +1 -0
  214. package/dist/structure.d.ts +34 -0
  215. package/dist/structure.d.ts.map +1 -0
  216. package/dist/structure.js +149 -0
  217. package/dist/structure.js.map +1 -0
  218. package/dist/tex-requirements.d.ts +43 -0
  219. package/dist/tex-requirements.d.ts.map +1 -0
  220. package/dist/tex-requirements.js +127 -0
  221. package/dist/tex-requirements.js.map +1 -0
  222. package/dist/toolchain.d.ts +159 -0
  223. package/dist/toolchain.d.ts.map +1 -0
  224. package/dist/toolchain.js +542 -0
  225. package/dist/toolchain.js.map +1 -0
  226. package/dist/types.d.ts +110 -0
  227. package/dist/types.d.ts.map +1 -0
  228. package/dist/types.js +2 -0
  229. package/dist/types.js.map +1 -0
  230. package/docs/configuration.md +235 -0
  231. package/docs/e2e.md +152 -0
  232. package/docs/incidents.md +59 -0
  233. package/docs/install.md +170 -0
  234. package/docs/optional-rules.md +107 -0
  235. package/docs/package-shape-options.md +262 -0
  236. package/docs/prior-art/README.md +76 -0
  237. package/docs/prior-art/blocking-vs-advisory.md +83 -0
  238. package/docs/prior-art/content-delivery.md +124 -0
  239. package/docs/prior-art/multi-mode-tools.md +106 -0
  240. package/docs/prior-art/nondeterministic-checks.md +99 -0
  241. package/docs/prior-art/package-location.md +422 -0
  242. package/docs/prior-art/paper-folder-scaffolding.md +538 -0
  243. package/docs/prior-art/readme-structure.md +69 -0
  244. package/docs/prior-art/repro/README.md +92 -0
  245. package/docs/prior-art/repro/claim1-allowedtools.mjs +66 -0
  246. package/docs/prior-art/repro/claim1-at2.mjs +40 -0
  247. package/docs/prior-art/repro/claim1-crosschannel.mjs +54 -0
  248. package/docs/prior-art/repro/claim1-frontmatter.mjs +76 -0
  249. package/docs/prior-art/repro/claim1-hook-payload-reporter.mjs +10 -0
  250. package/docs/prior-art/repro/claim1-plugin-frontmatter.mjs +27 -0
  251. package/docs/prior-art/repro/claim1-plugin-skill.mjs +52 -0
  252. package/docs/prior-art/repro/claim1-project-skill.mjs +81 -0
  253. package/docs/prior-art/repro/claim2-marketplace-flat-asclaimed.json +1 -0
  254. package/docs/prior-art/repro/claim2-marketplace-negative-control.json +1 -0
  255. package/docs/prior-art/repro/claim2-marketplace-nested-exact.json +9 -0
  256. package/docs/prior-art/repro/claim2-marketplace-nested-noversion.json +9 -0
  257. package/docs/prior-art/repro/claim2-marketplace-nested-range.json +1 -0
  258. package/docs/prior-art/repro/claim3-imports.mjs +50 -0
  259. package/docs/prior-art/repro/claim4-find-package-json.mjs +8 -0
  260. package/docs/prior-art/repro/claim4-package-dir.mjs +39 -0
  261. package/docs/prior-art/repro/claim4-parent-arg.mjs +17 -0
  262. package/docs/prior-art/repro/claim4-resolve-apis.mjs +21 -0
  263. package/docs/prior-art/repro/claim4-setup-consumers.mjs +45 -0
  264. package/docs/prior-art/repro/claim4-yarn-pnp.mjs +70 -0
  265. package/docs/prior-art/repro/claim5-bin-launch.mjs +39 -0
  266. package/docs/prior-art/repro/claim5-exports-mutation.mjs +57 -0
  267. package/docs/prior-art/repro/claim5-resolved-location-and-bin.mjs +33 -0
  268. package/docs/prior-art/repro/claim6-candidate-ambiguity.mjs +17 -0
  269. package/docs/prior-art/repro/claim6-doc-path-candidates.mjs +27 -0
  270. package/docs/prior-art/test-tooling.md +131 -0
  271. package/docs/rules.md +58 -0
  272. package/docs/texlive-install-decision.md +230 -0
  273. package/docs/toolchain.md +152 -0
  274. package/eslint-rules/doc-fields.harness.mjs +336 -0
  275. package/eslint-rules/doc-fields.mjs +186 -0
  276. package/eslint-rules/doc-fields.mutations.mjs +96 -0
  277. package/eslint-rules/install-path-literals.harness.mjs +121 -0
  278. package/eslint-rules/install-path-literals.mjs +108 -0
  279. package/eslint-rules/install-path-literals.mutations.mjs +62 -0
  280. package/eslint-rules/latex-language.harness.mjs +599 -0
  281. package/eslint-rules/latex-language.mjs +591 -0
  282. package/eslint-rules/latex-language.mutations.mjs +196 -0
  283. package/eslint-rules/paper-research-question.harness.mjs +146 -0
  284. package/eslint-rules/paper-research-question.mjs +180 -0
  285. package/eslint-rules/paper-research-question.mutations.mjs +127 -0
  286. package/eslint-rules/paper-stages.harness.mjs +356 -0
  287. package/eslint-rules/paper-stages.mjs +455 -0
  288. package/eslint-rules/paper-stages.mutations.mjs +157 -0
  289. package/eslint-rules/paper-typography.harness.mjs +291 -0
  290. package/eslint-rules/paper-typography.mjs +313 -0
  291. package/eslint-rules/paper-typography.mutations.mjs +131 -0
  292. package/eslint-rules/papers.harness.mjs +259 -0
  293. package/eslint-rules/papers.mjs +166 -0
  294. package/eslint-rules/papers.mutations.mjs +186 -0
  295. package/eslint-rules/pdf-last-page-balance.harness.mjs +206 -0
  296. package/eslint-rules/pdf-last-page-balance.mjs +208 -0
  297. package/eslint-rules/review-findings-cause.harness.mjs +228 -0
  298. package/eslint-rules/review-findings-cause.mjs +135 -0
  299. package/eslint-rules/review-findings-cause.mutations.mjs +72 -0
  300. package/eslint-rules/temp-root-realpath.harness.mjs +176 -0
  301. package/eslint-rules/temp-root-realpath.mjs +129 -0
  302. package/eslint-rules/temp-root-realpath.mutations.mjs +99 -0
  303. package/eslint-rules/tex-build.harness.mjs +753 -0
  304. package/eslint-rules/tex-build.mjs +322 -0
  305. package/eslint-rules/tex-build.mutations.mjs +258 -0
  306. package/eslint.config.mjs +521 -0
  307. package/fixtures/build-e2e/acmart/PIPELINE-STATUS.md +3 -0
  308. package/fixtures/build-e2e/acmart/paper.tex +11 -0
  309. package/fixtures/build-e2e/acmart/venue.json +1 -0
  310. package/fixtures/build-e2e/broken/PIPELINE-STATUS.md +3 -0
  311. package/fixtures/build-e2e/broken/paper.tex +7 -0
  312. package/fixtures/build-e2e/cite/PIPELINE-STATUS.md +3 -0
  313. package/fixtures/build-e2e/cite/build.sh +5 -0
  314. package/fixtures/build-e2e/cite/paper.tex +10 -0
  315. package/fixtures/build-e2e/cite/refs.bib +9 -0
  316. package/fixtures/build-e2e/empty/PIPELINE-STATUS.md +3 -0
  317. package/fixtures/build-e2e/empty/paper.tex +6 -0
  318. package/fixtures/build-e2e/fallback/PIPELINE-STATUS.md +3 -0
  319. package/fixtures/build-e2e/fallback/paper.tex +11 -0
  320. package/fixtures/build-e2e/guards/PIPELINE-STATUS.md +3 -0
  321. package/fixtures/build-e2e/guards/paper.tex +10 -0
  322. package/fixtures/build-e2e/no-source/PIPELINE-STATUS.md +3 -0
  323. package/fixtures/build-e2e/unbalanced/PIPELINE-STATUS.md +3 -0
  324. package/fixtures/build-e2e/unbalanced/paper.tex +28 -0
  325. package/fixtures/build-e2e/unbalanced/refs.bib +269 -0
  326. package/fixtures/install-path-literals/clean.fixture.mjs +3 -0
  327. package/fixtures/install-path-literals/clean.md +15 -0
  328. package/fixtures/install-path-literals/defect.fixture.mjs +3 -0
  329. package/fixtures/install-path-literals/defect.md +14 -0
  330. package/fixtures/latex-language/clean.tex +50 -0
  331. package/fixtures/latex-language/defect.tex +52 -0
  332. package/fixtures/paper-research-question/comment-only/PIPELINE-STATUS.md +9 -0
  333. package/fixtures/paper-research-question/comment-only/paper.tex +7 -0
  334. package/fixtures/paper-research-question/declared-not-in-paper/PIPELINE-STATUS.md +10 -0
  335. package/fixtures/paper-research-question/declared-not-in-paper/paper.tex +6 -0
  336. package/fixtures/paper-research-question/draft/PIPELINE-STATUS.md +6 -0
  337. package/fixtures/paper-research-question/draft/paper.tex +2 -0
  338. package/fixtures/paper-research-question/markdown-no-rq/PIPELINE-STATUS.md +9 -0
  339. package/fixtures/paper-research-question/markdown-no-rq/paper.md +4 -0
  340. package/fixtures/paper-research-question/shipped-no-rq/PIPELINE-STATUS.md +12 -0
  341. package/fixtures/paper-research-question/shipped-no-rq/paper.tex +3 -0
  342. package/fixtures/paper-research-question/shipped-with-rq/PIPELINE-STATUS.md +10 -0
  343. package/fixtures/paper-research-question/shipped-with-rq/paper.tex +2 -0
  344. package/fixtures/paper-stages/authors-ran/PIPELINE-STATUS.md +16 -0
  345. package/fixtures/paper-stages/marker-in-prose/PIPELINE-STATUS.md +17 -0
  346. package/fixtures/paper-stages/nofile/PIPELINE-STATUS.md +8 -0
  347. package/fixtures/paper-stages/noheader/PIPELINE-STATUS.md +1 -0
  348. package/fixtures/paper-stages/noheader/versions/2026-07-22-submitted.pdf +0 -0
  349. package/fixtures/paper-stages/nothing/PIPELINE-STATUS.md +3 -0
  350. package/fixtures/paper-stages/ok/PIPELINE-STATUS.md +9 -0
  351. package/fixtures/paper-stages/ok/versions/2026-07-22-submitted.pdf +0 -0
  352. package/fixtures/paper-stages/stale/PIPELINE-STATUS.md +1 -0
  353. package/fixtures/paper-stages/stale/versions/2026-07-22-submitted.STALE-WRONG-FILE.pdf +0 -0
  354. package/fixtures/paper-stages/twice/PIPELINE-STATUS.md +14 -0
  355. package/fixtures/paper-stages/twice/versions/2026-08-06-submitted.pdf +0 -0
  356. package/fixtures/paper-stages/twice/versions/2026-10-24-submitted.pdf +0 -0
  357. package/fixtures/paper-stages/undeclared/PIPELINE-STATUS.md +8 -0
  358. package/fixtures/paper-stages/undeclared/versions/2026-07-22-submitted.pdf +0 -0
  359. package/fixtures/paper-stages/undeclared/versions/2026-08-29-camera-ready.pdf +0 -0
  360. package/fixtures/paper-stages/wrongsize/PIPELINE-STATUS.md +8 -0
  361. package/fixtures/paper-stages/wrongsize/versions/2026-07-22-submitted.pdf +0 -0
  362. package/fixtures/paper-typography/clean-paper/paper.tex +29 -0
  363. package/fixtures/paper-typography/messy-paper/paper.tex +27 -0
  364. package/fixtures/pdf-facts/README.md +22 -0
  365. package/fixtures/pdf-facts/corrupt-font.pdf +0 -0
  366. package/fixtures/pdf-facts/encrypted.pdf +0 -0
  367. package/fixtures/pdf-facts/hidden-text.pdf +0 -0
  368. package/fixtures/pdf-facts/hidden-text.tex +28 -0
  369. package/fixtures/pdf-facts/t3-all.pdf +0 -0
  370. package/fixtures/pdf-facts/t3-all.tex +8 -0
  371. package/fixtures/pdf-facts/t3-mixed.pdf +0 -0
  372. package/fixtures/pdf-facts/t3-mixed.tex +9 -0
  373. package/fixtures/pdf-facts/ttf.pdf +2240 -1
  374. package/fixtures/pdf-facts/ttf.tex +6 -0
  375. package/fixtures/real-markdown-paper/baseline.json +24 -0
  376. package/fixtures/real-markdown-paper/baseline.mjs +48 -0
  377. package/fixtures/render-paper/build-clean.sh +25 -0
  378. package/fixtures/render-paper/build-defect.sh +15 -0
  379. package/fixtures/review-findings-cause/clean.md +17 -0
  380. package/fixtures/review-findings-cause/defect.md +14 -0
  381. package/fixtures/review-findings-cause/old-debt.md +14 -0
  382. package/fixtures/review-findings-cause/quiet-in-fence.md +16 -0
  383. package/fixtures/tex-build/clean.tex +21 -0
  384. package/fixtures/tex-build/defect.tex +24 -0
  385. package/fixtures/tex-build/frontmatter-clean.tex +25 -0
  386. package/fixtures/tex-build/frontmatter-defect.tex +23 -0
  387. package/fixtures/toolchain-mirror/catalog.txt +5 -0
  388. package/fixtures/toolchain-mirror/install-tl +27 -0
  389. package/fixtures/toolchain-mirror/release-texlive.txt +3 -0
  390. package/fixtures/toolchain-mirror/release-year +1 -0
  391. package/fixtures/toolchain-mirror/stub-kpsewhich +8 -0
  392. package/fixtures/toolchain-mirror/stub-pdflatex +3 -0
  393. package/fixtures/toolchain-mirror/stub-tlmgr +44 -0
  394. package/hooks/hooks.harness.mjs +713 -0
  395. package/hooks/hooks.mutations.mjs +337 -0
  396. package/hooks/paper-edit-guard.hook.d.mts +13 -0
  397. package/hooks/paper-edit-guard.hook.mjs +457 -0
  398. package/hooks/paper-skills-nudge.hook.mjs +136 -0
  399. package/hooks/paper-status-gates.hook.mjs +156 -0
  400. package/hooks/paper-status-gates.sh +91 -0
  401. package/lib/agent-cli-version.harness.mjs +165 -0
  402. package/lib/agent-cli-version.mjs +106 -0
  403. package/lib/agent-cli-version.mutations.mjs +109 -0
  404. package/lib/markdown.mjs +386 -0
  405. package/lib/mutation-driver.harness.mjs +227 -0
  406. package/lib/mutation-driver.mjs +397 -0
  407. package/lib/mutation-driver.mutations.mjs +68 -0
  408. package/lib/paper-config.d.mts +34 -0
  409. package/lib/paper-config.harness.mjs +286 -0
  410. package/lib/paper-config.mjs +142 -0
  411. package/lib/paper-config.mutations.mjs +143 -0
  412. package/lib/skill-checks.mjs +701 -0
  413. package/lib/skill-corpus.mjs +403 -0
  414. package/lib/skill-eval-fixture.mjs +63 -0
  415. package/lib/skill-eval-kit.mjs +257 -0
  416. package/lib/skill-trigger-cases.harness.mjs +170 -0
  417. package/lib/skill-trigger-cases.mjs +446 -0
  418. package/lib/skill-trigger-cases.mutations.mjs +65 -0
  419. package/lib/trigger-ledger.mjs +215 -0
  420. package/package.json +97 -0
  421. package/plugin/.claude-plugin/plugin.json +8 -0
  422. package/plugin/hooks/hooks.json +30 -0
  423. package/scripts/check.harness.mjs +177 -0
  424. package/scripts/check.mjs +239 -0
  425. package/scripts/check.mutations.mjs +110 -0
  426. package/scripts/eslint-report-guard.mjs +82 -0
  427. package/scripts/exclusive.mjs +138 -0
  428. package/scripts/harness-api.frozen.json +76 -0
  429. package/scripts/harness-api.test.ts +175 -0
  430. package/scripts/layer-legacy-frozen.d.mts +28 -0
  431. package/scripts/layer-legacy-frozen.mjs +152 -0
  432. package/scripts/layer-legacy-frozen.test.ts +115 -0
  433. package/scripts/layer-legacy.frozen.json +50 -0
  434. package/scripts/mutation-batteries-frozen.harness.mjs +204 -0
  435. package/scripts/mutation-batteries-frozen.mjs +238 -0
  436. package/scripts/mutation-batteries.frozen.json +117 -0
  437. package/scripts/release-config.test.ts +90 -0
  438. package/scripts/rules-are-content-only.harness.mjs +113 -0
  439. package/scripts/rules-are-content-only.mjs +138 -0
  440. package/scripts/rules-are-content-only.mutations.mjs +81 -0
  441. package/scripts/rules-see-files.harness.mjs +115 -0
  442. package/scripts/rules-see-files.mjs +99 -0
  443. package/scripts/rules-see-files.mutations.mjs +131 -0
  444. package/scripts/run-mutations.mjs +100 -0
  445. package/scripts/semantic-release-plugins.d.ts +16 -0
  446. package/skills/README.md +15 -0
  447. package/skills/analyze-sibling-paper/SKILL.md +170 -0
  448. package/skills/analyze-sibling-paper/SKILL.md.spec.ts +186 -0
  449. package/skills/analyze-sibling-paper/analyze-sibling-paper.eval.mjs +19 -0
  450. package/skills/analyze-sibling-paper/analyze-sibling-paper.harness.mjs +23 -0
  451. package/skills/argument-arc/SKILL.md +177 -0
  452. package/skills/argument-arc/SKILL.md.spec.ts +192 -0
  453. package/skills/argument-arc/argument-arc.eval.mjs +19 -0
  454. package/skills/argument-arc/argument-arc.harness.mjs +23 -0
  455. package/skills/build-benchmark/SKILL.md +213 -0
  456. package/skills/build-benchmark/SKILL.md.spec.ts +220 -0
  457. package/skills/build-benchmark/build-benchmark.eval.mjs +19 -0
  458. package/skills/build-benchmark/build-benchmark.harness.mjs +23 -0
  459. package/skills/build-benchmark/references/adversarial-cold-repro.md +68 -0
  460. package/skills/camera-ready/SKILL.md +148 -0
  461. package/skills/camera-ready/SKILL.md.spec.ts +164 -0
  462. package/skills/camera-ready/camera-ready.eval.mjs +19 -0
  463. package/skills/camera-ready/camera-ready.harness.mjs +23 -0
  464. package/skills/cold-read-diff/SKILL.md +160 -0
  465. package/skills/cold-read-diff/SKILL.md.spec.ts +166 -0
  466. package/skills/cold-read-diff/cold-read-diff.eval.mjs +19 -0
  467. package/skills/cold-read-diff/cold-read-diff.harness.mjs +23 -0
  468. package/skills/draft-paper/SKILL.md +152 -0
  469. package/skills/draft-paper/SKILL.md.spec.ts +169 -0
  470. package/skills/draft-paper/draft-paper.eval.mjs +19 -0
  471. package/skills/draft-paper/draft-paper.harness.mjs +23 -0
  472. package/skills/extend-paper/SKILL.md +99 -0
  473. package/skills/extend-paper/SKILL.md.spec.ts +116 -0
  474. package/skills/extend-paper/extend-paper.eval.mjs +19 -0
  475. package/skills/extend-paper/extend-paper.harness.mjs +23 -0
  476. package/skills/find-venue/SKILL.md +128 -0
  477. package/skills/find-venue/SKILL.md.spec.ts +145 -0
  478. package/skills/find-venue/find-venue.eval.mjs +19 -0
  479. package/skills/find-venue/find-venue.harness.mjs +23 -0
  480. package/skills/grade-paper-writing/SKILL.md +436 -0
  481. package/skills/grade-paper-writing/SKILL.md.spec.ts +453 -0
  482. package/skills/grade-paper-writing/fixtures/control_gopen.txt +1 -0
  483. package/skills/grade-paper-writing/fixtures/control_human_paper.txt +1 -0
  484. package/skills/grade-paper-writing/fixtures/rewrite.txt +1 -0
  485. package/skills/grade-paper-writing/fixtures/specimen.txt +1 -0
  486. package/skills/grade-paper-writing/fixtures/structure-checks.md +22 -0
  487. package/skills/grade-paper-writing/grade-paper-writing.eval.mjs +19 -0
  488. package/skills/grade-paper-writing/grade-paper-writing.harness.mjs +23 -0
  489. package/skills/grade-paper-writing/prose-lint.mjs +713 -0
  490. package/skills/harden-paper/SKILL.md +318 -0
  491. package/skills/harden-paper/SKILL.md.spec.ts +336 -0
  492. package/skills/harden-paper/check-numbers.sh +33 -0
  493. package/skills/harden-paper/check-release-claims.sh +35 -0
  494. package/skills/harden-paper/fixtures/uncited-assertions-sample.md +43 -0
  495. package/skills/harden-paper/fixtures/uncited-assertions-sample.tex +77 -0
  496. package/skills/harden-paper/harden-paper.eval.mjs +19 -0
  497. package/skills/harden-paper/harden-paper.harness.mjs +23 -0
  498. package/skills/map-prior-work/SKILL.md +211 -0
  499. package/skills/map-prior-work/SKILL.md.spec.ts +227 -0
  500. package/skills/map-prior-work/map-prior-work.eval.mjs +19 -0
  501. package/skills/map-prior-work/map-prior-work.harness.mjs +23 -0
  502. package/skills/osf-artifact-upload/SKILL.md +52 -0
  503. package/skills/osf-artifact-upload/SKILL.md.spec.ts +59 -0
  504. package/skills/osf-artifact-upload/osf-artifact-upload.eval.mjs +22 -0
  505. package/skills/osf-artifact-upload/osf-artifact-upload.harness.mjs +103 -0
  506. package/skills/paper-adversarial-review/SKILL.md +126 -0
  507. package/skills/paper-adversarial-review/SKILL.md.spec.ts +142 -0
  508. package/skills/paper-adversarial-review/paper-adversarial-review.eval.mjs +19 -0
  509. package/skills/paper-adversarial-review/paper-adversarial-review.harness.mjs +23 -0
  510. package/skills/paper-pipeline/PIPELINE-MAP.md +371 -0
  511. package/skills/paper-pipeline/SKILL.md +499 -0
  512. package/skills/paper-pipeline/SKILL.md.spec.ts +517 -0
  513. package/skills/paper-pipeline/description-language.eval.mjs +347 -0
  514. package/skills/paper-pipeline/framing-vs-vocabulary.eval.mjs +891 -0
  515. package/skills/paper-pipeline/grade-paper-writing-ablation.eval.mjs +1254 -0
  516. package/skills/paper-pipeline/paper-pipeline.eval.mjs +22 -0
  517. package/skills/paper-pipeline/paper-pipeline.harness.mjs +143 -0
  518. package/skills/paper-pipeline/pipeline-firing.baseline.json +270 -0
  519. package/skills/paper-pipeline/pipeline-firing.eval.mjs +664 -0
  520. package/skills/paper-pipeline/pipeline-language.eval.mjs +672 -0
  521. package/skills/paper-pipeline/references/acceptance-gate.md +329 -0
  522. package/skills/paper-pipeline/references/acl-venue-rules.md +142 -0
  523. package/skills/paper-pipeline/references/anonymization.md +68 -0
  524. package/skills/paper-pipeline/references/artifact-checklist.md +93 -0
  525. package/skills/paper-pipeline/references/body-vs-appendix.md +97 -0
  526. package/skills/paper-pipeline/references/credit-criteria.md +69 -0
  527. package/skills/paper-pipeline/references/occupancy-2026-08-06-probe/README.md +35 -0
  528. package/skills/paper-pipeline/references/occupancy-2026-08-06-probe/run_retext.mjs +24 -0
  529. package/skills/paper-pipeline/references/occupancy-2026-08-06-probe/sentences.txt +11 -0
  530. package/skills/paper-pipeline/references/occupancy-2026-08-06-probe/test_sentences.py +25 -0
  531. package/skills/paper-pipeline/references/occupancy-2026-08-06-prose-checkers.md +538 -0
  532. package/skills/paper-pipeline/references/occupancy-2026-08-06-reproducible-tooling.md +431 -0
  533. package/skills/paper-pipeline/references/occupancy-2026-08-06-staleness-and-orchestration.md +592 -0
  534. package/skills/paper-pipeline/references/pipeline-status-template.md +162 -0
  535. package/skills/paper-pipeline/references/review-ratchet.md +36 -0
  536. package/skills/paper-pipeline/references/sweep-2026-08-09-ideal-pipeline.md +585 -0
  537. package/skills/paper-pipeline/references/writing-craft.md +448 -0
  538. package/skills/paper-pipeline/repro/2026-08-07-description-language-control.log +63 -0
  539. package/skills/paper-pipeline/repro/2026-08-07-fork-check.log +52 -0
  540. package/skills/paper-pipeline/repro/2026-08-07-fork-check2.log +33 -0
  541. package/skills/paper-pipeline/repro/2026-08-07-language-eval-pilot.log +33 -0
  542. package/skills/paper-pipeline/repro/2026-08-07-language-eval-raw.log +166 -0
  543. package/skills/paper-pipeline/repro/2026-08-08-framing-vs-vocabulary-oracle.json +338 -0
  544. package/skills/paper-pipeline/repro/2026-08-08-framing-vs-vocabulary-oracle.log +118 -0
  545. package/skills/paper-pipeline/repro/2026-08-08-framing-vs-vocabulary-raw.log +245 -0
  546. package/skills/paper-pipeline/repro/2026-08-08-framing-vs-vocabulary.json +776 -0
  547. package/skills/paper-pipeline/repro/2026-08-08-grade-paper-writing-ablation-A6-oracle.log +53 -0
  548. package/skills/paper-pipeline/repro/2026-08-08-grade-paper-writing-ablation-A6-raw.log +89 -0
  549. package/skills/paper-pipeline/repro/2026-08-08-grade-paper-writing-ablation-oracle.json +450 -0
  550. package/skills/paper-pipeline/repro/2026-08-08-grade-paper-writing-ablation-oracle.log +136 -0
  551. package/skills/paper-pipeline/repro/2026-08-08-grade-paper-writing-ablation-raw.log +242 -0
  552. package/skills/paper-pipeline/repro/2026-08-08-grade-paper-writing-ablation-setupdiff.log +59 -0
  553. package/skills/paper-pipeline/repro/2026-08-08-grade-paper-writing-ablation.json +1032 -0
  554. package/skills/paper-pipeline/repro/2026-08-08-parent-replication-gpw.json +139 -0
  555. package/skills/paper-pipeline/repro/2026-08-08-parent-replication-gpw.log +98 -0
  556. package/skills/paper-pipeline/repro/2026-08-08-parent-replication.mjs +92 -0
  557. package/skills/paper-pipeline/repro/README.md +129 -0
  558. package/skills/paper-pipeline/repro/analyze-language-eval.py +116 -0
  559. package/skills/paper-pipeline/scripts/README.md +344 -0
  560. package/skills/paper-pipeline/scripts/announce.mjs +67 -0
  561. package/skills/paper-pipeline/scripts/artifact-coverage.harness.mjs +496 -0
  562. package/skills/paper-pipeline/scripts/artifact-coverage.mjs +397 -0
  563. package/skills/paper-pipeline/scripts/artifact-coverage.mutations.mjs +218 -0
  564. package/skills/paper-pipeline/scripts/check-provenance.mjs +184 -0
  565. package/skills/paper-pipeline/scripts/consumer.d.mts +32 -0
  566. package/skills/paper-pipeline/scripts/consumer.harness.mjs +562 -0
  567. package/skills/paper-pipeline/scripts/consumer.mjs +535 -0
  568. package/skills/paper-pipeline/scripts/consumer.mutations.mjs +190 -0
  569. package/skills/paper-pipeline/scripts/extract-ref-facts.harness.mjs +457 -0
  570. package/skills/paper-pipeline/scripts/extract-ref-facts.mjs +656 -0
  571. package/skills/paper-pipeline/scripts/extract-ref-facts.mutations.mjs +54 -0
  572. package/skills/paper-pipeline/scripts/fixtures/clean/PIPELINE-STATUS.md +51 -0
  573. package/skills/paper-pipeline/scripts/fixtures/dirty/PIPELINE-STATUS.md +52 -0
  574. package/skills/paper-pipeline/scripts/fixtures/dirty/paper.md +6 -0
  575. package/skills/paper-pipeline/scripts/fixtures/real-bib/refs.bib +153 -0
  576. package/skills/paper-pipeline/scripts/generated-code.harness.mjs +466 -0
  577. package/skills/paper-pipeline/scripts/generated-code.mjs +338 -0
  578. package/skills/paper-pipeline/scripts/generated-code.mutations.mjs +254 -0
  579. package/skills/paper-pipeline/scripts/ledger.mjs +623 -0
  580. package/skills/paper-pipeline/scripts/ledger.selftest.mjs +286 -0
  581. package/skills/paper-pipeline/scripts/pipeline-check.harness.mjs +389 -0
  582. package/skills/paper-pipeline/scripts/pipeline-check.mjs +737 -0
  583. package/skills/paper-pipeline/scripts/pipeline-check.mutations.mjs +54 -0
  584. package/skills/paper-pipeline/scripts/pipeline-edges.mjs +169 -0
  585. package/skills/paper-pipeline/scripts/population-map.harness.mjs +178 -0
  586. package/skills/paper-pipeline/scripts/population-map.mjs +181 -0
  587. package/skills/paper-pipeline/scripts/population-map.mutations.mjs +65 -0
  588. package/skills/paper-pipeline/scripts/population-map.selftest.mjs +122 -0
  589. package/skills/paper-pipeline/scripts/provenance.harness.mjs +240 -0
  590. package/skills/paper-pipeline/scripts/provenance.mutations.mjs +59 -0
  591. package/skills/paper-pipeline/scripts/round-diff.harness.mjs +881 -0
  592. package/skills/paper-pipeline/scripts/round-diff.mjs +576 -0
  593. package/skills/paper-pipeline/scripts/round-diff.mutations.mjs +276 -0
  594. package/skills/paper-pipeline/scripts/run-mechanical.mjs +633 -0
  595. package/skills/paper-pipeline/scripts/status.mjs +295 -0
  596. package/skills/paper-status/SKILL.md +183 -0
  597. package/skills/paper-status/SKILL.md.spec.ts +190 -0
  598. package/skills/paper-status/paper-status.eval.mjs +22 -0
  599. package/skills/paper-status/paper-status.harness.mjs +25 -0
  600. package/skills/pc-panel-review/SKILL.md +263 -0
  601. package/skills/pc-panel-review/SKILL.md.spec.ts +280 -0
  602. package/skills/pc-panel-review/pc-panel-review.eval.mjs +19 -0
  603. package/skills/pc-panel-review/pc-panel-review.harness.mjs +23 -0
  604. package/skills/plan-paper-timeline/SKILL.md +182 -0
  605. package/skills/plan-paper-timeline/SKILL.md.spec.ts +200 -0
  606. package/skills/plan-paper-timeline/fixtures/fake-google-calendar.mjs +239 -0
  607. package/skills/plan-paper-timeline/plan-paper-timeline.effects.harness.mjs +431 -0
  608. package/skills/plan-paper-timeline/plan-paper-timeline.effects.mutations.mjs +65 -0
  609. package/skills/plan-paper-timeline/plan-paper-timeline.eval.mjs +19 -0
  610. package/skills/plan-paper-timeline/plan-paper-timeline.harness.mjs +23 -0
  611. package/skills/render-paper/SKILL.md +159 -0
  612. package/skills/render-paper/SKILL.md.spec.ts +166 -0
  613. package/skills/render-paper/check-render.sh +419 -0
  614. package/skills/render-paper/checkers-requirements.txt +55 -0
  615. package/skills/render-paper/ensure-checkers.sh +69 -0
  616. package/skills/render-paper/extract-pdf-facts.harness.mjs +166 -0
  617. package/skills/render-paper/extract-pdf-facts.mjs +144 -0
  618. package/skills/render-paper/render-paper.eval.mjs +19 -0
  619. package/skills/render-paper/render-paper.harness.mjs +339 -0
  620. package/skills/research-ideate/SKILL.md +136 -0
  621. package/skills/research-ideate/SKILL.md.spec.ts +152 -0
  622. package/skills/research-ideate/research-ideate.eval.mjs +19 -0
  623. package/skills/research-ideate/research-ideate.harness.mjs +23 -0
  624. package/skills/skill-contract.mutations.mjs +179 -0
  625. package/skills/study-accepted-papers/SKILL.md +206 -0
  626. package/skills/study-accepted-papers/SKILL.md.spec.ts +223 -0
  627. package/skills/study-accepted-papers/study-accepted-papers.eval.mjs +19 -0
  628. package/skills/study-accepted-papers/study-accepted-papers.harness.mjs +23 -0
  629. package/skills/submit-paper/SKILL.md +182 -0
  630. package/skills/submit-paper/SKILL.md.spec.ts +199 -0
  631. package/skills/submit-paper/check-deanon.sh +149 -0
  632. package/skills/submit-paper/references/publishers/acm.md +92 -0
  633. package/skills/submit-paper/references/venues/agenticdev.jsonc +108 -0
  634. package/skills/submit-paper/references/venues/agenticdev.md +139 -0
  635. package/skills/submit-paper/references/venues/agenticdev.tex +19 -0
  636. package/skills/submit-paper/references/venues/aisec.jsonc +101 -0
  637. package/skills/submit-paper/references/venues/aisec.md +105 -0
  638. package/skills/submit-paper/references/venues/paper-guards.tex +41 -0
  639. package/skills/submit-paper/references/venues/realm.jsonc +81 -0
  640. package/skills/submit-paper/references/venues/realm.md +155 -0
  641. package/skills/submit-paper/references/venues/tex-base.jsonc +50 -0
  642. package/skills/submit-paper/references/venues/venue-profile.schema.json +74 -0
  643. package/skills/submit-paper/submit-paper.eval.mjs +19 -0
  644. package/skills/submit-paper/submit-paper.harness.mjs +23 -0
  645. package/skills/sweep-design-space/SKILL.md +269 -0
  646. package/skills/sweep-design-space/SKILL.md.spec.ts +285 -0
  647. package/skills/sweep-design-space/sweep-design-space.eval.mjs +19 -0
  648. package/skills/sweep-design-space/sweep-design-space.harness.mjs +23 -0
  649. package/skills/tighten-paper/SKILL.md +368 -0
  650. package/skills/tighten-paper/SKILL.md.spec.ts +384 -0
  651. package/skills/tighten-paper/structure.mjs +371 -0
  652. package/skills/tighten-paper/tighten-paper.eval.mjs +19 -0
  653. package/skills/tighten-paper/tighten-paper.harness.mjs +23 -0
  654. package/skills/verify-citations/SKILL.md +328 -0
  655. package/skills/verify-citations/SKILL.md.spec.ts +345 -0
  656. package/skills/verify-citations/scripts/bib-authors.mjs +479 -0
  657. package/skills/verify-citations/scripts/bib-authors.test.mjs +175 -0
  658. package/skills/verify-citations/scripts/verify-cites.mjs +1108 -0
  659. package/skills/verify-citations/scripts/verify-cites.test.mjs +735 -0
  660. package/skills/verify-citations/verify-citations.eval.mjs +19 -0
  661. package/skills/verify-citations/verify-citations.harness.mjs +23 -0
  662. package/src/CLAUDE.md +51 -0
  663. package/src/action-ref.test.ts +26 -0
  664. package/src/action-ref.ts +15 -0
  665. package/src/adapters/banal/failure.test.ts +63 -0
  666. package/src/adapters/banal/failure.ts +118 -0
  667. package/src/adapters/banal/index.test.ts +119 -0
  668. package/src/adapters/banal/index.ts +100 -0
  669. package/src/adapters/banal/install.test.ts +20 -0
  670. package/src/adapters/banal/install.ts +41 -0
  671. package/src/adapters/banal/invocation.test.ts +74 -0
  672. package/src/adapters/banal/invocation.ts +95 -0
  673. package/src/adapters/banal/locate.test.ts +52 -0
  674. package/src/adapters/banal/locate.ts +84 -0
  675. package/src/adapters/banal/output.test.ts +140 -0
  676. package/src/adapters/banal/output.ts +141 -0
  677. package/src/adapters/banal/pin.ts +30 -0
  678. package/src/adapters/banal/probe.ts +35 -0
  679. package/src/adapters/banal/run.test.ts +191 -0
  680. package/src/adapters/banal/run.ts +244 -0
  681. package/src/adapters/banal/settings.test.ts +31 -0
  682. package/src/adapters/banal/settings.ts +55 -0
  683. package/src/adapters/banal/xml.test.ts +111 -0
  684. package/src/adapters/banal/xml.ts +112 -0
  685. package/src/adapters/curl/download.io.ts +73 -0
  686. package/src/adapters/curl/download.test.ts +55 -0
  687. package/src/adapters/curl/index.ts +5 -0
  688. package/src/adapters/memory/index.ts +131 -0
  689. package/src/adapters/node/files.io.ts +39 -0
  690. package/src/adapters/node/files.test.ts +28 -0
  691. package/src/adapters/node/host.io.ts +15 -0
  692. package/src/adapters/node/index.ts +36 -0
  693. package/src/adapters/node/process.io.ts +49 -0
  694. package/src/adapters/node/process.test.ts +46 -0
  695. package/src/adapters/node/workspace.io.ts +40 -0
  696. package/src/adapters/node/workspace.test.ts +58 -0
  697. package/src/adapters/pdfjs/fill.test.ts +111 -0
  698. package/src/adapters/pdfjs/fill.ts +141 -0
  699. package/src/build-engine.harness.mjs +314 -0
  700. package/src/build-engine.ts +219 -0
  701. package/src/build.harness.mjs +631 -0
  702. package/src/build.mutations.mjs +195 -0
  703. package/src/build.ts +793 -0
  704. package/src/cli.harness.mjs +2007 -0
  705. package/src/cli.mutations.mjs +448 -0
  706. package/src/cli.ts +1189 -0
  707. package/src/doctor.harness.mjs +396 -0
  708. package/src/doctor.mutations.mjs +175 -0
  709. package/src/doctor.ts +356 -0
  710. package/src/domain/geometry.ts +108 -0
  711. package/src/domain/host.ts +23 -0
  712. package/src/domain/page-layout.ts +32 -0
  713. package/src/domain/paths.ts +5 -0
  714. package/src/domain/result.test.ts +26 -0
  715. package/src/domain/result.ts +29 -0
  716. package/src/domain/sha256.test.ts +12 -0
  717. package/src/domain/sha256.ts +21 -0
  718. package/src/domain/text.ts +11 -0
  719. package/src/engine.harness.mjs +252 -0
  720. package/src/engine.ts +176 -0
  721. package/src/exit-code.test.ts +21 -0
  722. package/src/exit-code.ts +38 -0
  723. package/src/facts-file.test.ts +240 -0
  724. package/src/facts-file.ts +241 -0
  725. package/src/hooks-settings.harness.mjs +386 -0
  726. package/src/hooks-settings.mutations.mjs +116 -0
  727. package/src/hooks-settings.ts +434 -0
  728. package/src/init.ts +900 -0
  729. package/src/latex-log.harness.mjs +226 -0
  730. package/src/latex-log.ts +234 -0
  731. package/src/latex-loop.harness.mjs +449 -0
  732. package/src/latex-loop.ts +211 -0
  733. package/src/link-skills.harness.mjs +273 -0
  734. package/src/link-skills.mutations.mjs +136 -0
  735. package/src/link-skills.ts +258 -0
  736. package/src/new-paper.harness.mjs +216 -0
  737. package/src/new-paper.mutations.mjs +79 -0
  738. package/src/new-paper.ts +158 -0
  739. package/src/pdf-facts.harness.mjs +188 -0
  740. package/src/pdf-facts.ts +327 -0
  741. package/src/pdf-geometry.harness.mjs +254 -0
  742. package/src/pdf-geometry.ts +300 -0
  743. package/src/ports/download.ts +10 -0
  744. package/src/ports/files.ts +11 -0
  745. package/src/ports/measure-geometry.ts +8 -0
  746. package/src/ports/process.ts +46 -0
  747. package/src/ports/tool-installer.ts +33 -0
  748. package/src/ports/workspace.ts +20 -0
  749. package/src/rules-config.harness.mjs +114 -0
  750. package/src/rules-config.ts +178 -0
  751. package/src/structure.harness.mjs +179 -0
  752. package/src/structure.mutations.mjs +83 -0
  753. package/src/structure.ts +166 -0
  754. package/src/tex-requirements.harness.mjs +238 -0
  755. package/src/tex-requirements.ts +181 -0
  756. package/src/toolchain.harness.mjs +651 -0
  757. package/src/toolchain.ts +755 -0
  758. package/src/types.ts +106 -0
  759. package/templates/paper/PIPELINE-STATUS.md +72 -0
  760. package/templates/paper/paper.md +4 -0
  761. package/templates/paper/paper.tex +8 -0
  762. package/tsconfig.json +23 -0
package/src/build.ts ADDED
@@ -0,0 +1,793 @@
1
+ /**
2
+ * `paperlint build <paper>` — paperlint compiles the paper ITSELF. A paper-supplied script is never run.
3
+ *
4
+ * 🔴 WHY paperlint OWNS THE BUILD, and why `build.sh` is now IGNORED rather than preferred. Until
5
+ * issue #59 this command looked for `build.sh` / `repro/build-submission.sh` and ran it. Every
6
+ * paper therefore carried its own copy of the same work — find the venue files, set `TEXINPUTS`,
7
+ * run pdflatex and bibtex until the references settle — and the copies drifted: one set
8
+ * `TEXINPUTS`, one did not; one printed the log on failure, one swallowed it; one left the old PDF
9
+ * in place after a failed pass. A convention every author re-implements is a convention nobody
10
+ * has. It lives here once, and a script found in the paper directory is named and ignored, so an
11
+ * author who still has one is not surprised.
12
+ *
13
+ * ── THE SHAPE: an ordered list of STEPS, composed, not configured ────────────
14
+ * Each step says whether it applies to THIS paper and why — decided from FACTS parsed out of the
15
+ * paper (its `\documentclass` and options, the venue named in `venue.json`), never from a config
16
+ * flag. `paperlint build` prints that plan before running anything, and `--dry-run` prints only the
17
+ * plan. Adding a step is one entry in `STEPS`.
18
+ *
19
+ * ── AFTER A GREEN COMPILE, THE PDF IS MEASURED ──────────────────────────────
20
+ * The `measure` step writes `_build/paper.facts.json` — page count, fonts, the last page's columns —
21
+ * through `writeFacts` (`facts-file.ts`), the one writer of that file. It judges nothing; the lint
22
+ * rules read the file. The PDF is read with pdf.js through the `readPdf` port, so a test hands the
23
+ * build any outcome and nothing needs poppler.
24
+ *
25
+ * ── WHAT THE BUILD DOES NOT DO: JUDGE THE LAYOUT ───────────────────────────
26
+ * Until 2026-09-24 a third step searched for a `\balance` position that evened out the last page's
27
+ * columns, rebuilding once per `\bibitem`, and failed the build (deleting the PDF) when none worked.
28
+ * It was removed: only some venues require a balanced last page, every mechanism for producing one
29
+ * is documented by its own authors as unreliable, and a build step has no severity, no suppression
30
+ * and no per-venue switch. Balance is now an optional lint rule over the facts this build writes
31
+ * (`pdf/last-page-balance`, off unless the consumer turns it on).
32
+ *
33
+ * ── WHAT IS PURE AND WHAT IS NOT ─────────────────────────────────────────────
34
+ * The decision of the LaTeX loop is `latex-loop.ts`, reading the log is `latex-log.ts`; both are
35
+ * pure. This file is the shell: it runs processes through the injected `run` (the existing port —
36
+ * `spawnSync` by default) and reads the files a pass left behind. Where rpp's own files live is
37
+ * answered by `consumer.mjs`, the one module allowed to know it (rule 10).
38
+ */
39
+ // eslint-disable-next-line boundaries/dependencies -- legacy I/O, moves behind a port in #76
40
+ import { existsSync, readdirSync, readFileSync, rmSync } from "node:fs";
41
+ // eslint-disable-next-line boundaries/dependencies -- legacy I/O, moves behind a port in #76
42
+ import { spawnSync } from "node:child_process";
43
+ import { createHash } from "node:crypto";
44
+ import { delimiter, join, relative } from "node:path";
45
+ // eslint-disable-next-line boundaries/dependencies -- legacy layer, moves behind a port in #76
46
+ import { getParser } from "@unified-latex/unified-latex-util-parse";
47
+ import { packageVenuesDir } from "../skills/paper-pipeline/scripts/consumer.mjs";
48
+ import {
49
+ declaredVenue,
50
+ factsPath,
51
+ measurePaper,
52
+ writeFactsFile,
53
+ FACTS_DIR,
54
+ FACTS_FILE,
55
+ type FactsDocument,
56
+ } from "./facts-file.ts";
57
+ import { readPdf as pdfjsReader, type PdfReader } from "./pdf-facts.ts";
58
+ // eslint-disable-next-line boundaries/dependencies -- legacy layer, moves behind a port in #76
59
+ import { banalMeasurer, parseBanalSettings } from "./adapters/banal/index.ts";
60
+ // eslint-disable-next-line boundaries/dependencies -- legacy layer, moves behind a port in #76
61
+ import { hostDirs, nodeAdapters, nodeFiles } from "./adapters/node/index.ts";
62
+ import { whyNoGeometry } from "./domain/geometry.ts";
63
+ import type { AbsolutePath } from "./domain/paths.ts";
64
+ import type { Files } from "./ports/files.ts";
65
+ import type { MeasureGeometry } from "./ports/measure-geometry.ts";
66
+ import {
67
+ auxBib,
68
+ bibtexExcerpt,
69
+ errorExcerpt,
70
+ logMarkers,
71
+ unwrapLog,
72
+ } from "./latex-log.ts";
73
+ import {
74
+ nextStep,
75
+ summarize,
76
+ TRACKED,
77
+ type BibInput,
78
+ type Hashes,
79
+ type Observation,
80
+ type Step,
81
+ } from "./latex-loop.ts";
82
+ import type { BuildResult, PlanLine } from "./types.ts";
83
+
84
+ /** A directory counts as a paper by the same markers as `structure.ts` — one shared dictionary. */
85
+ export const PAPER_MARKERS = [
86
+ "PIPELINE-STATUS.md",
87
+ "paper.tex",
88
+ "paper.md",
89
+ "venue.json",
90
+ ];
91
+
92
+ /** The source paperlint compiles, and the job name every output file carries. */
93
+ export const MAIN = "paper.tex";
94
+ export const JOB = "paper";
95
+
96
+ /**
97
+ * Scripts that `paperlint build` USED to run. Their presence is reported and nothing more: running a
98
+ * file from the paper directory is exactly what this command stopped doing.
99
+ */
100
+ export const IGNORED_SCRIPTS = ["build.sh", "repro/build-submission.sh"];
101
+
102
+ /** The pdflatex flags: never stop for input, stop at the first error, and say file:line. */
103
+ export const PDFLATEX_FLAGS = [
104
+ "-interaction=nonstopmode",
105
+ "-halt-on-error",
106
+ "-file-line-error",
107
+ ];
108
+
109
+ /** Everything the steps may know about a paper — parsed from it, not configured. */
110
+ export interface PaperFacts {
111
+ /** `paper.tex` when it exists, else null. */
112
+ readonly main: string | null;
113
+ readonly documentclass: {
114
+ readonly name: string;
115
+ readonly options: readonly string[];
116
+ } | null;
117
+ /** The `venue` field of `venue.json`, or null. */
118
+ readonly venue: string | null;
119
+ /** Paper-supplied build scripts found on disk — reported as ignored. */
120
+ readonly ignoredScripts: readonly string[];
121
+ }
122
+
123
+ /** A process runner with `spawnSync`'s shape — the port the harness replaces. */
124
+ export type Runner = typeof spawnSync;
125
+
126
+ /** What a step's `run` receives. `env` is the environment accumulated by the steps before it. */
127
+ export interface BuildContext {
128
+ readonly paperDir: string;
129
+ readonly env: NodeJS.ProcessEnv;
130
+ readonly run: Runner;
131
+ /** Reads a finished PDF — pdf.js by default; the harness passes a fake. */
132
+ readonly readPdf: PdfReader;
133
+ /** The page-geometry measurer the facts writer uses. */
134
+ readonly measure: MeasureGeometry;
135
+ /** Where the facts file is written. */
136
+ readonly files: Files;
137
+ }
138
+
139
+ export type StepOutcome =
140
+ | {
141
+ readonly ok: true;
142
+ readonly env?: NodeJS.ProcessEnv;
143
+ readonly note?: string;
144
+ }
145
+ | { readonly ok: false; readonly lines: readonly string[] };
146
+
147
+ export interface BuildStep {
148
+ readonly name: string;
149
+ /** A required step REFUSES the build when it does not apply; an optional one is skipped. */
150
+ readonly required: boolean;
151
+ applies(facts: PaperFacts): { yes: boolean; why: string };
152
+ run(ctx: BuildContext): StepOutcome | Promise<StepOutcome>;
153
+ }
154
+
155
+ // ── facts ───────────────────────────────────────────────────────────────────────────────
156
+
157
+ /**
158
+ * unified-latex's own AST types, reached through the parser this package declares. The types
159
+ * package behind it is a transitive dependency, and importing it by name would be depending on it
160
+ * without declaring it.
161
+ */
162
+ type LatexRoot = ReturnType<ReturnType<typeof getParser>["parse"]>;
163
+ type LatexNode = LatexRoot["content"][number];
164
+ type LatexArgument = NonNullable<
165
+ Extract<LatexNode, { type: "macro" }>["args"]
166
+ >[number];
167
+
168
+ type LatexMacro = Extract<LatexNode, { type: "macro" }>;
169
+
170
+ /** `node` is a call of the macro `\name`. */
171
+ const isMacro = (
172
+ node: LatexNode | LatexArgument,
173
+ name: string,
174
+ ): node is LatexMacro => node.type === "macro" && node.content === name;
175
+
176
+ /** The concatenated text of a unified-latex argument node. */
177
+ function argText(arg: LatexArgument | undefined): string {
178
+ return (arg?.content ?? [])
179
+ .map((n) =>
180
+ n.type === "string" ? n.content : n.type === "whitespace" ? " " : "",
181
+ )
182
+ .join("");
183
+ }
184
+
185
+ /** A unified-latex AST, or null when the text does not parse. */
186
+ function parseTex(tex: string): LatexRoot | null {
187
+ try {
188
+ return getParser().parse(tex);
189
+ } catch {
190
+ return null;
191
+ }
192
+ }
193
+
194
+ /**
195
+ * `\documentclass[opts]{name}`, read by the unified-latex parser the lint rules already use —
196
+ * so a `\documentclass` inside a comment is a comment, not a class.
197
+ */
198
+ export function parseDocumentclass(tex: string): PaperFacts["documentclass"] {
199
+ return documentclassOf(parseTex(tex));
200
+ }
201
+
202
+ function documentclassOf(ast: LatexRoot | null): PaperFacts["documentclass"] {
203
+ const node = (ast?.content ?? []).find((n): n is LatexMacro =>
204
+ isMacro(n, "documentclass"),
205
+ );
206
+ if (!node) return null;
207
+ const args = node.args ?? [];
208
+ const name = argText(args.find((a) => a.openMark === "{")).trim();
209
+ if (!name) return null;
210
+ const options = argText(args.find((a) => a.openMark === "["))
211
+ .split(",")
212
+ .map((s) => s.trim())
213
+ .filter(Boolean);
214
+ return { name, options };
215
+ }
216
+
217
+ export function readFacts(paperDir: string): PaperFacts {
218
+ const mainPath = join(paperDir, MAIN);
219
+ const main = existsSync(mainPath) ? MAIN : null;
220
+ const venue = declaredVenue(nodeFiles, paperDir);
221
+ const ast = main ? parseTex(readFileSync(mainPath, "utf8")) : null;
222
+ return {
223
+ main,
224
+ documentclass: documentclassOf(ast),
225
+ venue: venue?.venue ?? null,
226
+ ignoredScripts: IGNORED_SCRIPTS.filter((s) =>
227
+ existsSync(join(paperDir, s)),
228
+ ),
229
+ };
230
+ }
231
+
232
+ // ── step: inputs ────────────────────────────────────────────────────────────────────────
233
+
234
+ /**
235
+ * `TEXINPUTS` with `dirs` in front. With nothing set before, the value ENDS in the separator, and
236
+ * that is load-bearing: an empty element tells kpathsea "and then the system tree", so without it
237
+ * `article.cls` stops resolving. A value the user already set is kept as they wrote it.
238
+ */
239
+ export function withTexInputs(
240
+ env: NodeJS.ProcessEnv,
241
+ dirs: readonly string[],
242
+ ): NodeJS.ProcessEnv {
243
+ return {
244
+ ...env,
245
+ TEXINPUTS: [...dirs, env.TEXINPUTS ?? ""].join(delimiter),
246
+ };
247
+ }
248
+
249
+ export const inputsStep: BuildStep = {
250
+ name: "inputs",
251
+ required: false,
252
+ applies: () => ({
253
+ yes: true,
254
+ why: `TEXINPUTS += ${packageVenuesDir()}`,
255
+ }),
256
+ run: (ctx) => ({
257
+ ok: true,
258
+ env: withTexInputs(ctx.env, [packageVenuesDir()]),
259
+ }),
260
+ };
261
+
262
+ // ── step: compile ───────────────────────────────────────────────────────────────────────
263
+
264
+ const sha = (buf: Buffer): string =>
265
+ createHash("sha256").update(buf).digest("hex");
266
+
267
+ function hashes(paperDir: string): Hashes {
268
+ const h = (ext: string): string | null => {
269
+ const p = join(paperDir, `${JOB}.${ext}`);
270
+ return existsSync(p) ? sha(readFileSync(p)) : null;
271
+ };
272
+ const out: Record<string, string | null> = {};
273
+ for (const t of TRACKED) out[t] = h(t);
274
+ return out as Hashes;
275
+ }
276
+
277
+ const readOr = (path: string, enc: BufferEncoding): string | null =>
278
+ existsSync(path) ? readFileSync(path, enc) : null;
279
+
280
+ /** What bibtex would be run on, as the `.aux` stands now. */
281
+ export function bibInput(paperDir: string): BibInput {
282
+ const aux = readOr(join(paperDir, `${JOB}.aux`), "utf8");
283
+ if (aux === null) return { kind: "none" };
284
+ const bib = auxBib(aux, (name) => readOr(join(paperDir, name), "utf8"));
285
+ if (bib.databases.length === 0) return { kind: "none" };
286
+ const files = bib.databases
287
+ .map((d) => join(paperDir, d.endsWith(".bib") ? d : `${d}.bib`))
288
+ .filter((p) => existsSync(p));
289
+ return {
290
+ kind: "needed",
291
+ citations: bib.citations,
292
+ databases: bib.databases,
293
+ style: bib.style,
294
+ bibHash: files.length
295
+ ? sha(Buffer.concat(files.map((p) => readFileSync(p))))
296
+ : null,
297
+ };
298
+ }
299
+
300
+ /** pdflatex arguments for a pass. The final pass defines `\finalpass` — see `latex-loop.ts`. */
301
+ export function pdflatexArgs(final: boolean): string[] {
302
+ return final
303
+ ? [
304
+ ...PDFLATEX_FLAGS,
305
+ `-jobname=${JOB}`,
306
+ `\\def\\finalpass{}\\input{${MAIN}}`,
307
+ ]
308
+ : [...PDFLATEX_FLAGS, MAIN];
309
+ }
310
+
311
+ const notInstalled = (bin: string): string[] => [
312
+ `${bin} could not be started — it is not installed, or not on PATH.`,
313
+ `paperlint compiles with TeX Live's pdflatex and bibtex: see docs/toolchain.md.`,
314
+ ];
315
+
316
+ /** The log was read byte for byte to count TeX's columns; show it to a human as UTF-8. */
317
+ const fromLatin1 = (s: string): string =>
318
+ Buffer.from(s, "latin1").toString("utf8");
319
+
320
+ type Terminal = Extract<Step, { kind: "done" } | { kind: "fail" }>;
321
+
322
+ /** How every pass is spawned: in the paper directory, output read byte for byte (`fromLatin1`). */
323
+ function spawnOptions(ctx: BuildContext) {
324
+ return {
325
+ cwd: ctx.paperDir,
326
+ env: ctx.env,
327
+ stdio: ["ignore", "pipe", "pipe"] as ["ignore", "pipe", "pipe"],
328
+ encoding: "latin1" as const,
329
+ maxBuffer: 64 * 1024 * 1024,
330
+ };
331
+ }
332
+
333
+ type SpawnOptions = ReturnType<typeof spawnOptions>;
334
+
335
+ const cannotStart = (step: "latex" | "bibtex"): Terminal => ({
336
+ kind: "fail",
337
+ step,
338
+ cause: { kind: "exit", code: 127 },
339
+ lines: notInstalled(step === "latex" ? "pdflatex" : "bibtex"),
340
+ });
341
+
342
+ /** One pdflatex pass and what it left behind, or null when pdflatex could not be started. */
343
+ function latexPass(
344
+ ctx: BuildContext,
345
+ final: boolean,
346
+ opts: SpawnOptions,
347
+ ): Observation | null {
348
+ const before = hashes(ctx.paperDir);
349
+ const r = ctx.run("pdflatex", pdflatexArgs(final), opts);
350
+ if (r.error) return null;
351
+ const exitCode = r.status ?? 1;
352
+ const log = unwrapLog(
353
+ readOr(join(ctx.paperDir, `${JOB}.log`), "latin1") ?? "",
354
+ );
355
+ return {
356
+ step: "latex",
357
+ final,
358
+ exitCode,
359
+ before,
360
+ after: hashes(ctx.paperDir),
361
+ markers: logMarkers(log),
362
+ bib: bibInput(ctx.paperDir),
363
+ errorLines: exitCode === 0 ? [] : errorExcerpt(log).map(fromLatin1),
364
+ };
365
+ }
366
+
367
+ /** One bibtex run, on the input the `.aux` names BEFORE it runs, or null when it could not start. */
368
+ function bibtexPass(ctx: BuildContext, opts: SpawnOptions): Observation | null {
369
+ const before = hashes(ctx.paperDir);
370
+ const bib = bibInput(ctx.paperDir);
371
+ const r = ctx.run("bibtex", [JOB], opts);
372
+ if (r.error) return null;
373
+ const exitCode = r.status ?? 1;
374
+ return {
375
+ step: "bibtex",
376
+ exitCode,
377
+ before,
378
+ after: hashes(ctx.paperDir),
379
+ bib,
380
+ errorLines:
381
+ exitCode === 0 ? [] : bibtexExcerpt(fromLatin1(String(r.stdout ?? ""))),
382
+ };
383
+ }
384
+
385
+ /** Run the loop until `nextStep` says done or fail. */
386
+ export function compile(ctx: BuildContext): {
387
+ end: Terminal;
388
+ latex: number;
389
+ bibtex: number;
390
+ } {
391
+ const history: Observation[] = [];
392
+ const opts = spawnOptions(ctx);
393
+ const runs = { latex: 0, bibtex: 0 };
394
+ for (;;) {
395
+ const step = nextStep(summarize(history));
396
+ if (step.kind === "done" || step.kind === "fail")
397
+ return { end: step, ...runs };
398
+ runs[step.kind]++;
399
+ const seen =
400
+ step.kind === "latex"
401
+ ? latexPass(ctx, step.final, opts)
402
+ : bibtexPass(ctx, opts);
403
+ if (seen === null) return { end: cannotStart(step.kind), ...runs };
404
+ history.push(seen);
405
+ }
406
+ }
407
+
408
+ const NO_PDF = `pdflatex exited 0 but wrote no ${JOB}.pdf — does the document have any pages?`;
409
+
410
+ const plural = (n: number, one: string, many: string): string =>
411
+ `${n} ${n === 1 ? one : many}`;
412
+
413
+ export const compileStep: BuildStep = {
414
+ name: "compile",
415
+ required: true,
416
+ applies: (facts) => {
417
+ if (!facts.main)
418
+ return {
419
+ yes: false,
420
+ why: `no ${MAIN}; paperlint compiles LaTeX, and this paper has none`,
421
+ };
422
+ const dc = facts.documentclass;
423
+ const cls = dc
424
+ ? `\\documentclass${dc.options.length ? `[${dc.options.join(",")}]` : ""}{${dc.name}}`
425
+ : "no \\documentclass found";
426
+ const venue = facts.venue ? `, venue ${facts.venue}` : "";
427
+ return { yes: true, why: `${facts.main} (${cls}${venue})` };
428
+ },
429
+ run: (ctx) => {
430
+ const { end, latex, bibtex } = compile(ctx);
431
+ if (end.kind === "fail") {
432
+ const bin = end.step === "latex" ? "pdflatex" : "bibtex";
433
+ const where =
434
+ end.cause.kind === "exit"
435
+ ? `${bin} exited with ${end.cause.code}`
436
+ : `${bin} did not converge`;
437
+ const log = join(
438
+ ctx.paperDir,
439
+ `${JOB}.${end.step === "latex" ? "log" : "blg"}`,
440
+ );
441
+ return {
442
+ ok: false,
443
+ lines: [
444
+ where,
445
+ ...(end.lines.length
446
+ ? end.lines
447
+ : ["(no error line found in the log)"]),
448
+ `full log: ${log}`,
449
+ ],
450
+ };
451
+ }
452
+ // 🔴 EXIT 0 IS NOT A PDF. pdflatex on a document with no pages prints "No pages of output."
453
+ // and exits 0. Existence is enough to know THIS run wrote it: `buildPapers` deleted paper.pdf
454
+ // before anything ran, so no timestamp comparison is needed. Checked here, in the step that
455
+ // wrote it, so a later step that reads the PDF never sees this case as its own failure.
456
+ if (!existsSync(join(ctx.paperDir, `${JOB}.pdf`)))
457
+ return {
458
+ ok: false,
459
+ lines: [NO_PDF, `full log: ${join(ctx.paperDir, `${JOB}.log`)}`],
460
+ };
461
+ const warn = end.warnings.length
462
+ ? ` — ⚠️ the final log still reports ${end.warnings.join(", ")}`
463
+ : "";
464
+ return {
465
+ ok: true,
466
+ note: `${plural(latex, "pdflatex pass", "pdflatex passes")}, ${plural(bibtex, "bibtex run", "bibtex runs")}${warn}`,
467
+ };
468
+ },
469
+ };
470
+
471
+ // ── step: measure ───────────────────────────────────────────────────────────────────────
472
+
473
+ const columnsNote = (f: FactsDocument): string => {
474
+ const p = f.last_page;
475
+ if (p.kind === "measured")
476
+ return `last page ${p.columns_pt[0].toFixed(1)} / ${p.columns_pt[1].toFixed(1)} pt`;
477
+ return p.kind === "review"
478
+ ? "last page has numbered lines (a review build), not measured"
479
+ : `last page has ${plural(p.words, "word", "words")}, too few to measure`;
480
+ };
481
+
482
+ /**
483
+ * Measure the PDF the compile step wrote and write `_build/paper.facts.json`. A PDF pdf.js cannot
484
+ * read fails the build: a PDF nothing can measure is not one to hand in. banal (page geometry) is
485
+ * optional here: without it the geometry fields are null and the build still succeeds — but the
486
+ * note SAYS so, with the command that installs it (`paperlint toolchain`), because null geometry means
487
+ * the page-size, column and font-size rules have nothing to judge. A banal that fails is named too.
488
+ */
489
+ export const measureStep: BuildStep = {
490
+ name: "measure",
491
+ required: false,
492
+ applies: (facts) =>
493
+ facts.main
494
+ ? {
495
+ yes: true,
496
+ why: `pdf.js → ${FACTS_DIR}/${FACTS_FILE} (facts for the lint rules; nothing is judged here)`,
497
+ }
498
+ : { yes: false, why: "nothing is compiled" },
499
+ run: async (ctx) => {
500
+ const m = await measurePaper(
501
+ ctx.paperDir,
502
+ join(ctx.paperDir, `${JOB}.pdf`),
503
+ {
504
+ readPdf: ctx.readPdf,
505
+ measure: ctx.measure,
506
+ files: ctx.files,
507
+ },
508
+ );
509
+ if (!m.ok) return { ok: false, lines: [m.error] };
510
+ writeFactsFile(ctx.files, ctx.paperDir, m.value.facts);
511
+ const g = m.value.geometry;
512
+ const banal =
513
+ g.kind === "unmeasured"
514
+ ? `; page geometry not measured — ${whyNoGeometry(g)}`
515
+ : "";
516
+ return {
517
+ ok: true,
518
+ note: `facts: ${relative(ctx.paperDir, factsPath(ctx.paperDir))}, ${columnsNote(m.value.facts)}${banal}`,
519
+ };
520
+ },
521
+ };
522
+
523
+ /** The build, in order. A new step is one entry here. */
524
+ export const STEPS: readonly BuildStep[] = [
525
+ inputsStep,
526
+ compileStep,
527
+ measureStep,
528
+ ];
529
+
530
+ // ── the command ─────────────────────────────────────────────────────────────────────────
531
+
532
+ export function planFor(
533
+ facts: PaperFacts,
534
+ steps: readonly BuildStep[] = STEPS,
535
+ ): PlanLine[] {
536
+ return steps.map((s) => {
537
+ const a = s.applies(facts);
538
+ return { step: s.name, applies: a.yes, required: s.required, why: a.why };
539
+ });
540
+ }
541
+
542
+ export function formatPlan(plan: readonly PlanLine[]): string[] {
543
+ const state = (p: PlanLine): string =>
544
+ p.applies ? "" : p.required ? "refused — " : "skipped — ";
545
+ return plan.map((p) => ` ${p.step}: ${state(p)}${p.why}`);
546
+ }
547
+
548
+ /**
549
+ * 🔴 A FAILED BUILD LEAVES NO PDF. An old paper.pdf beside a red build looks current, and "the PDF
550
+ * is there" is exactly what a human checks first. Returns whether there was one.
551
+ */
552
+ function removePdf(paperDir: string): boolean {
553
+ const pdf = join(paperDir, `${JOB}.pdf`);
554
+ const was = existsSync(pdf);
555
+ rmSync(pdf, { force: true });
556
+ return was;
557
+ }
558
+
559
+ /** The one wording for "the PDF is gone", on every path that ends without a new one. */
560
+ export const PDF_REMOVED = `${JOB}.pdf removed — a stale PDF must not pass for this build`;
561
+
562
+ export interface BuildOptions {
563
+ run?: Runner;
564
+ cwd?: string;
565
+ env?: NodeJS.ProcessEnv;
566
+ steps?: readonly BuildStep[];
567
+ log?: (line: string) => void;
568
+ dryRun?: boolean;
569
+ readPdf?: PdfReader;
570
+ /** Where a project's `vendor/banal` is looked for. Default: `$CLAUDE_PROJECT_DIR`, else `cwd`. */
571
+ projectRoot?: string;
572
+ /** The page-geometry measurer. Default: banal, as `env` and `projectRoot` configure it. */
573
+ measure?: MeasureGeometry;
574
+ /** Where the facts file is written. Default: the disk. */
575
+ files?: Files;
576
+ }
577
+
578
+ /**
579
+ * The options with their defaults. Destructuring defaults and not a spread over a defaults object:
580
+ * an option passed as `undefined` (the CLI passes `dryRun: a.dryRun`) must still get its default.
581
+ */
582
+ function baseDefaults({
583
+ run = spawnSync,
584
+ // eslint-disable-next-line no-restricted-globals -- legacy I/O, moves behind a port in #76
585
+ cwd = process.cwd(),
586
+ // eslint-disable-next-line no-restricted-globals -- legacy I/O, moves behind a port in #76
587
+ env = process.env,
588
+ steps = STEPS,
589
+ log = console.log,
590
+ // 🔴 `--dry-run` must be a DECLARED option here, not only in the CLI: options destructuring
591
+ // swallows an unknown key silently, and the first version of this flag ran a full build and
592
+ // rewrote paper.pdf while looking like a verbose dry run.
593
+ dryRun = false,
594
+ readPdf = pdfjsReader,
595
+ projectRoot = env["CLAUDE_PROJECT_DIR"] || cwd,
596
+ }: BuildOptions): Required<Omit<BuildOptions, "measure" | "files">> {
597
+ return { run, cwd, env, steps, log, dryRun, readPdf, projectRoot };
598
+ }
599
+
600
+ /** banal as the measurer, wired from the build's environment: the one piece of root work left here (#76). */
601
+ function defaultMeasurer(
602
+ b: Required<Omit<BuildOptions, "measure" | "files">>,
603
+ ): MeasureGeometry {
604
+ const dirs = hostDirs({ cwd: b.cwd });
605
+ return banalMeasurer(
606
+ nodeAdapters({ tmpDir: dirs.tmp }),
607
+ parseBanalSettings(b.env, dirs),
608
+ b.projectRoot as AbsolutePath,
609
+ );
610
+ }
611
+
612
+ /** `baseDefaults`, plus the measurer and the files — the real ones, from the environment, unless passed. */
613
+ function withDefaults(o: BuildOptions): Required<BuildOptions> {
614
+ const base = baseDefaults(o);
615
+ const measure = o.measure ?? defaultMeasurer(base);
616
+ return { ...base, measure, files: o.files ?? nodeFiles };
617
+ }
618
+
619
+ /** Run the applicable steps in order, each on the environment the steps before it left. */
620
+ async function runSteps(
621
+ paperDir: string,
622
+ dir: string,
623
+ plan: PlanLine[],
624
+ { run, env, steps, readPdf, measure, files }: Required<BuildOptions>,
625
+ ): Promise<BuildResult> {
626
+ let stepEnv = env;
627
+ const notes: string[] = [];
628
+ for (const [i, step] of steps.entries()) {
629
+ if (!plan[i]?.applies) continue;
630
+ const out = await step.run({
631
+ paperDir,
632
+ env: stepEnv,
633
+ run,
634
+ readPdf,
635
+ measure,
636
+ files,
637
+ });
638
+ if (!out.ok) {
639
+ // The PDF THIS run wrote and the step then rejected (a partial pass).
640
+ // A PDF from an earlier run is already gone — `buildPapers` removed it before anything ran.
641
+ removePdf(paperDir);
642
+ return {
643
+ dir,
644
+ status: "failed",
645
+ plan,
646
+ failure: { step: step.name, lines: [...out.lines] },
647
+ };
648
+ }
649
+ if (out.env) stepEnv = out.env;
650
+ if (out.note) notes.push(out.note);
651
+ }
652
+ return { dir, status: "built", plan, ...(notes.length ? { notes } : {}) };
653
+ }
654
+
655
+ export async function buildPaper(
656
+ paperDir: string,
657
+ options: BuildOptions = {},
658
+ ): Promise<BuildResult> {
659
+ const o = withDefaults(options);
660
+ const { cwd, steps, log, dryRun } = o;
661
+ const dir = relative(cwd, paperDir) || paperDir;
662
+ log(dir);
663
+ let facts: PaperFacts;
664
+ try {
665
+ facts = readFacts(paperDir);
666
+ } catch (e) {
667
+ return {
668
+ dir,
669
+ status: "failed",
670
+ plan: [],
671
+ failure: { step: "facts", lines: [(e as Error).message] },
672
+ };
673
+ }
674
+ for (const s of facts.ignoredScripts)
675
+ log(` note: ${s} is ignored — paperlint builds the paper itself`);
676
+ const plan = planFor(facts, steps);
677
+ for (const line of formatPlan(plan)) log(line);
678
+
679
+ if (plan.some((p) => !p.applies && p.required))
680
+ return { dir, status: "no-source", plan };
681
+ if (dryRun) return { dir, status: "built", plan, dry: true };
682
+ return runSteps(paperDir, dir, plan, o);
683
+ }
684
+
685
+ /** A command's run over its targets: stopped before any paper for want of an engine, or ran. */
686
+ export type BuildRun =
687
+ | { readonly kind: "no-engine" }
688
+ | { readonly kind: "ran"; readonly results: readonly BuildResult[] };
689
+
690
+ /**
691
+ * `paperlint build` over its targets — the ONE path every outcome goes through.
692
+ *
693
+ * 🔴 A STALE PDF IS REMOVED HERE, FIRST, AND NOWHERE ELSE. Before the engine is resolved and before
694
+ * any step, every targeted paper's paper.pdf goes. Then no outcome can leave an old PDF looking
695
+ * current, including the ones that never reach a step: no qualifying TeX Live, no paper.tex, facts
696
+ * that do not parse. Removing it per failure path instead is how three of those paths once kept it.
697
+ * `--dry-run` removes nothing: a plan has no side effects.
698
+ */
699
+ export async function buildPapers(
700
+ targets: readonly string[],
701
+ {
702
+ engine,
703
+ ...options
704
+ }: BuildOptions & { engine: () => Promise<NodeJS.ProcessEnv | null> },
705
+ ): Promise<BuildRun> {
706
+ const { cwd, log, dryRun } = withDefaults(options);
707
+ const stale = new Set(dryRun ? [] : targets.filter(removePdf));
708
+ const env = await engine();
709
+ if (env === null) {
710
+ for (const t of stale) log(`${relative(cwd, t) || t}: ${PDF_REMOVED}`);
711
+ return { kind: "no-engine" };
712
+ }
713
+ const results: BuildResult[] = [];
714
+ for (const t of targets) {
715
+ const r = {
716
+ ...(await buildPaper(t, { ...options, env })),
717
+ staleRemoved: stale.has(t),
718
+ };
719
+ log(formatResult(r));
720
+ results.push(r);
721
+ }
722
+ return { kind: "ran", results };
723
+ }
724
+
725
+ /** Immediate subdirectories that look like a paper. Hidden ones are not papers. */
726
+ export function papersIn(
727
+ root: string,
728
+ markers: readonly string[] = PAPER_MARKERS,
729
+ ): string[] {
730
+ try {
731
+ return readdirSync(root, { withFileTypes: true })
732
+ .filter((e) => e.isDirectory() && !e.name.startsWith("."))
733
+ .map((e) => join(root, e.name))
734
+ .filter((d) => markers.some((m) => existsSync(join(d, m))));
735
+ } catch {
736
+ return [];
737
+ }
738
+ }
739
+
740
+ /**
741
+ * The verdict for one paper, printed under its plan.
742
+ *
743
+ * ✗ compile: pdflatex exited with 1
744
+ * ./paper.tex:4: Undefined control sequence.
745
+ * l.4 \foo
746
+ * bar baz
747
+ * full log: …/paper.log
748
+ * paper.pdf removed — a stale PDF must not pass for this build
749
+ */
750
+ export function formatResult(r: BuildResult): string {
751
+ if (r.status === "built")
752
+ return r.dry
753
+ ? ` – not run (--dry-run)`
754
+ : ` ✓ ${JOB}.pdf${r.notes?.length ? ` — ${r.notes.join("; ")}` : ""}`;
755
+ return r.status === "failed" ? formatFailure(r) : formatRefusal(r);
756
+ }
757
+
758
+ /** Always says the PDF is gone: this run may have written one and rejected it. */
759
+ function formatFailure(r: BuildResult): string {
760
+ const [head, ...rest] = r.failure?.lines ?? ["failed"];
761
+ return [
762
+ ` ✗ ${r.failure?.step ?? "build"}: ${head}`,
763
+ ...rest.map((l) => ` ${l}`),
764
+ ` ${PDF_REMOVED}`,
765
+ ].join("\n");
766
+ }
767
+
768
+ /** Says the PDF is gone only when there was one — a paper with no paper.tex usually has none. */
769
+ function formatRefusal(r: BuildResult): string {
770
+ const refused = ` ✗ nothing to compile: no ${MAIN}`;
771
+ return r.staleRemoved ? `${refused}\n ${PDF_REMOVED}` : refused;
772
+ }
773
+
774
+ export function formatResults(results: readonly BuildResult[]): string {
775
+ return results.map(formatResult).join("\n");
776
+ }
777
+
778
+ /**
779
+ * 🔴 "Nothing to compile" COUNTS AS A REFUSAL on a par with a failed build. Failing to tell these
780
+ * two apart is what once produced a green run over a paper no job had built.
781
+ */
782
+ export const anyFailed = (results: readonly BuildResult[]): boolean =>
783
+ results.some((r) => r.status !== "built");
784
+
785
+ export function remedyFor(results: readonly BuildResult[]): string {
786
+ const missing = results.filter((r) => r.status === "no-source");
787
+ if (missing.length === 0) return "";
788
+ return (
789
+ `\nNo ${MAIN} in: ${missing.map((r) => r.dir).join(", ")}.\n` +
790
+ `This is NOT "nothing to build" — paperlint compiles LaTeX, and these papers have no LaTeX source.\n` +
791
+ `Write the paper in ${MAIN}; \`paperlint new <name>\` creates one.`
792
+ );
793
+ }