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
@@ -0,0 +1,449 @@
1
+ /**
2
+ * `latex-loop.ts` — three tables, in order:
3
+ * 1. `nextStep` over a `State`: one row per question, plus the cap;
4
+ * 2. `summarize` over histories: one row per field;
5
+ * 3. the two composed, over the histories the loop really sees — the behaviour the build has.
6
+ * A defect in `nextStep` fails in table 1, one in `summarize` in table 2, before table 3 runs.
7
+ *
8
+ * The decision is pure, so none of this needs TeX. The real-pdflatex half lives in
9
+ * `test/e2e/build.mjs`.
10
+ */
11
+ import assert from "node:assert/strict";
12
+ import { dirname, join } from "node:path";
13
+ import { fileURLToPath } from "node:url";
14
+
15
+ const HERE = dirname(fileURLToPath(import.meta.url));
16
+ const { nextStep, summarize, changedFiles, MAX_PASSES } = await import(
17
+ join(HERE, "latex-loop.ts")
18
+ );
19
+
20
+ let n = 0;
21
+ const check = (label, cond) => {
22
+ assert.ok(cond, label);
23
+ n++;
24
+ };
25
+
26
+ // ── builders ────────────────────────────────────────────────────────────────────────────
27
+ const H = (aux, toc = null, out = null, bbl = null) => ({ aux, toc, out, bbl });
28
+ const NO_BIB = { kind: "none" };
29
+ const bib = (citations, bibHash = "b1") => ({
30
+ kind: "needed",
31
+ citations,
32
+ databases: ["refs"],
33
+ style: "plain",
34
+ bibHash,
35
+ });
36
+ const latex = (o = {}) => ({
37
+ step: "latex",
38
+ final: false,
39
+ exitCode: 0,
40
+ before: H("a1"),
41
+ after: H("a1"),
42
+ markers: [],
43
+ bib: NO_BIB,
44
+ errorLines: [],
45
+ ...o,
46
+ });
47
+ const bibtex = (o = {}) => ({
48
+ step: "bibtex",
49
+ exitCode: 0,
50
+ before: H("a1"),
51
+ after: H("a1", null, null, "bbl1"),
52
+ bib: bib(["k"]),
53
+ errorLines: [],
54
+ ...o,
55
+ });
56
+ const kind = (s) =>
57
+ s.kind === "latex" ? `latex${s.final ? "-final" : ""}` : s.kind;
58
+
59
+ // ── 1. nextStep over a State ─────────────────────────────────────────────────────────────
60
+ const S = (o = {}) => ({
61
+ failed: null,
62
+ bibOutdated: false,
63
+ unsettled: [],
64
+ latexPasses: 1,
65
+ finalDone: false,
66
+ warnings: [],
67
+ ...o,
68
+ });
69
+ const exitFail = {
70
+ step: "bibtex",
71
+ cause: { kind: "exit", code: 2 },
72
+ lines: ["I couldn't open database file nope.bib"],
73
+ };
74
+ const STATES = [
75
+ // Guards: the only moment paper-guards can speak — its error comes on the final pass, so testing
76
+ // 'final' before 'exit code' would ship the undefined reference green.
77
+ [
78
+ "Q1 the last program failed — fail, whatever else holds",
79
+ S({
80
+ failed: exitFail,
81
+ bibOutdated: true,
82
+ unsettled: ["paper.aux"],
83
+ finalDone: true,
84
+ }),
85
+ "fail",
86
+ ],
87
+ ["the final pass ran clean — done", S({ finalDone: true }), "done"],
88
+ // Guards: running bibtex at all — the first pass writes the \citation list; without this question
89
+ // the loop reruns pdflatex, never produces a .bbl, and every \cite stays undefined.
90
+ [
91
+ "Q2 the bibliography input moved since bibtex ran — bibtex, before any rerun",
92
+ S({ bibOutdated: true, unsettled: ["paper.aux"] }),
93
+ "bibtex",
94
+ ],
95
+ [
96
+ "Q3 a tracked file moved or the log asked — another plain pass",
97
+ S({ unsettled: ["paper.aux"] }),
98
+ "latex",
99
+ ],
100
+ // Guards: the stop on a document that never settles — without it the loop runs forever; with a
101
+ // silent cap it ships a PDF with stale cross-references.
102
+ [
103
+ `Q3 cap: ${MAX_PASSES} passes and still unsettled — fail`,
104
+ S({ unsettled: ["paper.aux"], latexPasses: MAX_PASSES }),
105
+ "fail",
106
+ ],
107
+ // Guards: the \finalpass run that arms paper-guards.tex — skipping it saves one pdflatex run and
108
+ // turns every undefined \ref and \cite back into a green build.
109
+ [
110
+ "Q4 nothing unsettled — the FINAL pass (even at the cap)",
111
+ S({ latexPasses: MAX_PASSES }),
112
+ "latex-final",
113
+ ],
114
+ ];
115
+ for (const [label, state, expected] of STATES) {
116
+ const got = kind(nextStep(state));
117
+ check(`${label} (expected ${expected}, got ${got})`, got === expected);
118
+ }
119
+ const q1 = nextStep(S({ failed: exitFail }));
120
+ check(
121
+ "Q1 passes the failure through: the step, the cause and the lines verbatim",
122
+ q1.step === "bibtex" && q1.cause.code === 2 && q1.lines === exitFail.lines,
123
+ );
124
+ const q3cap = nextStep(
125
+ S({ unsettled: ["paper.aux", "labels-changed"], latexPasses: MAX_PASSES }),
126
+ );
127
+ check(
128
+ "the cap's failure is no-convergence and NAMES what stayed unsettled",
129
+ q3cap.cause.kind === "no-convergence" &&
130
+ q3cap.lines[0].includes("paper.aux, labels-changed"),
131
+ );
132
+ check(
133
+ "done carries the state's warnings",
134
+ nextStep(S({ finalDone: true, warnings: ["undefined-citations"] }))
135
+ .warnings[0] === "undefined-citations",
136
+ );
137
+
138
+ // ── 2. summarize over histories ─────────────────────────────────────────────────────────
139
+ const sum = summarize;
140
+ check(
141
+ "summarize: nothing ran — paper.aux is unsettled (no pass has written it), nothing else holds",
142
+ JSON.stringify(sum([])) ===
143
+ JSON.stringify({
144
+ failed: null,
145
+ bibOutdated: false,
146
+ unsettled: ["paper.aux"],
147
+ latexPasses: 0,
148
+ finalDone: false,
149
+ warnings: [],
150
+ }),
151
+ );
152
+ // Guards: failing on a failed pass — -halt-on-error stops pdflatex at the first error, and a loop
153
+ // reading the half-written aux as progress would rerun it or call it converged.
154
+ check(
155
+ "summarize: a non-zero exit is `failed`, naming the step, the code and the log excerpt",
156
+ (() => {
157
+ const f = sum([latex({ exitCode: 1, errorLines: ["l.4 \\foo"] })]).failed;
158
+ return (
159
+ f?.step === "latex" && f.cause.code === 1 && f.lines[0] === "l.4 \\foo"
160
+ );
161
+ })(),
162
+ );
163
+ check(
164
+ "summarize: a FINAL pass that exited non-zero is failed, not finalDone-and-clean",
165
+ sum([latex({ final: true, exitCode: 1 })]).failed !== null,
166
+ );
167
+ check(
168
+ "summarize: bibOutdated when the aux names a bibliography bibtex has not run on",
169
+ sum([latex({ bib: bib(["k"]) })]).bibOutdated === true,
170
+ );
171
+ check(
172
+ "summarize: bibOutdated is false once bibtex ran on exactly that input",
173
+ sum([latex({ bib: bib(["k"]) }), bibtex()]).bibOutdated === false,
174
+ );
175
+ // Guards: the .bib hash as an input — a corrected author or year in refs.bib would keep the old
176
+ // .bbl, and the PDF would print the entry as it was before the fix.
177
+ check(
178
+ "summarize: the .bib content changed since bibtex ran — bibOutdated",
179
+ sum([latex({ bib: bib(["k"]) }), bibtex(), latex({ bib: bib(["k"], "b2") })])
180
+ .bibOutdated === true,
181
+ );
182
+ check(
183
+ "summarize: no bibliography at all — never bibOutdated",
184
+ sum([latex()]).bibOutdated === false,
185
+ );
186
+ check(
187
+ "summarize: unsettled names every tracked file the last pass changed",
188
+ JSON.stringify(
189
+ sum([latex({ before: H("a1", "t1"), after: H("a2", "t2") })]).unsettled,
190
+ ) === JSON.stringify(["paper.aux", "paper.toc"]),
191
+ );
192
+ // Guards: the log's own request for another pass — a package that keeps state outside the tracked
193
+ // files (longtable widths, hyperref outlines) speaks only through this line.
194
+ check(
195
+ "summarize: marker: Rerun to get — unsettled names the rerun marker even with identical files",
196
+ JSON.stringify(sum([latex({ markers: ["rerun-requested"] })]).unsettled) ===
197
+ JSON.stringify(["rerun-requested"]),
198
+ );
199
+ // Guards: 'not settled yet' vs 'this key does not exist' — on stable files an undefined reference
200
+ // never resolves, so rerunning for it burns the cap and reports no-convergence for a typo.
201
+ check(
202
+ "summarize: undefined references ALONE leave nothing unsettled — on stable files they are a real bad key",
203
+ sum([latex({ markers: ["undefined-references"] })]).unsettled.length === 0,
204
+ );
205
+ check(
206
+ "summarize: bibtex that rewrote the .bbl leaves paper.bbl unsettled",
207
+ JSON.stringify(sum([latex({ bib: bib(["k"]) }), bibtex()]).unsettled) ===
208
+ JSON.stringify(["paper.bbl"]),
209
+ );
210
+ // Guards: reading bibtex's effect from the .bbl — a byte-identical .bbl changes nothing the next
211
+ // pass would read.
212
+ check(
213
+ "summarize: bibtex left the .bbl byte-identical — the pass before it answers",
214
+ sum([latex({ bib: bib(["k"]) }), bibtex({ after: H("a1") })]).unsettled
215
+ .length === 0,
216
+ );
217
+ check(
218
+ "summarize: latexPasses counts the non-final pdflatex passes only",
219
+ sum([latex(), bibtex(), latex(), latex({ final: true })]).latexPasses === 2,
220
+ );
221
+ check(
222
+ "summarize: finalDone and the undefined-reference warnings of the final log, nothing else",
223
+ (() => {
224
+ const s = sum([
225
+ latex(),
226
+ latex({
227
+ final: true,
228
+ markers: ["undefined-citations", "rerun-requested"],
229
+ }),
230
+ ]);
231
+ return (
232
+ s.finalDone &&
233
+ JSON.stringify(s.warnings) === JSON.stringify(["undefined-citations"])
234
+ );
235
+ })(),
236
+ );
237
+
238
+ // ── 3. composed: nextStep(summarize(history)) ───────────────────────────────────────────
239
+ const step = (history) => nextStep(summarize(history));
240
+
241
+ // ── the histories the loop sees ───────────────────────────────────────────────────────────────────────────
242
+ const TABLE = [
243
+ ["nothing ran yet — the first step is a plain pass", [], "latex"],
244
+ [
245
+ "a pass that created the aux — rerun",
246
+ [latex({ before: H(null), after: H("a1") })],
247
+ "latex",
248
+ ],
249
+ [
250
+ "a pass whose tracked files did not change and whose log asks for nothing — converged, so the FINAL pass",
251
+ [latex({ before: H(null), after: H("a1") }), latex()],
252
+ "latex-final",
253
+ ],
254
+ ["the final pass exited 0 — done", [latex(), latex({ final: true })], "done"],
255
+ [
256
+ "a .toc change alone is a rerun",
257
+ [latex({ before: H("a1", "t1"), after: H("a1", "t2") })],
258
+ "latex",
259
+ ],
260
+ [
261
+ "a .out change alone is a rerun (hyperref bookmarks)",
262
+ [latex({ before: H("a1", null, "o1"), after: H("a1", null, "o2") })],
263
+ "latex",
264
+ ],
265
+ [
266
+ "marker: Rerun to get … — rerun even with identical files",
267
+ [latex({ markers: ["rerun-requested"] })],
268
+ "latex",
269
+ ],
270
+ [
271
+ "marker: Label(s) may have changed — rerun",
272
+ [latex({ markers: ["labels-changed"] })],
273
+ "latex",
274
+ ],
275
+ [
276
+ "marker: rerunfilecheck — rerun",
277
+ [latex({ markers: ["rerunfilecheck"] })],
278
+ "latex",
279
+ ],
280
+ [
281
+ "undefined references ALONE do not request a rerun — on stable files they are a real bad key",
282
+ [latex({ markers: ["undefined-references"] })],
283
+ "latex-final",
284
+ ],
285
+ [
286
+ "the aux names a bibliography bibtex has not run on — bibtex",
287
+ [latex({ before: H(null), after: H("a1"), bib: bib(["k"]) })],
288
+ "bibtex",
289
+ ],
290
+ [
291
+ "after bibtex changed the .bbl — rerun",
292
+ [latex({ bib: bib(["k"]) }), bibtex()],
293
+ "latex",
294
+ ],
295
+ [
296
+ "bibtex ran on exactly this input — not again",
297
+ [
298
+ latex({ bib: bib(["k"]) }),
299
+ bibtex(),
300
+ latex({
301
+ before: H("a1", null, null, "bbl1"),
302
+ after: H("a1", null, null, "bbl1"),
303
+ bib: bib(["k"]),
304
+ }),
305
+ ],
306
+ "latex-final",
307
+ ],
308
+ [
309
+ "the \\citation set changed since bibtex ran — bibtex again",
310
+ [latex({ bib: bib(["k"]) }), bibtex(), latex({ bib: bib(["k", "k2"]) })],
311
+ "bibtex",
312
+ ],
313
+ [
314
+ "the .bib content changed since bibtex ran — bibtex again",
315
+ [latex({ bib: bib(["k"]) }), bibtex(), latex({ bib: bib(["k"], "b2") })],
316
+ "bibtex",
317
+ ],
318
+ [
319
+ "bibtex left the .bbl byte-identical — the verdict of the pass before it stands (converged)",
320
+ [
321
+ latex({ bib: bib(["k"]) }),
322
+ bibtex({
323
+ before: H("a1", null, null, "bbl1"),
324
+ after: H("a1", null, null, "bbl1"),
325
+ }),
326
+ ],
327
+ "latex-final",
328
+ ],
329
+ [
330
+ "a pdflatex pass exited non-zero — fail",
331
+ [
332
+ latex({
333
+ exitCode: 1,
334
+ errorLines: ["./paper.tex:4: Undefined control sequence.", "l.4 \\foo"],
335
+ }),
336
+ ],
337
+ "fail",
338
+ ],
339
+ [
340
+ "bibtex exited non-zero — fail",
341
+ [
342
+ latex({ bib: bib(["k"]) }),
343
+ bibtex({
344
+ exitCode: 2,
345
+ errorLines: ["I couldn't open database file nope.bib"],
346
+ }),
347
+ ],
348
+ "fail",
349
+ ],
350
+ [
351
+ "the FINAL pass exited non-zero (a paper-guards error) — fail, not done",
352
+ [latex(), latex({ final: true, exitCode: 1 })],
353
+ "fail",
354
+ ],
355
+ ];
356
+
357
+ for (const [label, history, expected] of TABLE) {
358
+ const got = kind(step(history));
359
+ check(`${label} (expected ${expected}, got ${got})`, got === expected);
360
+ }
361
+
362
+ // ── what a fail carries ─────────────────────────────────────────────────────────────────
363
+ const latexFail = step([
364
+ latex({
365
+ exitCode: 1,
366
+ errorLines: ["./paper.tex:4: Undefined control sequence.", "l.4 \\foo"],
367
+ }),
368
+ ]);
369
+ check(
370
+ "a latex fail names the step, the exit code and the log excerpt verbatim",
371
+ latexFail.step === "latex" &&
372
+ latexFail.cause.kind === "exit" &&
373
+ latexFail.cause.code === 1 &&
374
+ latexFail.lines[1] === "l.4 \\foo",
375
+ );
376
+ const bibFail = step([
377
+ latex({ bib: bib(["k"]) }),
378
+ bibtex({
379
+ exitCode: 2,
380
+ errorLines: ["I couldn't open database file nope.bib"],
381
+ }),
382
+ ]);
383
+ check(
384
+ "a bibtex fail names BIBTEX, not latex",
385
+ bibFail.kind === "fail" &&
386
+ bibFail.step === "bibtex" &&
387
+ bibFail.cause.code === 2,
388
+ );
389
+
390
+ // ── the cap ─────────────────────────────────────────────────────────────────────────────
391
+ const moving = (i) => latex({ before: H(`a${i}`), after: H(`a${i + 1}`) });
392
+ const underCap = Array.from({ length: MAX_PASSES - 1 }, (_, i) => moving(i));
393
+ check(
394
+ `the cap is ${MAX_PASSES} and it is not hit early: after ${MAX_PASSES - 1} moving passes, one more`,
395
+ MAX_PASSES === 5 && kind(step(underCap)) === "latex",
396
+ );
397
+ const atCap = Array.from({ length: MAX_PASSES }, (_, i) => moving(i));
398
+ const capped = step(atCap);
399
+ check(
400
+ "🔴 after the cap, a still-moving aux is a FAIL — no convergence — not a PDF with stale references",
401
+ capped.kind === "fail" && capped.cause.kind === "no-convergence",
402
+ );
403
+ check(
404
+ "the non-convergence failure NAMES the file that kept changing",
405
+ capped.kind === "fail" && capped.lines[0].includes("paper.aux"),
406
+ );
407
+ const cappedMarker = step(
408
+ Array.from({ length: MAX_PASSES }, () =>
409
+ latex({ markers: ["labels-changed"] }),
410
+ ),
411
+ );
412
+ check(
413
+ "a log that keeps asking for a rerun hits the same cap, and names the marker",
414
+ cappedMarker.kind === "fail" &&
415
+ cappedMarker.lines[0].includes("labels-changed"),
416
+ );
417
+ const convergedAtCap = [...atCap.slice(0, -1), latex()];
418
+ check(
419
+ "reaching the cap WITH a converged last pass is not a failure — the final pass still runs",
420
+ kind(step(convergedAtCap)) === "latex-final",
421
+ );
422
+
423
+ // ── done carries the undefined-reference summaries ─────────────────────────────────────
424
+ const doneWarn = step([
425
+ latex(),
426
+ latex({ final: true, markers: ["undefined-citations", "rerun-requested"] }),
427
+ ]);
428
+ check(
429
+ "done reports undefined citations still in the final log, and nothing else",
430
+ doneWarn.kind === "done" &&
431
+ doneWarn.warnings.length === 1 &&
432
+ doneWarn.warnings[0] === "undefined-citations",
433
+ );
434
+
435
+ // ── helpers ─────────────────────────────────────────────────────────────────────────────
436
+ check(
437
+ "changedFiles lists exactly the differing tracked files",
438
+ JSON.stringify(
439
+ changedFiles(H("a", "t", null, "b"), H("a", "t2", "o", "b")),
440
+ ) === JSON.stringify(["toc", "out"]),
441
+ );
442
+ check(
443
+ "a file appearing (null → digest) is a change",
444
+ changedFiles(H(null), H("a1")).includes("aux"),
445
+ );
446
+
447
+ console.log(
448
+ `✓ ${String(n)} assertions passed — latex-loop: nextStep over State, summarize over histories, and the two composed, including the ${MAX_PASSES}-pass cap`,
449
+ );
@@ -0,0 +1,211 @@
1
+ /**
2
+ * THE DECISION of the LaTeX build loop: given what the runs so far observed, what runs next.
3
+ *
4
+ * PURE. No disk, no process, no clock. The shell in `build.ts` runs a step, records what it saw
5
+ * as an `Observation`, appends it to the history and asks `nextStep(summarize(history))` again.
6
+ * Both halves are table tests (`latex-loop.harness.mjs`), not scenarios that need TeX.
7
+ *
8
+ * ── THE LOOP: `summarize` reduces the history to a `State`, and `nextStep` asks four questions ──
9
+ * 1. did the last program fail? → fail, with its error lines
10
+ * 2. did the citations, a .bib or the style change since
11
+ * bibtex last ran? → bibtex
12
+ * 3. did the last pass change a tracked file, or did the
13
+ * log ask for a rerun? → latex (fail after MAX_PASSES)
14
+ * 4. otherwise → one FINAL latex pass, then done
15
+ *
16
+ * 🔴 WHY A FINAL PASS AFTER CONVERGENCE. `paper-guards.tex`, shipped by this package, turns an
17
+ * undefined `\ref` or `\cite` into a BUILD FAILURE — but only on a pass that defines
18
+ * `\finalpass`, because on the first pass every reference is undefined by construction. Measured
19
+ * on 2026-08-26 with the unconditional form: pass 1 fails, the `.aux` is never written, pass 2
20
+ * fails the same way and no PDF ever appears. So the guards need to know which pass is the last,
21
+ * and only the loop knows it. The price is one extra pdflatex run per build, paid for turning
22
+ * "?? in the PDF" into a red build.
23
+ *
24
+ * 🔴 WHY A CAP. A document can keep changing its own aux on every pass — the textbook case is a
25
+ * page reference whose own wording moves its target to the other page, and back — and the aux never
26
+ * settles. Without a cap the loop runs forever; with a silent cap it stops and ships an
27
+ * unconverged PDF with wrong cross-references. It stops, and it FAILS, naming the file that kept
28
+ * changing.
29
+ *
30
+ * ⚠️ `filecontents` with `[overwrite]` is NOT such a case, though this comment said so until
31
+ * 2026-09-24. The environment body is verbatim, so every pass writes the same bytes, and the loop
32
+ * compares CONTENT digests, not modification times. Measured with `paperlint build` on TeX Live 2026: a
33
+ * real acmart paper embedding its `refs.bib` that way converged in 3 pdflatex passes, and an
34
+ * `article` with an overwritten `\input` file carrying `\label`/`\ref`, a `\tableofcontents` and
35
+ * an overwritten `.bib` converged in 4.
36
+ */
37
+
38
+ /** Files a pdflatex pass writes and the next pass reads. A change means the output is stale. */
39
+ export type Tracked = "aux" | "toc" | "out" | "bbl";
40
+ export const TRACKED: readonly Tracked[] = ["aux", "toc", "out", "bbl"];
41
+
42
+ /** Content digests of the tracked files; `null` = the file does not exist. */
43
+ export type Hashes = { readonly [K in Tracked]: string | null };
44
+
45
+ /** Log markers, as `latex-log.ts` reads them. Re-declared as a type so this module has no imports. */
46
+ export type LogMarker =
47
+ | "rerun-requested"
48
+ | "labels-changed"
49
+ | "rerunfilecheck"
50
+ | "undefined-references"
51
+ | "undefined-citations";
52
+
53
+ const RERUN: readonly LogMarker[] = [
54
+ "rerun-requested",
55
+ "labels-changed",
56
+ "rerunfilecheck",
57
+ ];
58
+
59
+ /**
60
+ * What bibtex would be run on. `none` — the aux names no `\bibdata`, so there is nothing for
61
+ * bibtex to do. Two `needed` states are the same input exactly when every field is equal: the
62
+ * citation set moved, a database was added, or a `.bib` file's content changed ⇒ bibtex again.
63
+ */
64
+ export type BibInput =
65
+ | { readonly kind: "none" }
66
+ | {
67
+ readonly kind: "needed";
68
+ /** Sorted, unique cited keys. */
69
+ readonly citations: readonly string[];
70
+ readonly databases: readonly string[];
71
+ readonly style: string | null;
72
+ /** A digest over the content of every database file that exists; null when none was found. */
73
+ readonly bibHash: string | null;
74
+ };
75
+
76
+ export type Observation =
77
+ | {
78
+ readonly step: "latex";
79
+ /** Whether this pass defined `\finalpass`. */
80
+ readonly final: boolean;
81
+ readonly exitCode: number;
82
+ readonly before: Hashes;
83
+ readonly after: Hashes;
84
+ readonly markers: readonly LogMarker[];
85
+ /** The bibliography input as the aux stood after this pass. */
86
+ readonly bib: BibInput;
87
+ /** The first error and its source context, from the log. Empty on success. */
88
+ readonly errorLines: readonly string[];
89
+ }
90
+ | {
91
+ readonly step: "bibtex";
92
+ readonly exitCode: number;
93
+ readonly before: Hashes;
94
+ readonly after: Hashes;
95
+ /** The input bibtex was run on. */
96
+ readonly bib: BibInput;
97
+ readonly errorLines: readonly string[];
98
+ };
99
+
100
+ export type FailCause =
101
+ | { readonly kind: "exit"; readonly code: number }
102
+ | { readonly kind: "no-convergence"; readonly unsettled: readonly string[] };
103
+
104
+ export type Step =
105
+ | { readonly kind: "latex"; readonly final: boolean }
106
+ | { readonly kind: "bibtex" }
107
+ | {
108
+ readonly kind: "done";
109
+ /** Undefined-reference summaries still in the final log — a converged doc with a bad key. */
110
+ readonly warnings: readonly LogMarker[];
111
+ }
112
+ | {
113
+ readonly kind: "fail";
114
+ readonly step: "latex" | "bibtex";
115
+ readonly cause: FailCause;
116
+ /** What to show the human: the log excerpt, or the non-convergence explanation. */
117
+ readonly lines: readonly string[];
118
+ };
119
+
120
+ /** Non-final pdflatex passes allowed before the build is declared non-converging. */
121
+ export const MAX_PASSES = 5;
122
+
123
+ /** The history, reduced to the four questions `nextStep` asks. */
124
+ export type State = {
125
+ readonly failed: {
126
+ readonly step: "latex" | "bibtex";
127
+ readonly cause: FailCause;
128
+ readonly lines: readonly string[];
129
+ } | null;
130
+ /** Citations, databases, style or `.bib` content changed since bibtex last ran. */
131
+ readonly bibOutdated: boolean;
132
+ /** Tracked files the last pass changed (`paper.aux`), or the rerun markers its log printed. */
133
+ readonly unsettled: readonly string[];
134
+ /** Non-final pdflatex passes so far. */
135
+ readonly latexPasses: number;
136
+ readonly finalDone: boolean;
137
+ /** Undefined-reference summaries in the log of the final pass. */
138
+ readonly warnings: readonly LogMarker[];
139
+ };
140
+
141
+ export function nextStep(s: State): Step {
142
+ if (s.failed) return { kind: "fail", ...s.failed };
143
+ if (s.finalDone) return { kind: "done", warnings: s.warnings };
144
+ if (s.bibOutdated) return { kind: "bibtex" };
145
+ if (s.unsettled.length === 0) return { kind: "latex", final: true };
146
+ if (s.latexPasses < MAX_PASSES) return { kind: "latex", final: false };
147
+ return noConvergence(s.unsettled);
148
+ }
149
+
150
+ const noConvergence = (unsettled: readonly string[]): Step => ({
151
+ kind: "fail",
152
+ step: "latex",
153
+ cause: { kind: "no-convergence", unsettled },
154
+ lines: [
155
+ `the build does not converge: after ${MAX_PASSES} pdflatex passes, the last one still changed or asked to rerun: ${unsettled.join(", ")}.`,
156
+ `A document whose aux keeps changing on every pass never settles; stopping here instead of`,
157
+ `shipping a PDF with stale cross-references.`,
158
+ ],
159
+ });
160
+
161
+ /** The tracked files whose digest differs between two snapshots. */
162
+ export function changedFiles(before: Hashes, after: Hashes): Tracked[] {
163
+ return TRACKED.filter((f) => before[f] !== after[f]);
164
+ }
165
+
166
+ const sameBib = (a: BibInput, b: BibInput): boolean =>
167
+ JSON.stringify(a) === JSON.stringify(b);
168
+
169
+ /**
170
+ * What the last run left unsettled. Walking back: a run that changed a tracked file answers with
171
+ * those files; a latex pass that changed none answers with the rerun markers in its log. A bibtex
172
+ * run that left the `.bbl` byte-identical changes nothing, so the pass before it answers.
173
+ */
174
+ function unsettledBy(history: readonly Observation[]): string[] {
175
+ if (history.length === 0) return ["paper.aux"]; // no pass has written it yet
176
+ for (const o of history.toReversed()) {
177
+ const files = changedFiles(o.before, o.after);
178
+ if (files.length > 0) return files.map((f) => `paper.${f}`);
179
+ if (o.step === "latex") return o.markers.filter((m) => RERUN.includes(m));
180
+ }
181
+ return [];
182
+ }
183
+
184
+ export function summarize(history: readonly Observation[]): State {
185
+ const last = history.at(-1);
186
+ const latexRuns = history.filter((o) => o.step === "latex");
187
+ const lastLatex = latexRuns.at(-1);
188
+ const lastBibtex = history.filter((o) => o.step === "bibtex").at(-1);
189
+ return {
190
+ failed:
191
+ last && last.exitCode !== 0
192
+ ? {
193
+ step: last.step,
194
+ cause: { kind: "exit", code: last.exitCode },
195
+ lines: last.errorLines,
196
+ }
197
+ : null,
198
+ bibOutdated:
199
+ lastLatex?.bib.kind === "needed" &&
200
+ !(lastBibtex && sameBib(lastBibtex.bib, lastLatex.bib)),
201
+ unsettled: unsettledBy(history),
202
+ latexPasses: latexRuns.filter((o) => !o.final).length,
203
+ finalDone: last?.step === "latex" && last.final,
204
+ warnings:
205
+ last?.step === "latex"
206
+ ? last.markers.filter(
207
+ (m) => m === "undefined-references" || m === "undefined-citations",
208
+ )
209
+ : [],
210
+ };
211
+ }