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,166 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * `extract-pdf-facts.mjs` — the command-line shim over `writeFacts`, run as a process the way CI
4
+ * runs it, on a real committed PDF.
5
+ *
6
+ * What is pinned here is the CONTRACT callers branch on — the exit codes and where the file lands —
7
+ * plus the one defect of the poppler reader this shim used to carry: it read `yes` anywhere in a
8
+ * `pdffonts` row, so a font printed `no no yes` (not embedded, not subset, has a ToUnicode map)
9
+ * came out embedded. `fixtures/pdf-facts/t3-mixed.pdf` carries exactly such a font.
10
+ *
11
+ * The shim imports the compiled package (`dist/`), so `npm run build` runs before this.
12
+ */
13
+ import assert from "node:assert/strict";
14
+ import { spawnSync } from "node:child_process";
15
+ import {
16
+ cpSync,
17
+ existsSync,
18
+ mkdirSync,
19
+ mkdtempSync,
20
+ readFileSync,
21
+ realpathSync,
22
+ rmSync,
23
+ writeFileSync,
24
+ } from "node:fs";
25
+ import { tmpdir } from "node:os";
26
+ import { dirname, join, resolve } from "node:path";
27
+ import { fileURLToPath } from "node:url";
28
+
29
+ const HERE = dirname(fileURLToPath(import.meta.url));
30
+ const SHIM = join(HERE, "extract-pdf-facts.mjs");
31
+ const FIX = resolve(HERE, "..", "..", "fixtures", "pdf-facts");
32
+
33
+ let n = 0;
34
+ const check = (label, cond, detail = "") => {
35
+ assert.ok(cond, detail ? `${label} — ${detail}` : label);
36
+ n++;
37
+ };
38
+
39
+ const root = realpathSync(mkdtempSync(join(tmpdir(), "rpp-extract-")));
40
+ /**
41
+ * Run the shim from `root`, with no banal anywhere unless `env` names one. `HOME` is the temp root:
42
+ * without it Node falls back to the account's home, where `paperlint toolchain` may have installed banal.
43
+ */
44
+ const shim = (args, env = {}) =>
45
+ spawnSync(process.execPath, [SHIM, ...args], {
46
+ cwd: root,
47
+ encoding: "utf8",
48
+ env: {
49
+ PATH: process.env.PATH,
50
+ HOME: root,
51
+ CLAUDE_PROJECT_DIR: root,
52
+ ...env,
53
+ },
54
+ });
55
+ const said = (r) => `${r.stdout}${r.stderr}`;
56
+
57
+ try {
58
+ const paper = join(root, "papers", "mixed");
59
+ mkdirSync(paper, { recursive: true });
60
+ cpSync(join(FIX, "t3-mixed.pdf"), join(paper, "paper.pdf"));
61
+ writeFileSync(
62
+ join(paper, "venue.json"),
63
+ JSON.stringify({ venue: "agenticdev", kind: "short" }),
64
+ );
65
+ const factsFile = join(paper, "_build", "paper.facts.json");
66
+
67
+ check("usage: no target is exit 2", shim([]).status === 2);
68
+ check("no such path is exit 1", shim([join(root, "nope")]).status === 1);
69
+
70
+ const strict = shim([paper, "--strict"]);
71
+ check(
72
+ "🔴 --strict with no banal: exit 1, named as an environment error, and nothing written",
73
+ strict.status === 1 &&
74
+ /banal not found/.test(said(strict)) &&
75
+ /`npx paperlint toolchain` installs it/.test(said(strict)) &&
76
+ /environment error/.test(said(strict)) &&
77
+ !existsSync(factsFile),
78
+ said(strict),
79
+ );
80
+
81
+ const local = shim([paper]);
82
+ check(
83
+ "without --strict and no banal: exit 0, facts written, the missing geometry said out loud",
84
+ local.status === 0 &&
85
+ existsSync(factsFile) &&
86
+ /banal not found/.test(local.stderr),
87
+ said(local),
88
+ );
89
+ const facts = JSON.parse(readFileSync(factsFile, "utf8"));
90
+ check(
91
+ "the facts are schema 2, about paper.pdf, for the venue venue.json declares",
92
+ facts.schema === 2 &&
93
+ facts.pdf === "paper.pdf" &&
94
+ facts.venue === "agenticdev" &&
95
+ facts.kind === "short",
96
+ JSON.stringify(facts).slice(0, 200),
97
+ );
98
+ check(
99
+ "a text PDF has fonts",
100
+ Array.isArray(facts.fonts) && facts.fonts.length === 4,
101
+ JSON.stringify(facts.fonts),
102
+ );
103
+ const times = facts.fonts.find((f) => f.name === "Times-Roman");
104
+ check(
105
+ "🔴 Times-Roman — pdffonts `Type 1 Custom no no yes` — is embedded: false (the old reader said true)",
106
+ times?.embedded === false && times.type === "Type 1",
107
+ JSON.stringify(times),
108
+ );
109
+ check(
110
+ "the Type 3 font keeps poppler's type spelling, which consumers match on",
111
+ facts.fonts.some((f) => f.type === "Type 3"),
112
+ );
113
+
114
+ const fake = join(root, "banal.pl");
115
+ writeFileSync(
116
+ fake,
117
+ `print '{"papersize":[792,612],"columns":2,"bodyfontsize":9,"pages":[{}]}';\n`,
118
+ );
119
+ const withBanal = shim([paper, "--strict"], { BANAL: fake });
120
+ const g = JSON.parse(readFileSync(factsFile, "utf8"));
121
+ check(
122
+ "--strict with banal ($BANAL): exit 0, and the geometry is banal's",
123
+ withBanal.status === 0 && g.geometry_source === "banal" && g.columns === 2,
124
+ said(withBanal),
125
+ );
126
+
127
+ // Guards: missing perl is its own diagnosis, with the fix — not a generic "banal failed".
128
+ const noPerl = shim([paper, "--strict"], {
129
+ BANAL: fake,
130
+ PATH: mkdtempSync(join(root, "empty-path-")),
131
+ });
132
+ check(
133
+ "🔴 --strict with banal but no perl: exit 1, and the message names perl and how to get it",
134
+ noPerl.status === 1 &&
135
+ /perl is not installed/.test(said(noPerl)) &&
136
+ /apt-get install perl/.test(said(noPerl)),
137
+ said(noPerl),
138
+ );
139
+ const noPerlLocal = shim([paper], {
140
+ BANAL: fake,
141
+ PATH: mkdtempSync(join(root, "empty-path-")),
142
+ });
143
+ check(
144
+ "without --strict and no perl: exit 0, and the missing geometry is said, naming perl",
145
+ noPerlLocal.status === 0 &&
146
+ /perl is not installed/.test(noPerlLocal.stderr),
147
+ said(noPerlLocal),
148
+ );
149
+
150
+ writeFileSync(
151
+ join(paper, "venue.json"),
152
+ JSON.stringify({ venue: "x", pdf: "build/other.pdf" }),
153
+ );
154
+ const missing = shim([paper, "--strict"], { BANAL: fake });
155
+ check(
156
+ "a declared artifact that is not built is exit 3, not 1",
157
+ missing.status === 3 && /not built/.test(said(missing)),
158
+ said(missing),
159
+ );
160
+ } finally {
161
+ rmSync(root, { recursive: true, force: true });
162
+ }
163
+
164
+ console.log(
165
+ `✓ ${String(n)} assertions passed — extract-pdf-facts: the exit-code contract, schema 2, and the no-no-yes font`,
166
+ );
@@ -0,0 +1,144 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * extract-pdf-facts.mjs — measure a finished PDF and write its FACTS into
4
+ * `<paper>/_build/paper.facts.json`. It judges nothing; lint rules judge the file.
5
+ *
6
+ * 🔴 A THIN SHIM. The measuring and the writing are `measurePaper` and `writeFactsFile` in
7
+ * `src/facts-file.ts`, the one writer of that file; `paperlint build` calls the same functions right after
8
+ * it compiles a paper. This file is a composition root: it reads the environment and builds the real
9
+ * adapters (`dist/adapters/node/`), and it owns the `--strict` policy. This
10
+ * script exists for two callers the build does not serve: a PDF paperlint did not build (a paper that
11
+ * declares its artifact elsewhere in `venue.json`), and CI steps that name this script by path.
12
+ * It reaches the package's compiled code through `../../dist/`, resolved from this file's real
13
+ * location, so it works the same from a checkout, from `node_modules` and through a symlink.
14
+ *
15
+ * WHAT IT MEASURES WITH. pdf.js (the `unpdf` package, installed with paperlint) for the page count, the
16
+ * fonts and the last page — nothing to install. `banal` (the page-geometry script HotCRP's format
17
+ * checker runs) for paper size, columns and font sizes — run by perl on XML paperlint writes from the same
18
+ * pdf.js read, no poppler. `npx paperlint toolchain` installs it; `$BANAL` or a project's `vendor/banal`
19
+ * take precedence (`src/adapters/banal/`).
20
+ *
21
+ * ── THE EXIT-CODE CONTRACT (callers branch on it; the table is `src/exit-code.ts`) ──
22
+ * 0 facts written — or, without `--strict`, not written and said so (a local run may lack banal)
23
+ * 1 no such path; or, with `--strict`, the facts could not be taken (no banal, no perl, banal
24
+ * failed, unreadable PDF).
25
+ * In CI a missing tool is an ENVIRONMENT error, and a skipped step looks like a passed one
26
+ * 2 usage
27
+ * 3 the paper declares an artifact that is not built — a different event from "extraction
28
+ * crashed", and the caller must be able to tell them apart (external review, 2026-08-31)
29
+ */
30
+ import { existsSync, statSync } from "node:fs";
31
+ import { join, dirname, basename, relative } from "node:path";
32
+ import { isMain } from "../paper-pipeline/scripts/consumer.mjs";
33
+ import {
34
+ declaredVenue as declaredIn,
35
+ measurePaper,
36
+ writeFactsFile,
37
+ } from "../../dist/facts-file.js";
38
+ import { readPdf } from "../../dist/pdf-facts.js";
39
+ import {
40
+ banalMeasurer,
41
+ parseBanalSettings,
42
+ } from "../../dist/adapters/banal/index.js";
43
+ import {
44
+ hostDirs,
45
+ nodeAdapters,
46
+ nodeFiles,
47
+ } from "../../dist/adapters/node/index.js";
48
+ import { whyNoGeometry } from "../../dist/domain/geometry.js";
49
+ import { exitCodeFor } from "../../dist/exit-code.js";
50
+
51
+ /** The paper's `venue.json`, read from disk. */
52
+ export const declaredVenue = (paperDir) => declaredIn(nodeFiles, paperDir);
53
+
54
+ const ROOT = process.env.CLAUDE_PROJECT_DIR || process.cwd();
55
+
56
+ /**
57
+ * Accept either a path to a PDF or a PAPER DIRECTORY — and in the second case find the artifact on
58
+ * its own: the path the paper declares in the `pdf` field of `venue.json`, else `paper.pdf` beside
59
+ * it. A paper written in markdown and built by its own script into `build/acl_latex.pdf` would
60
+ * otherwise be skipped silently. The facts ALWAYS go into `<paper directory>/_build/`, not next to
61
+ * the PDF, where neither a rule's glob nor a human looks.
62
+ */
63
+ export function resolveTarget(arg) {
64
+ const isDir = existsSync(arg) && statSync(arg).isDirectory();
65
+ const paperDir = isDir ? arg : dirname(arg);
66
+ const decl = declaredVenue(paperDir);
67
+ const pdf = isDir ? join(paperDir, decl?.pdf || "paper.pdf") : arg;
68
+ return { paperDir, pdf, decl: decl || {} };
69
+ }
70
+
71
+ /**
72
+ * The ONE `process.exit`: print what happened, then exit with the contract's code for this outcome
73
+ * in this mode.
74
+ */
75
+ function finish(outcome, strict, say = null) {
76
+ if (say) console.error(say);
77
+ process.exit(exitCodeFor(outcome, { strict }));
78
+ }
79
+
80
+ /** Parse the command line, or finish with the contract's code. */
81
+ function parseCommandLine(argv) {
82
+ const strict = argv.includes("--strict");
83
+ const [target, venue, kind] = argv.filter((a) => a !== "--strict");
84
+ if (!target)
85
+ finish(
86
+ "usage",
87
+ strict,
88
+ "usage: extract-pdf-facts.mjs <paper.pdf | paper directory> [venue] [kind] [--strict]",
89
+ );
90
+ if (!existsSync(target))
91
+ finish("no-such-path", strict, `🛑 no such path ${target}`);
92
+ return { strict, target, venue, kind };
93
+ }
94
+
95
+ /** Why no facts were taken, in the words for this mode. */
96
+ const refusal = (msg, strict) =>
97
+ strict
98
+ ? `🛑 facts not taken: ${msg} — in CI this is an environment error, not a skip`
99
+ : `⏭️ facts not taken: ${msg} (a local run, without --strict). THE PDF IS NOT CHECKED`;
100
+
101
+ // 🔴 `isMain`, NOT a comparison of `import.meta.url` with `process.argv[1]`. Node resolves the entry
102
+ // point to its REAL path for `import.meta.url` but leaves `process.argv[1]` as typed, so through a
103
+ // symlink the two differ and the CLI silently does nothing, exiting 0. Observed 2026-09-14 on run
104
+ // 34784079821: `extract-pdf-facts.mjs --strict` returned RC=0 and created no facts file.
105
+ if (isMain(import.meta.url)) {
106
+ const { strict, target, venue, kind } = parseCommandLine(
107
+ process.argv.slice(2),
108
+ );
109
+ const { paperDir, pdf } = resolveTarget(target);
110
+ if (!existsSync(pdf))
111
+ finish(
112
+ "no-artifact",
113
+ strict,
114
+ `🛑 no artifact ${pdf} — the paper declared it in venue.json, but it is not built`,
115
+ );
116
+ // The composition root: banal as the measurer, wired from this process's environment.
117
+ const settings = parseBanalSettings(process.env, hostDirs());
118
+ const measure = banalMeasurer(
119
+ nodeAdapters({ tmpDir: settings.tmpDir }),
120
+ settings,
121
+ ROOT,
122
+ );
123
+ // Command-line arguments override the declaration — for a one-off check of someone else's PDF.
124
+ const m = await measurePaper(paperDir, pdf, {
125
+ readPdf,
126
+ venue: venue ?? null,
127
+ kind: kind ?? null,
128
+ measure,
129
+ files: nodeFiles,
130
+ });
131
+ if (!m.ok) finish("unreadable", strict, refusal(m.error, strict));
132
+ const { facts, geometry } = m.value;
133
+ const why = geometry.kind === "unmeasured" ? whyNoGeometry(geometry) : null;
134
+ if (why && strict) finish("no-geometry", strict, refusal(why, strict));
135
+ const out = writeFactsFile(nodeFiles, paperDir, facts);
136
+ if (why)
137
+ console.error(
138
+ `⚠️ ${why} — page size, columns and font sizes are null in the facts (run with --strict to require them)`,
139
+ );
140
+ console.log(
141
+ `✅ ${basename(pdf)} → ${relative(ROOT, out) || out} (${facts.fonts.length} faces, ${facts.npages} pp.)`,
142
+ );
143
+ finish("written", strict);
144
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * render-paper — the PAID tier: does this skill's description actually fire?
3
+ *
4
+ * COLOCATED ON PURPOSE (vigiles decides coverage by placement as of 2026-08-11).
5
+ * The prompts live in `.claude/lib/skill-trigger-cases.mjs` so all 21 cases
6
+ * are reviewed as one table where collisions between siblings are visible;
7
+ * copying them here would recreate the drift that rule exists to prevent.
8
+ *
9
+ * Measures recall (fires on its own territory) AND precision (stays quiet on a
10
+ * colliding sibling's territory), against the REAL `.claude` harness so the skill
11
+ * competes with every other installed description — an isolated run overstates
12
+ * recall and understates false positives.
13
+ *
14
+ * Costs money; not CI.
15
+ * node .claude/skills/render-paper/render-paper.eval.mjs [trials]
16
+ */
17
+ import { runSkillTriggerEval } from "../../lib/skill-eval-kit.mjs";
18
+
19
+ await runSkillTriggerEval("render-paper");
@@ -0,0 +1,339 @@
1
+ /**
2
+ * render-paper — the free, deterministic tier. No model, no network.
3
+ *
4
+ * COLOCATED ON PURPOSE. vigiles decides coverage by PLACEMENT as of 2026-08-11:
5
+ * a test that merely names a surface no longer counts, because that tier was
6
+ * crediting surfaces nothing touched. So each skill needs a file inside its own
7
+ * directory — this one.
8
+ *
9
+ * The assertions live in `.claude/lib/skill-checks.mjs` and are CALLED here with
10
+ * this skill's name. They are not copied: 22 copies of the same checks is the drift that
11
+ * module exists to avoid. (Until 2026-08-11 this was an env-var side channel into a
12
+ * 614-line file named after no surface; it is a function call now.)
13
+ *
14
+ * What this proves: this skill's frontmatter parses as strict YAML, its declared
15
+ * tool contract is sane, its pipeline wiring points at scripts that exist, and it
16
+ * announces/records under ITS OWN identity rather than a sibling's.
17
+ *
18
+ * What it does NOT prove: that the skill fires, or that its guidance produces a
19
+ * good result. Those need a real model — see `render-paper.eval.mjs`.
20
+ */
21
+ import assert from "node:assert/strict";
22
+ import {
23
+ copyFileSync,
24
+ existsSync,
25
+ mkdirSync,
26
+ mkdtempSync,
27
+ readFileSync,
28
+ readdirSync,
29
+ writeFileSync,
30
+ realpathSync,
31
+ } from "node:fs";
32
+ import { tmpdir } from "node:os";
33
+ import { spawnSync } from "node:child_process";
34
+ import { fileURLToPath } from "node:url";
35
+ import { dirname, join } from "node:path";
36
+ import { checkSkill } from "../../lib/skill-checks.mjs";
37
+ import { papersRoot } from "../../eslint-rules/papers.mjs";
38
+ import {
39
+ consumerPkg,
40
+ consumerRoot,
41
+ } from "../paper-pipeline/scripts/consumer.mjs";
42
+
43
+ await checkSkill("render-paper");
44
+
45
+ // The TeX package pins that stood here (texlive-fonts-extra, texlive-plain-generic in
46
+ // ensure-toolchain.sh) moved with the list itself: the venue profiles declare the packages, and
47
+ // src/tex-requirements.harness.mjs asserts that every acmart venue declares libertine, inconsolata,
48
+ // newtx, kastrup (binhex.tex) and fancyhdr — each proved by the file whose absence broke a build.
49
+
50
+ // ── TEXINPUTS: WHERE THE SCRIPTS LOOK FOR `paper-guards.tex` ─────────────────
51
+ //
52
+ // Why this is guarded by a test and not by proofreading. The reference guard is included via
53
+ // `\input{paper-guards}` WITHOUT a path — the file is found through `TEXINPUTS`, which the caller
54
+ // sets. An unresolved `\input` does NOT crash pdflatex: it writes a line to the log and continues,
55
+ // meaning the PDF builds GREEN and simply stops checking for dangling `\ref`/`\cite`. So a miss in
56
+ // this wiring is indistinguishable from success right up until the day someone finds a `??` in the
57
+ // submitted paper.
58
+ //
59
+ // What's checked is the RESOLUTION, not the build: that's what `check-render.sh`'s
60
+ // `--print-venues` mode is for — it prints the chosen directory and compiles nothing.
61
+ //
62
+ // What these assertions do NOT prove: that the found file is correct, that pdflatex read it, and
63
+ // that the guard fired. That's caught by `paper-guards.tex` itself (it fails with
64
+ // `\PackageError`) — here it's only "a directory was found, and found in the right order."
65
+ {
66
+ const HERE = dirname(fileURLToPath(import.meta.url));
67
+ const CHECK = join(HERE, "check-render.sh");
68
+ const run = (args, opts = {}) =>
69
+ spawnSync("bash", [CHECK, ...args], { encoding: "utf8", ...opts });
70
+
71
+ // First half — on TODAY's tree, resolution must succeed IF THERE IS ANYTHING TO
72
+ // resolve.
73
+ //
74
+ // 🔴 THERE USED TO BE AN UNCONDITIONAL `assert.equal(status, 0)` HERE, AND THE MOVE BROKE IT —
75
+ // for a real reason, not by accident. It relied on the SURROUNDING tree: while the skill lived
76
+ // at the consumer, the second rung of the ladder won
77
+ // (`.claude/skills/submit-paper/references/venues/`). The package checkout has neither that nor
78
+ // the first rung — `venues/` arrives together with `submit-paper`, which hasn't moved yet. So
79
+ // the assertion wasn't checking resolution, it was checking for the presence of an unrelated
80
+ // directory nearby.
81
+ //
82
+ // The form now is: resolution must succeed IF AND ONLY IF at least one candidate exists on
83
+ // disk. This is still a real assertion — it fails if a candidate exists and the script didn't
84
+ // find it — but it stops requiring the existence of something this repository doesn't have yet.
85
+ // The absence of both candidates gets PRINTED, not swallowed: "the check found nothing to
86
+ // check" and "the check passed" have to look different.
87
+ const candidates = [
88
+ // rung 1 — the package
89
+ join(HERE, "..", "..", "venues", "paper-guards.tex"),
90
+ // rung 2 — the skill directory at the consumer
91
+ join(
92
+ HERE,
93
+ "..",
94
+ "submit-paper",
95
+ "references",
96
+ "venues",
97
+ "paper-guards.tex",
98
+ ),
99
+ ].filter((c) => existsSync(c));
100
+
101
+ const ok = run(["--print-venues"]);
102
+ if (candidates.length === 0) {
103
+ assert.notEqual(
104
+ ok.status,
105
+ 0,
106
+ `there is NO paper-guards.tex on disk at all, but --print-venues returned 0 and printed ` +
107
+ `${JSON.stringify(ok.stdout.trim())}. Resolution succeeding out of nothing is exactly the ` +
108
+ `silent success this whole block was written to catch.`,
109
+ );
110
+ console.log(
111
+ ` --print-venues: NOT VERIFIED on the live tree — paper-guards.tex is absent from both ` +
112
+ `the package (venues/) and next to it (submit-paper/references/venues/). It arrives with ` +
113
+ `\`submit-paper\`; until then only the fixtures below hold this half.`,
114
+ );
115
+ } else {
116
+ assert.equal(
117
+ ok.status,
118
+ 0,
119
+ `a candidate exists on disk (${candidates[0]}), so --print-venues must resolve:\n${ok.stderr}`,
120
+ );
121
+ assert.ok(
122
+ existsSync(join(ok.stdout.trim(), "paper-guards.tex")),
123
+ `--print-venues printed ${ok.stdout.trim()}, but paper-guards.tex isn't there`,
124
+ );
125
+ }
126
+
127
+ // First half, part two — RESOLUTION DOES NOT DEPEND ON THE CURRENT DIRECTORY, and this case is
128
+ // written against a real regression from 09-12, not invented. `check-render.sh` itself `cd`s
129
+ // into the paper's directory BEFORE it looks for venues, and `gates.harness.mjs` calls it
130
+ // against a temporary fixture with a trimmed environment. So `git rev-parse --show-toplevel`
131
+ // runs in a temp directory, and `CLAUDE_PROJECT_DIR` isn't set — so the ladder found nothing,
132
+ // stopping a build on a test that had passed the day before. The anchor became the SCRIPT'S OWN
133
+ // directory: it depends on neither the cwd nor whether the caller sits inside a git tree.
134
+ //
135
+ // ⚠️ Like the first half, this case requires that at least one `paper-guards.tex` exists on
136
+ // disk: it checks that resolution doesn't depend on cwd, not that the file exists. With no
137
+ // candidate there's nothing to check, and that gets printed, not swallowed.
138
+ if (candidates.length > 0) {
139
+ const outside = realpathSync(mkdtempSync(join(tmpdir(), "venues-cwd-")));
140
+ const env = { ...process.env };
141
+ delete env.CLAUDE_PROJECT_DIR;
142
+ const r = run(["--print-venues"], { cwd: outside, env });
143
+ assert.equal(
144
+ r.status,
145
+ 0,
146
+ `resolution must work outside a git tree and without CLAUDE_PROJECT_DIR:\n${r.stderr}`,
147
+ );
148
+ assert.ok(
149
+ existsSync(join(r.stdout.trim(), "paper-guards.tex")),
150
+ `outside the repository, printed ${r.stdout.trim()}, but paper-guards.tex isn't there`,
151
+ );
152
+ } else {
153
+ console.log(
154
+ ` --print-venues (outside a git tree): NOT VERIFIED — no candidate on disk, see above.`,
155
+ );
156
+ }
157
+
158
+ // Second half — and it's the load-bearing one, because the silent failure lives right here.
159
+ // When the file is NOWHERE, the script must exit with code 2 and say why. A silent success here
160
+ // is exactly the defect the whole ladder was written for.
161
+ //
162
+ // 🔴 RUN AGAINST A COPY OF THE SCRIPT, AND THIS IS NOT A TEST WORKAROUND, IT'S A CONSEQUENCE OF
163
+ // THE MOVE (09-12, wave ③). Before `submit-paper` moved, "nowhere" was reproduced by an empty
164
+ // consumer root: both former rungs looked outward (the package name from cwd · `.claude/` under
165
+ // the root), and an empty directory found nothing at either. Now the script has a rung anchored
166
+ // to ITS OWN directory (`$SELF_DIR/../submit-paper/references/venues`) — added for
167
+ // cwd-independence — so as long as the script sits inside the package, venues next to it exist
168
+ // ALWAYS, and "nowhere" can no longer be reached through the environment. The only honest way to
169
+ // demonstrate that state is to move the script itself out of the package. The property being
170
+ // checked is otherwise identical, word for word: nothing resolved ⇒ exit 2 + the name of the
171
+ // missing file. ⚠️ What this form no longer checks: that an empty consumer root won't make the
172
+ // script lie — and it shouldn't, because such a root is now legitimately shadowed by the copy in
173
+ // the package.
174
+ const empty = realpathSync(mkdtempSync(join(tmpdir(), "venues-none-")));
175
+ const lonely = join(empty, "check-render.sh");
176
+ copyFileSync(CHECK, lonely);
177
+ const none = spawnSync("bash", [lonely, "--print-venues"], {
178
+ encoding: "utf8",
179
+ cwd: empty, // outside a git tree ⇒ the package doesn't resolve
180
+ env: { ...process.env, CLAUDE_PROJECT_DIR: empty },
181
+ });
182
+ assert.equal(
183
+ none.status,
184
+ 2,
185
+ "no venues anywhere ⇒ exit 2, not a silent success",
186
+ );
187
+ assert.match(
188
+ none.stderr,
189
+ /paper-guards\.tex/,
190
+ "the failure message must name the missing file",
191
+ );
192
+
193
+ // Third half — the ORDER of the rungs. Not cosmetic: on the day of the move, both rungs will be
194
+ // true at once, and the package has to win, otherwise the consumer silently keeps building with
195
+ // their old copy. Plant BOTH candidates and see which one is chosen.
196
+ const both = realpathSync(mkdtempSync(join(tmpdir(), "venues-both-")));
197
+ const pkgVenues = join(both, "node_modules", "paperlint", "venues");
198
+ const skillVenues = join(
199
+ both,
200
+ ".claude",
201
+ "skills",
202
+ "submit-paper",
203
+ "references",
204
+ "venues",
205
+ );
206
+ for (const d of [pkgVenues, skillVenues]) mkdirSync(d, { recursive: true });
207
+ writeFileSync(
208
+ join(both, "node_modules", "paperlint", "package.json"),
209
+ JSON.stringify({
210
+ name: "paperlint",
211
+ version: "0.0.0",
212
+ exports: { "./venues/*": "./venues/*" },
213
+ }),
214
+ );
215
+ writeFileSync(join(pkgVenues, "paper-guards.tex"), "% from package\n");
216
+ writeFileSync(join(skillVenues, "paper-guards.tex"), "% from skill dir\n");
217
+ const race = run(["--print-venues"], {
218
+ cwd: both,
219
+ env: { ...process.env, CLAUDE_PROJECT_DIR: both },
220
+ });
221
+ assert.equal(
222
+ race.status,
223
+ 0,
224
+ `both candidates are in place ⇒ resolution must succeed:\n${race.stderr}`,
225
+ );
226
+ assert.equal(
227
+ race.stdout.trim(),
228
+ pkgVenues,
229
+ "with both candidates present, the PACKAGE wins, not the skill directory",
230
+ );
231
+ }
232
+
233
+ // ── THE SAME LADDER IN THE PAPER's build.sh, AND WHY THE ASSERTION DIFFERS ───
234
+ //
235
+ // Before 2026-09-12 the paper's build script held a HARDCODED
236
+ // `export TEXINPUTS="$ROOT/.claude/skills/submit-paper/references/venues:"` — an address inside
237
+ // the skills directory that, after their move into the package, points into a void, and again
238
+ // SILENTLY: LaTeX's reaction to a missing \input is silence, not an error.
239
+ //
240
+ // The `--print-venues` trick can't be repeated here: build.sh has no mode that prints the
241
+ // decision without running pdflatex, and adding one just for the test means reshaping the
242
+ // paper's build script around the test. So the assertion is STRUCTURAL, and it deliberately
243
+ // anchors on the EXECUTABLE line, not on a substring anywhere: the file quotes the old path in a
244
+ // comment explaining the fix, and a search for "is this string present" would have caught the
245
+ // explanation instead of the defect.
246
+ //
247
+ // 🔴 THE SUBJECT OF THIS CHECK IS SOMEONE ELSE'S FILE, AND THAT SHAPES ITS WHOLE FORM. Before the
248
+ // move into the package, this block read ONE hardcoded path into the first consumer's private
249
+ // corpus. In the package no such path exists or can exist, so the property was pulled out into a
250
+ // pure function and is proven TWICE, per the "both halves" rule:
251
+ //
252
+ // 1. on FIXTURES — `build-clean.sh` must pass, `build-defect.sh` must fail. This is the only
253
+ // half that works in a checkout of the package itself, where there are no papers at all.
254
+ // Without it, the check in the package would be vacuous and would print a green zero;
255
+ // 2. on the consumer's REAL papers — via the `papers` carrier, not a path in code. There can be
256
+ // many consumers, each names the directory differently, and that carrier exists exactly for
257
+ // this.
258
+ //
259
+ // The counter below is printed ALWAYS and grows together with the verdict, not before it: "0
260
+ // consumer scripts" is an honest result (in a checkout of the package itself it should be zero),
261
+ // whereas a silently-produced "0" would be indistinguishable from a verified corpus.
262
+ {
263
+ const HERE = dirname(fileURLToPath(import.meta.url));
264
+
265
+ /** The property of a paper's build script. A pure function — so it can also be BROKEN. */
266
+ const assertLadder = (build, where) => {
267
+ assert.doesNotMatch(
268
+ build,
269
+ /^\s*export\s+TEXINPUTS="\$ROOT\/\.claude\//m,
270
+ `${where}: is assigning TEXINPUTS the hardcoded skills-directory path again`,
271
+ );
272
+ assert.match(
273
+ build,
274
+ /^\s*export\s+TEXINPUTS="\$VENUES_DIR:"/m,
275
+ `${where}: must set TEXINPUTS from the resolved VENUES_DIR`,
276
+ );
277
+ assert.match(
278
+ build,
279
+ /require\.resolve\("paperlint\/venues\/paper-guards\.tex"\)/,
280
+ `${where}: the ladder's first rung (the package) must remain`,
281
+ );
282
+ assert.match(
283
+ build,
284
+ /^\s*kpsewhich paper-guards\.tex/m,
285
+ `${where}: after setting TEXINPUTS, must CONFIRM that LaTeX can see the file`,
286
+ );
287
+ };
288
+
289
+ // ── First half: fixtures. Both sides, otherwise "silent" is indistinguishable from "dead."
290
+ const fx = join(HERE, "..", "..", "fixtures", "render-paper");
291
+ assertLadder(
292
+ readFileSync(join(fx, "build-clean.sh"), "utf8"),
293
+ "fixture build-clean.sh",
294
+ );
295
+ assert.throws(
296
+ () =>
297
+ assertLadder(
298
+ readFileSync(join(fx, "build-defect.sh"), "utf8"),
299
+ "fixture build-defect.sh",
300
+ ),
301
+ /TEXINPUTS/,
302
+ "the defective fixture must fail the check — otherwise the check isn't checking anything",
303
+ );
304
+
305
+ // ── Second half: the consumer's real papers, addressed via the `papers` carrier.
306
+ // `papersRoot()` THROWS when the directory doesn't exist; in a checkout of the package itself
307
+ // it doesn't, and that's not an error, it's the absence of a consumer. Catch exactly that case
308
+ // and say so out loud.
309
+ const root = (() => {
310
+ try {
311
+ return papersRoot(consumerPkg(), consumerRoot());
312
+ } catch {
313
+ return null;
314
+ }
315
+ })();
316
+
317
+ let checked = 0;
318
+ if (root) {
319
+ const base = join(consumerRoot(), root);
320
+ for (const entry of readdirSync(base, { withFileTypes: true })) {
321
+ if (!entry.isDirectory()) continue;
322
+ const script = join(base, entry.name, "build.sh");
323
+ if (!existsSync(script)) continue;
324
+ // The count grows TOGETHER WITH the verdict, not ahead of it.
325
+ assertLadder(
326
+ readFileSync(script, "utf8"),
327
+ `${root}/${entry.name}/build.sh`,
328
+ );
329
+ checked += 1;
330
+ }
331
+ }
332
+
333
+ console.log(
334
+ ` build.sh ladder: 2 fixture(s) + ${checked} consumer script(s)` +
335
+ (root === null
336
+ ? " (no papers root on disk — package checkout)"
337
+ : ` under ${root}/`),
338
+ );
339
+ }