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,623 @@
1
+ // ledger.mjs — the pipeline's memory. One append-only file, and everything else is derived.
2
+ //
3
+ // WHY THIS EXISTS. Until 2026-08-07 the pipeline's state lived in a hand-maintained markdown
4
+ // table (`PIPELINE-STATUS.md`) whose rows a human ticked. It lied constantly and in one
5
+ // direction: a gate marked ā˜‘ had been run against text that was later rewritten, and nothing
6
+ // noticed. On the day paper #3 shipped, the author found defects that every green gate had
7
+ // passed over — because the green was about a document that no longer existed.
8
+ //
9
+ // Occupancy research (2026-08-06, `paper-pipeline/references/occupancy-2026-08-06-*`) said this
10
+ // is a solved problem and named the solution: content-addressed build systems. `make` is not
11
+ // enough — it keys on timestamps and has NO notion of the recipe changing, which is exactly our
12
+ // bug, because editing a checker leaves its old verdict looking fresh. Nix and Bazel both fold
13
+ // the builder's own source into the cache key. So do we.
14
+ //
15
+ // THREE PROPERTIES, and each one is a defect we actually shipped:
16
+ //
17
+ // 1. STALENESS IS COMPUTED, NEVER DECLARED. A run is fresh only if the paper's bytes hash to
18
+ // what they hashed when the gate ran. No human ticks a box.
19
+ // 2. THE CHECKER'S OWN SOURCE IS PART OF THE KEY. Improve a skill and its old verdicts go
20
+ // stale, because they were verdicts of the old skill.
21
+ // 3. A GATE THAT HAS NEVER FOUND ANYTHING IS REPORTED AS SUSPECT. Borrowed from mutation
22
+ // testing: a test that kills no mutant is not a test. We shipped a tightening pass that
23
+ // returned KEEP on 80 of 81 sections and a justification pass that never once recommended
24
+ // deletion. Both "ran". Neither could have said no.
25
+ //
26
+ // NOT A BUILD SYSTEM. It does not run anything or decide what to run next. It records what ran,
27
+ // against what, and what it said — the one thing nobody was doing.
28
+ //
29
+ // ════════════════════════════════════════════════════════════════════════════════════════════
30
+ // 2026-08-10 — THERE IS NO `PASS` CONSTRUCTOR, AND THE ROW KEY IS THE CHECK
31
+ // ════════════════════════════════════════════════════════════════════════════════════════════
32
+ //
33
+ // FIVE FAILURES were observed on this pipeline. Three of them were defects of the CHECKING
34
+ // APPARATUS rather than of any manuscript, and all three are the same defect wearing three coats:
35
+ //
36
+ // 1. a gate was recorded as PASS while the input it needs did not exist;
37
+ // 2. a check that had never once returned a negative counted as a working check;
38
+ // 3. five model reviewers from one vendor agreeing was recorded as an acquittal.
39
+ //
40
+ // Each is a stored value meaning "nothing was wrong" — written by a process that had no evidence
41
+ // for it, and thereafter indistinguishable from one that did. So the value is gone. A run may
42
+ // record exactly two things:
43
+ //
44
+ // FINDING — something is wrong. MUST carry a path to the evidence. Refusing to record it
45
+ // would destroy the finding, so the row is written first and the command fails
46
+ // afterwards (see the CLI at the bottom; that ordering is load-bearing).
47
+ // ABSTAINED — no judgement was produced. MUST carry a reason from a closed vocabulary, so
48
+ // "I ran and found nothing" and "I could not run" are different rows, not
49
+ // different sentences in a note nobody parses.
50
+ //
51
+ // "Nothing was wrong" is not a value that can be written down. It is the ABSENCE of findings,
52
+ // derived at read time by whoever is asking, who then owns the inference.
53
+ //
54
+ // ── PRIOR ART, stated honestly because both halves of it cut against us ──────────────────────
55
+ //
56
+ // SARIF (OASIS Standard 2.1.0 §3.27.9) defines `kind: "pass"` verbatim as "The rule … was
57
+ // evaluated, and no problem was found", and the only REQUIRED property of a `result` object is
58
+ // `message` — `locations` is optional. So a conformant SARIF acquittal can name no place, no
59
+ // span and no artifact at all. That is the stored justification we delete.
60
+ //
61
+ // šŸ”“ What must NOT be said, here or in any paper: that industry lacks abstention. It does not.
62
+ // `kind: "open"` sits in the same paragraph — "the tool concluded that there was insufficient
63
+ // information to decide whether a problem exists" — and its NOTE 1 draws exactly the trichotomy
64
+ // we build on. Our position is therefore "DELETE PASS, leaving abstain as the only unwitnessed
65
+ // outcome", never "add abstain". A reviewer opens §3.27.9 and finds `open` in ten seconds.
66
+ // (Established [A] against the OASIS text and its JSON schema:
67
+ // `the author's private research notes` §5.)
68
+ //
69
+ // The ancestor is `Certifying algorithms` — McConnell, Mehlhorn, NƤher & Schweitzer, Computer
70
+ // Science Review 5(2):119–161, 2011 — where BOTH answers carry a witness: "bipartite" ships a
71
+ // 2-colouring, "not bipartite" ships an odd cycle. šŸ”“ Our discipline is STRICTLY WEAKER than
72
+ // theirs and the code must say so rather than borrow their credit: we witness only the POSITIVE
73
+ // answer. We have no witness for "nothing is wrong", which is precisely why we do not offer a
74
+ // constructor that asserts it — the honest name for a completed clean run is `no-witness`, an
75
+ // abstention, and it is not a pass with better manners.
76
+ //
77
+ // ── THE SECOND DEFECT, FIXED IN THE SAME PASS: the row key ───────────────────────────────────
78
+ //
79
+ // `status()` reduces a key's rows with `runs[runs.length - 1]` — last row wins. That is correct
80
+ // for repeated runs of ONE check and catastrophic across two. The unit that produces a verdict
81
+ // is the CHECK, not the skill: two mechanical checks filed under one skill name took turns being
82
+ // the answer, so a clean run of the second erased a finding of the first from every derived view
83
+ // while the ledger file itself still honestly held both lines.
84
+ //
85
+ // The key is now `skill` + `check`, rendered `skill/check` (or just `skill` when a skill has a
86
+ // single check). `check-provenance`, `arm-permutation` and `delivered-pdf` all belong to
87
+ // `build-benchmark` and now hold three separate rows that cannot overwrite one another.
88
+
89
+ import { createHash } from "node:crypto";
90
+ import {
91
+ readFileSync,
92
+ appendFileSync,
93
+ existsSync,
94
+ mkdirSync,
95
+ statSync,
96
+ readdirSync,
97
+ } from "node:fs";
98
+ import { join, dirname, resolve, basename } from "node:path";
99
+ import { fileURLToPath } from "node:url";
100
+ import { consumerSkillsDir, isMain, ledgerPath } from "./consumer.mjs";
101
+
102
+ const HERE = dirname(fileURLToPath(import.meta.url));
103
+
104
+ // šŸ”“ Overridable so a test never touches real history. The first version of the selftest wrote to
105
+ // the production ledger and restored it afterwards — which works right up until the run crashes
106
+ // between the two, and then fixture rows are in the record permanently. That is not hypothetical:
107
+ // two `my-paper` rows survived into the working tree on 2026-08-07, hours after this shipped.
108
+ // Save-and-restore is not isolation. A separate file is.
109
+ //
110
+ // šŸ”“ THREE RUNGS SINCE 2026-09-12, AND THE THIRD ONE THROWS. `join(HERE, 'runs.jsonl')` was
111
+ // the whole resolution while this file lived in the repository it serves. Once the mechanism moved
112
+ // into a package, `HERE` is inside `node_modules` — a directory `npm ci` deletes. Appending there
113
+ // SUCCEEDS, so rows would look recorded right up until the next install removed them, with no
114
+ // error at any point: the exact stored-untruth this ledger exists to abolish, arriving through its
115
+ // own front door. `ledgerPath()` refuses that path and names the key to declare instead; the
116
+ // environment variable above is unchanged and still outranks everything.
117
+ export const LEDGER = ledgerPath(HERE);
118
+ // šŸ”“ `resolve(HERE, '..', 'skills')` until 2026-08-15, correct while this lived in
119
+ // `.claude/pipeline/` and silently wrong the moment it moved into the skill it serves: from
120
+ // `.claude/skills/paper-pipeline/scripts/` that resolves to `…/paper-pipeline/skills`, which does
121
+ // not exist. And the failure is SILENT in the worst direction — `skillHash()` over a missing
122
+ // directory is stable, so every recorded verdict reads FRESH forever and staleness detection is
123
+ // dead without a single error. Caught only because `ledger.selftest.mjs` asserts the transition.
124
+ //
125
+ // šŸ”“ AND IT BROKE A SECOND TIME, THE SAME WAY, ON 2026-09-12 — which is why it is no longer a
126
+ // walk at all. The skills being hashed are the CONSUMER's: `skillHash()` answers "has the checker
127
+ // changed since it rendered this verdict", and the checker is a SKILL.md in the repository under
128
+ // test. From inside `node_modules` two `..` land on the package's own skills directory, which
129
+ // holds no consumer skill — and a hash over a missing directory is stable, i.e. every verdict
130
+ // reads FRESH forever. Same silent direction as last time, so the anchor is now the consumer root
131
+ // rather than a distance from this file.
132
+ const SKILLS = consumerSkillsDir();
133
+
134
+ /**
135
+ * The two things a run may record. There is no third, and in particular there is no positive.
136
+ *
137
+ * A gate that can only ABSTAIN is not a gate — that property is asserted against every SKILL.md
138
+ * in `skill-checks.mjs` (assertion 10), because it cannot be observed at runtime until the gate
139
+ * has run, and by then a paper has shipped.
140
+ */
141
+ export const KINDS = ["FINDING", "ABSTAINED"];
142
+
143
+ /**
144
+ * Why a run produced no finding. CLOSED, because the whole point of deleting `PASS` is to stop
145
+ * one word covering several unrelated states — and a free-text reason is that same word with
146
+ * more characters.
147
+ *
148
+ * šŸ”“ `no-witness` IS NOT A PASS. It says: this check ran to completion and produced no finding,
149
+ * and — unlike a certifying algorithm (McConnell et al. 2011) — it has no witness for the
150
+ * negative answer and therefore does not assert one. Everything downstream must render it as an
151
+ * abstention. `status.mjs` asserts, in its own harness, that its output contains no word meaning
152
+ * "passed".
153
+ */
154
+ export const ABSTENTIONS = new Map([
155
+ [
156
+ "started",
157
+ "the run has begun and has not reported yet (written by announce.mjs)",
158
+ ],
159
+ [
160
+ "no-witness",
161
+ "ran to completion, produced no finding — NOT a claim that nothing is wrong",
162
+ ],
163
+ ["input-missing", "could not run: the thing it reads is not there"],
164
+ ["blocked", "a required upstream gate has not produced its input"],
165
+ ["crashed", "the check itself errored out"],
166
+ ]);
167
+
168
+ /** The retired vocabulary, kept BY NAME so a caller reaching for it gets an explanation. */
169
+ export const RETIRED = new Map([
170
+ [
171
+ "PASS",
172
+ 'deleted: it stored "nothing was wrong" as a value. Record ABSTAINED `no-witness`, and let the reader derive cleanliness from the absence of findings.',
173
+ ],
174
+ [
175
+ "FINDINGS",
176
+ "renamed FINDING (one row is one check, and it either found something or it did not).",
177
+ ],
178
+ [
179
+ "FAIL",
180
+ "is a FINDING. Blockingness is a property of the finding — pass `blocking: true` — not a second constructor.",
181
+ ],
182
+ ["ABSENT", "is ABSTAINED with reason `input-missing`."],
183
+ [
184
+ "ERROR",
185
+ "is ABSTAINED with reason `crashed` (or `started`, for an announce row).",
186
+ ],
187
+ ]);
188
+
189
+ const sha = (buf) =>
190
+ createHash("sha256").update(buf).digest("hex").slice(0, 16);
191
+
192
+ /** Hash of the document under test. Missing file is not an error — it is a distinct state. */
193
+ export function inputHash(paperDir) {
194
+ const p = join(paperDir, "paper.md");
195
+ if (!existsSync(p)) return null;
196
+ return sha(readFileSync(p));
197
+ }
198
+
199
+ /**
200
+ * Hash of the CHECKER ITSELF — its SKILL.md plus any scripts it ships. This is the property
201
+ * `make` lacks and the reason its staleness answers cannot be trusted. Rewrite a skill's
202
+ * instructions and every verdict it ever gave becomes a verdict about a tool that is gone.
203
+ */
204
+ export function skillHash(skill) {
205
+ const dir = join(SKILLS, skill);
206
+ if (!existsSync(dir)) return null;
207
+ const parts = [];
208
+ const walk = (d) => {
209
+ for (const e of readdirSorted(d)) {
210
+ const full = join(d, e);
211
+ const st = statSync(full);
212
+ if (st.isDirectory()) {
213
+ if (e !== "fixtures" && e !== "node_modules") walk(full);
214
+ } else if (/\.(md|mjs|js|sh|py|ts)$/.test(e))
215
+ parts.push(e + ":" + sha(readFileSync(full)));
216
+ }
217
+ };
218
+ walk(dir);
219
+ return sha(parts.join("\n"));
220
+ }
221
+
222
+ function readdirSorted(d) {
223
+ // Sorted so the hash is stable across filesystems. An unstable key would make every run look
224
+ // stale on another machine, and a check that cries wolf is a check that gets muted.
225
+ return readdirSync(d).sort();
226
+ }
227
+
228
+ // ── the key ───────────────────────────────────────────────────────────────────────────────────
229
+
230
+ /** The row key of a ledger row: `skill` for a single-check skill, `skill/check` otherwise. */
231
+ export const rowKey = (r) =>
232
+ r.check && r.check !== r.skill ? `${r.skill}/${r.check}` : r.skill;
233
+
234
+ /** Split a gate identifier (`skill` or `skill/check`) into its parts. Inverse of `rowKey`. */
235
+ export function parseGate(g) {
236
+ const [skill, check] = String(g).split("/");
237
+ return { key: String(g), skill, check: check || skill };
238
+ }
239
+
240
+ // ── writing ───────────────────────────────────────────────────────────────────────────────────
241
+
242
+ /**
243
+ * Record one run. Called by the skill itself, at the END, with what it produced.
244
+ *
245
+ * Throws BEFORE writing when the vocabulary is wrong (nothing is lost — there is no evidence in
246
+ * a malformed call), and AFTER writing when a FINDING's evidence is missing (the finding must
247
+ * survive; see the CLI comment). An error thrown after the write carries `err.recorded`.
248
+ */
249
+ export function record({
250
+ skill,
251
+ check = skill,
252
+ paper,
253
+ kind,
254
+ findings = 0,
255
+ report = null,
256
+ reason = null,
257
+ note = null,
258
+ blocking = false,
259
+ }) {
260
+ if (RETIRED.has(kind)) {
261
+ throw new Error(
262
+ `"${kind}" is not a verdict any more — ${RETIRED.get(kind)}\n` +
263
+ ` Record one of ${KINDS.join("|")}. Reasons for ABSTAINED: ${[...ABSTENTIONS.keys()].join("|")}.`,
264
+ );
265
+ }
266
+ if (!KINDS.includes(kind)) {
267
+ throw new Error(
268
+ `kind must be one of ${KINDS.join("|")}, got ${JSON.stringify(kind)}`,
269
+ );
270
+ }
271
+ if (kind === "ABSTAINED" && !ABSTENTIONS.has(reason)) {
272
+ throw new Error(
273
+ `ABSTAINED needs a reason from ${[...ABSTENTIONS.keys()].join("|")}, got ${JSON.stringify(reason)}.\n` +
274
+ ` A free-text reason is "PASS" with more characters: the point of the closed set is that\n` +
275
+ ` "ran and found nothing" and "could not run" stop being the same row.`,
276
+ );
277
+ }
278
+ if (kind === "FINDING" && !(Number(findings) >= 1)) {
279
+ throw new Error(
280
+ `FINDING with findings=${JSON.stringify(findings)}. A finding of nothing is an abstention:\n` +
281
+ ` record ABSTAINED no-witness instead. Nothing is lost by refusing here — there is no\n` +
282
+ ` evidence in a count of zero.`,
283
+ );
284
+ }
285
+
286
+ const paperDir = resolve(paper);
287
+ const row = {
288
+ ts: new Date().toISOString(),
289
+ skill,
290
+ check,
291
+ paper: basename(paperDir),
292
+ inputSha: inputHash(paperDir),
293
+ skillSha: skillHash(skill),
294
+ kind,
295
+ findings: kind === "FINDING" ? Number(findings) : 0,
296
+ blocking: kind === "FINDING" ? Boolean(blocking) : false,
297
+ reason,
298
+ report,
299
+ note,
300
+ };
301
+ mkdirSync(dirname(LEDGER), { recursive: true });
302
+ appendFileSync(LEDGER, JSON.stringify(row) + "\n");
303
+
304
+ if (kind === "FINDING") {
305
+ const problem = evidenceProblem(row, paperDir);
306
+ if (problem) {
307
+ const e = new Error(problem);
308
+ e.recorded = row; // the row IS on disk; only the run failed
309
+ throw e;
310
+ }
311
+ }
312
+ return row;
313
+ }
314
+
315
+ /**
316
+ * šŸ”“ THE EVIDENCE CONTRACT. A FINDING must point at something a reader can open, and if that
317
+ * something states its own count the two must agree.
318
+ *
319
+ * This is where an output contract can actually be enforced, and the reason is structural: a
320
+ * skill is prose and returns nothing to a runtime, so there is no `return skill({schema})` to
321
+ * typecheck. What there IS, is a choke point — every skill must call `record` or its row never
322
+ * reaches the status table and the gate reads NEVER-RUN, which is loud. Anything checked here
323
+ * cannot be skipped, and nothing checked anywhere else has that property.
324
+ *
325
+ * Graduated on purpose, the gradual-typing shape rather than a flag day:
326
+ * no path at all -> HARD. The row survives (see `record`); the RUN fails.
327
+ * path points at nothing -> HARD. Worse than no path: it reads as evidence and sends the
328
+ * next reader into a 404.
329
+ * report states no count -> nudge only. Reports written before this convention are fine;
330
+ * demanding a field they lack would get the check disabled on
331
+ * day one.
332
+ * report states a DIFFERENT -> HARD. Two numbers that disagree mean one of them is already
333
+ * count wrong, and silence picks the wrong one at random.
334
+ */
335
+ function evidenceProblem(row, paperDir) {
336
+ if (!row.report) {
337
+ return (
338
+ `${row.skill}: recorded ${row.findings} finding(s) with NO report path.\n` +
339
+ ` The row is saved, so nothing is lost — but a count with no location is a rumour, and\n` +
340
+ ` next session will re-derive it from scratch. Re-run with the report path last:\n` +
341
+ ` node .claude/skills/paper-pipeline/scripts/ledger.mjs record ${row.skill} <paper-dir> FINDING ${row.findings} <report-path>`
342
+ );
343
+ }
344
+ const reportPath = resolve(paperDir, row.report);
345
+ if (!existsSync(reportPath)) {
346
+ return (
347
+ `${row.skill}: recorded report path "${row.report}" — nothing is there.\n` +
348
+ ` Resolved to ${reportPath}\n` +
349
+ ` A path that points at nothing is worse than no path: the status table shows it as\n` +
350
+ ` evidence and the next reader follows it into a 404. Write the report, then record.`
351
+ );
352
+ }
353
+ const stated = /^findings:\s*(\d+)\s*$/m.exec(
354
+ readFileSync(reportPath, "utf8"),
355
+ );
356
+ if (!stated) {
357
+ console.error(
358
+ `\nāš ļø ${row.skill}: ${row.report} does not state its own finding count.\n` +
359
+ ` Add \`findings: ${row.findings}\` to its frontmatter and the ledger and the report can\n` +
360
+ ` be checked against each other. Without it, the count here is unverifiable prose.`,
361
+ );
362
+ return null;
363
+ }
364
+ if (Number(stated[1]) !== Number(row.findings)) {
365
+ return (
366
+ `${row.skill}: the ledger says ${row.findings} finding(s), ${row.report} says ${stated[1]}.\n` +
367
+ ` One of the two is already wrong and nothing here can tell which. Fix the report or the\n` +
368
+ ` recorded count so they agree, then record again.`
369
+ );
370
+ }
371
+ return null;
372
+ }
373
+
374
+ // ── reading ───────────────────────────────────────────────────────────────────────────────────
375
+
376
+ /**
377
+ * šŸ”“ HOW ROWS FROM THE OLD VOCABULARY ARE TREATED, decided deliberately and loudly.
378
+ *
379
+ * `runs.jsonl` is append-only and holds real rows carrying `PASS`, `FINDINGS`, `FAIL`, `ABSENT`
380
+ * and `ERROR`. The file is NOT rewritten — rewriting history to make it agree with today's
381
+ * design is the same move as ticking a box in the old status table.
382
+ *
383
+ * Nor are those rows TRANSLATED into the new constructors. A silent reinterpretation is exactly
384
+ * the class of defect this refactor exists to remove: were an old `PASS` mapped onto
385
+ * `ABSTAINED no-witness`, a row asserting "nothing was wrong" would come back out of the reader
386
+ * wearing the new vocabulary's honesty without having earned it.
387
+ *
388
+ * So a legacy row keeps its own kind, `LEGACY`, and its original word verbatim in
389
+ * `legacyVerdict`. `LEGACY` is deliberately NOT in `KINDS`: it can be read and can never be
390
+ * written. Downstream:
391
+ *
392
+ * • it counts as a RUN (someone did run something, and pretending otherwise is its own lie);
393
+ * • a legacy `FINDINGS`/`FAIL` row still surfaces its count and report path, because those
394
+ * rows carry evidence and discarding evidence is the destructive direction;
395
+ * • a legacy `PASS`/`ABSENT`/`ERROR` row carries nothing forward. It is displayed as what it
396
+ * is — a word from a vocabulary we retired — and satisfies nothing.
397
+ */
398
+ export const LEGACY_KIND = "LEGACY";
399
+
400
+ function normalise(row) {
401
+ if (row.kind) return { ...row, check: row.check || row.skill };
402
+ return {
403
+ ...row,
404
+ check: row.check || row.skill,
405
+ kind: LEGACY_KIND,
406
+ legacyVerdict: row.verdict ?? null,
407
+ // Only the two negatives carried anything. Everything else is a word, not a fact.
408
+ findings:
409
+ row.verdict === "FINDINGS" || row.verdict === "FAIL"
410
+ ? Number(row.findings ?? 0)
411
+ : 0,
412
+ blocking: row.verdict === "FAIL",
413
+ reason: null,
414
+ };
415
+ }
416
+
417
+ /** Does this row assert that something is wrong? Legacy negatives count; legacy `PASS` does not. */
418
+ export const isFinding = (r) =>
419
+ r.kind === "FINDING" ||
420
+ (r.kind === LEGACY_KIND &&
421
+ (r.legacyVerdict === "FINDINGS" || r.legacyVerdict === "FAIL"));
422
+
423
+ export function readLedger() {
424
+ if (!existsSync(LEDGER)) return [];
425
+ return readFileSync(LEDGER, "utf8")
426
+ .split("\n")
427
+ .filter(Boolean)
428
+ .map((l) => {
429
+ try {
430
+ return normalise(JSON.parse(l));
431
+ } catch {
432
+ return null;
433
+ }
434
+ })
435
+ .filter(Boolean);
436
+ }
437
+
438
+ /**
439
+ * The most recent run of one key — by TIMESTAMP, not by position in the file.
440
+ *
441
+ * šŸ”“ WHY THIS IS NOT `runs[runs.length - 1]`, WHICH IS WHAT IT USED TO BE. The comment at the
442
+ * top of this file works out that "last row wins" is right for repeated runs of one check, and
443
+ * that is still the rule here. What it assumed without saying is that FILE ORDER EQUALS TIME
444
+ * ORDER. That holds for an append-only file with one writer, and it stops holding the moment two
445
+ * branches both append and git merges them: `.gitattributes` in the consumer declares
446
+ * `merge=union`, which concatenates "ours, then theirs" and orders nothing.
447
+ *
448
+ * Measured 2026-09-15 (external review, P1), two rows of one check:
449
+ *
450
+ * by position in file ABSTAINED findings=0 2026-09-14
451
+ * by timestamp FINDING findings=3 2026-09-15
452
+ *
453
+ * So a merge that lands an older row after a newer one HIDES the finding and reports a clean
454
+ * state — a substituted verdict, not a cosmetic reordering of a log.
455
+ *
456
+ * āš ļø THE FIX BELONGS TO THE READER, AND THAT IS THE ARCHITECTURAL POINT. A merge driver that
457
+ * sorts by `ts` would close exactly one source of disorder and would have to be installed on
458
+ * every clone (`git config merge.<name>.driver`), which is the cost `union` exists to avoid.
459
+ * Disorder has other doors: a hand-edited ledger, two processes appending at once, a restored
460
+ * backup. Making the READER independent of order closes all of them at once, and after it the
461
+ * merge driver's behaviour stops mattering.
462
+ *
463
+ * Ties and missing stamps keep the OLD behaviour on purpose, so this change is a strict
464
+ * refinement rather than a new policy:
465
+ * - equal `ts` -> the later row in the file wins (two runs inside one second: file order is
466
+ * the only information there is, and it is the true order for one writer);
467
+ * - one row has no parsable `ts` -> the stamped row wins (a stamp is evidence, its absence is not);
468
+ * - neither has one -> file order, exactly as before.
469
+ */
470
+ function latestRun(runs) {
471
+ let best = null;
472
+ let bestTime = null;
473
+ for (const r of runs) {
474
+ const t = Date.parse(r.ts ?? "");
475
+ const time = Number.isFinite(t) ? t : null;
476
+ if (best === null) {
477
+ best = r;
478
+ bestTime = time;
479
+ continue;
480
+ }
481
+ if (time === null && bestTime === null) {
482
+ best = r;
483
+ bestTime = time;
484
+ continue;
485
+ }
486
+ if (time === null) continue;
487
+ if (bestTime === null || time >= bestTime) {
488
+ best = r;
489
+ bestTime = time;
490
+ }
491
+ }
492
+ return best;
493
+ }
494
+
495
+ /**
496
+ * The computed state of every CHECK for one paper. This REPLACES the hand-maintained table —
497
+ * it is not a second copy of it. Nothing here is declared; every field is derived from the
498
+ * ledger plus the current bytes on disk.
499
+ *
500
+ * šŸ”“ There is no `verdict` field, and that is the interface change. A caller that wants to know
501
+ * whether a check is clean must read `findings === 0` and own the inference, which is a sentence
502
+ * it has to write down rather than a value it can copy.
503
+ */
504
+ export function status(paperDir, { gates } = {}) {
505
+ const dir = resolve(paperDir);
506
+ const paper = basename(dir);
507
+ const nowInput = inputHash(dir);
508
+ const rows = readLedger().filter((r) => r.paper === paper);
509
+
510
+ const known = gates || [...new Set(rows.map(rowKey))];
511
+ return known.map((g) => {
512
+ const { key, skill, check } = parseGate(g);
513
+ const runs = rows.filter((r) => rowKey(r) === key);
514
+ const last = latestRun(runs);
515
+ const nowSkill = skillHash(skill);
516
+
517
+ let state;
518
+ if (!last) state = "NEVER-RUN";
519
+ else if (last.inputSha !== nowInput) state = "STALE-PAPER";
520
+ else if (last.skillSha !== nowSkill) state = "STALE-SKILL";
521
+ else state = "FRESH";
522
+
523
+ const found = last && isFinding(last);
524
+ return {
525
+ key,
526
+ skill,
527
+ check,
528
+ state,
529
+ runs: runs.length,
530
+ // The last run's finding, if it made one. Zero here means NO FINDING WAS RECORDED — it is
531
+ // not, and must never be rendered as, a claim that the paper is clean.
532
+ findings: found ? Number(last.findings ?? 0) : 0,
533
+ blocking: found ? Boolean(last.blocking) : false,
534
+ report: found ? (last.report ?? null) : null,
535
+ abstained:
536
+ last && last.kind === "ABSTAINED"
537
+ ? { reason: last.reason, note: last.note }
538
+ : null,
539
+ legacy: last && last.kind === LEGACY_KIND ? last.legacyVerdict : null,
540
+ ts: last?.ts ?? null,
541
+ // Mutation-testing borrow: a check with runs and no finding has never demonstrated it is
542
+ // able to produce one. Not proof of a broken check — a reason to go plant a defect.
543
+ everFound: runs.some(isFinding),
544
+ };
545
+ });
546
+ }
547
+
548
+ // ── CLI ───────────────────────────────────────────────────────────────────────────────────────
549
+
550
+ const USAGE = [
551
+ "usage: ledger.mjs record <skill> <paperDir> FINDING <count> <report-path> [--check=<id>] [--blocking]",
552
+ " ledger.mjs record <skill> <paperDir> ABSTAINED <reason> [note] [--check=<id>]",
553
+ " ledger.mjs status <paperDir>",
554
+ "",
555
+ ` reasons: ${[...ABSTENTIONS.keys()].join(" | ")}`,
556
+ " --blocking marks a finding that must stop the pass (a fact is wrong, a required predecessor",
557
+ " does not exist). It is a PROPERTY of the finding, not a second constructor: the old `FAIL`.",
558
+ " there is no PASS: a clean run is `ABSTAINED no-witness`, and cleanliness is derived by the",
559
+ " reader from the absence of findings, never stored as a value.",
560
+ ].join("\n");
561
+
562
+ if (isMain(import.meta.url)) {
563
+ // šŸ”“ EVERY FLAG THE USAGE TEXT ADVERTISES IS PARSED HERE, and that sentence is load-bearing
564
+ // because it was false for the first hour of this file's life: `--blocking` was written into the
565
+ // usage block and into all 22 SKILL.md files while nothing read it, so every skill instructing
566
+ // `--blocking` for its desk-reject case would have recorded an ordinary advisory finding and the
567
+ // status view would have shown a bullet where it should show šŸ”“. A documented flag that does
568
+ // nothing is this repository's oldest recurring defect — three dead hooks, a twelve-day-dead
569
+ // nudge — arriving inside the refactor written to end it. `gates.harness.mjs` block 14 asserts
570
+ // the flag from the outside, which is the only form of this that cannot rot.
571
+ const argv = process.argv.slice(2);
572
+ const flag = (name) => argv.some((a) => a === `--${name}`);
573
+ const valued = (name) =>
574
+ argv.find((a) => a.startsWith(`--${name}=`))?.slice(name.length + 3);
575
+ const [cmd, ...rest] = argv.filter((a) => !a.startsWith("--"));
576
+
577
+ if (cmd === "record") {
578
+ const [skill, paper, kind, a4, a5] = rest;
579
+ const check = valued("check") || skill;
580
+ const args =
581
+ kind === "FINDING"
582
+ ? {
583
+ kind,
584
+ findings: Number(a4 || 0),
585
+ report: a5 || null,
586
+ blocking: flag("blocking"),
587
+ }
588
+ : { kind, reason: a4 || null, note: a5 || null };
589
+
590
+ // šŸ”“ A FINDING WITH BROKEN EVIDENCE IS RECORDED, AND THEN THE COMMAND FAILS.
591
+ //
592
+ // Every SKILL.md instructs `record <skill> <dir> FINDING <count> <report-path>`. On
593
+ // 2026-08-07, surfacing open findings for the first time showed that four of six real runs
594
+ // had passed the count and omitted the path: build-benchmark 13, tighten-paper 9,
595
+ // grade-paper-writing 11, draft-paper 3 — thirty-six findings that are a number and nothing
596
+ // else, unrecoverable without rerunning the gate. The instruction was written, read, and not
597
+ // followed, which is this pipeline's own thesis landing on itself.
598
+ //
599
+ // The row is written FIRST and deliberately: refusing outright would destroy the finding,
600
+ // which is worse than storing it badly. What must not happen is the run reading as DONE. So
601
+ // the data survives and the command exits non-zero, which the author sees in the session
602
+ // rather than a week later in a status table. `record()` throws AFTER appending, and this
603
+ // catch is the half that turns that into an exit code.
604
+ let row;
605
+ try {
606
+ row = record({ skill, check, paper, ...args });
607
+ } catch (e) {
608
+ if (e.recorded) {
609
+ console.log(JSON.stringify(e.recorded));
610
+ console.error(`\nšŸ”“ ${e.message}`);
611
+ process.exit(1);
612
+ }
613
+ console.error(`\nšŸ”“ ${e.message}\n\n${USAGE}`);
614
+ process.exit(2);
615
+ }
616
+ console.log(JSON.stringify(row));
617
+ } else if (cmd === "status") {
618
+ console.log(JSON.stringify(status(rest[0] || "."), null, 2));
619
+ } else {
620
+ console.error(USAGE);
621
+ process.exit(2);
622
+ }
623
+ }