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,386 @@
1
+ /**
2
+ * markdown.mjs — markup parsing for EVERYTHING in this repo that reads markdown.
3
+ *
4
+ * Three kinds of consumer: two advisory linters (`kb-lint`, `paper-lint` — through the
5
+ * re-export from `lint-core.mjs`) and skill scripts (`tighten-paper/structure.mjs`,
6
+ * `grade-paper-writing/prose-lint.mjs`, `paper-pipeline/scripts/*`). Hence the home is
7
+ * here and not in the linter core: importing "the linter core" from a skill script
8
+ * would be a lie about the direction of the dependency.
9
+ *
10
+ * CHECKS NOTHING AND PRINTS NOTHING: text in, data out. What to do with a missing
11
+ * parser is the caller's decision, and the decisions DIFFER:
12
+ * • a linter — degrades and nags LOUDLY about it (the blocking gate must keep
13
+ * working meanwhile: PreToolUse counts only exit code 2 as a block, and a
14
+ * failure on import would switch the gate off silently);
15
+ * • a skill script — fails through `requireMarkdown()`, because its output goes
16
+ * into the pipeline gates, and "0 headings" reads there as a fact about the paper.
17
+ */
18
+ // THE RULE (`CLAUDE.md`, 2026-08-11): markdown structure is parsed with a PARSER, not
19
+ // with regexes. Stepped on four times, three of them the same defect: JS `\b` is
20
+ // defined over `[A-Za-z0-9_]`, and next to a Cyrillic letter there is NO word boundary,
21
+ // and the `u` flag does not change that. A level-2 heading whose text is a Cyrillic word
22
+ // never matched once over the whole life of the check. A heading out of the parser has a
23
+ // LEVEL and a TEXT — "word boundary" does not exist there in nature, and the mistake
24
+ // becomes IMPOSSIBLE to express.
25
+ //
26
+ // The helper moved here from `paper-lint.mjs` on 2026-08-11, when a second consumer
27
+ // appeared (`kb-lint.checkRejectedRowsCited` cut `topics/*.md` into sections with a
28
+ // regex — and did so TWO LINES ABOVE its own comment about this very bug). The
29
+ // condition for the move had been written down in advance, for exactly this case.
30
+ //
31
+ // 🔴 THE IMPORT IS OPTIONAL. Before 2026-08-11 the hooks had not a single dependency, and
32
+ // a bare top-level `import` changed the shape of the failure: no `node_modules` → exit 1
33
+ // → **the blocking `pre` gate in paper-lint stops blocking silently**, because
34
+ // PreToolUse counts only exit code 2 as a block. Measured: `ERR_MODULE_NOT_FOUND`, exit 1.
35
+ let md = null;
36
+ try {
37
+ const { default: MarkdownIt } = await import("markdown-it");
38
+ md = new MarkdownIt();
39
+ } catch {
40
+ md = null;
41
+ }
42
+
43
+ /**
44
+ * Whether the parser resolved. A linter must check this ITSELF and say out loud that
45
+ * its heading checks are off: with no parser `headings()` returns an empty list, and
46
+ * silent emptiness reads as "the document has no headings", that is, as a clean
47
+ * document. The nudge lives in the linters, because by contract (see the header) this
48
+ * file has no right to produce a single finding.
49
+ */
50
+ export const MD_AVAILABLE = () => md !== null;
51
+
52
+ /**
53
+ * The frontmatter is BLANKED with spaces, not cut out: the offsets must stay the
54
+ * same, because the checks cut the document by offset. Blanking is necessary — inside
55
+ * a YAML block scalar a line indented by two spaces (` ## Foo`) is a real ATX
56
+ * heading to CommonMark, and without blanking the frontmatter starts supplying
57
+ * sections.
58
+ */
59
+ const FRONTMATTER_RE = /^---[ \t]*\r?\n[\s\S]*?\r?\n---[ \t]*(?=\r?\n|$)/;
60
+ export function blankFrontmatter(text) {
61
+ const m = FRONTMATTER_RE.exec(text);
62
+ return m ? m[0].replace(/[^\n]/g, " ") + text.slice(m[0].length) : text;
63
+ }
64
+
65
+ /**
66
+ * The frontmatter body (between the fences), or `null` — if the file opens without one.
67
+ *
68
+ * Added 2026-08-20, when eight places in `.claude/**` cut the frontmatter with eight
69
+ * slightly different regexes: some knew about `\r\n`, some did not; some required a
70
+ * newline after the closing fence, some did not. The difference between them is not
71
+ * style, it is a different answer on one and the same file.
72
+ *
73
+ * ⚠️ This is NOT a parser and does not claim to be: `---` frontmatter is not markdown,
74
+ * no markdown parser models it (the analysis is in the root `CLAUDE.md`, "Considered
75
+ * and rejected: gray-matter"). The value here is one fence instead of eight, and the
76
+ * parsing of VALUES is done by `js-yaml` at the caller.
77
+ */
78
+ export function frontmatterBlock(text) {
79
+ const m = FRONTMATTER_RE.exec(text);
80
+ if (!m) return null;
81
+ const inner = m[0]
82
+ .replace(/^---[ \t]*\r?\n/, "")
83
+ .replace(/\r?\n---[ \t]*$/, "");
84
+ return inner;
85
+ }
86
+
87
+ /**
88
+ * The document WITHOUT the frontmatter — the block is cut out together with the
89
+ * newline after it.
90
+ *
91
+ * Differs from {@link blankFrontmatter} in that the offsets SHIFT. Both forms are
92
+ * needed and both have been used already: blanking — where character positions are
93
+ * later shown to the user; cutting — where the text is merely counted. Choose by what
94
+ * the caller does, not by taste.
95
+ */
96
+ export function stripFrontmatter(text) {
97
+ const m = FRONTMATTER_RE.exec(text);
98
+ return m ? text.slice(m[0].length).replace(/^\r?\n/, "") : text;
99
+ }
100
+
101
+ /** Line-start offsets — so that `token.map` becomes a position in the source string. */
102
+ function lineStarts(text) {
103
+ const out = [0];
104
+ for (let i = 0; i < text.length; i++) if (text[i] === "\n") out.push(i + 1);
105
+ return out;
106
+ }
107
+
108
+ /**
109
+ * All the document's headings: `[{ depth, text, line, offset }]`, in order of occurrence.
110
+ *
111
+ * `offset` — the index of the start of the heading's line IN THE SAME text that was
112
+ * passed in, in `slice`/`indexOf` units (UTF-16), because those are exactly what the
113
+ * checks cut the document with. `line` — 0-based line number. `text` — without the
114
+ * hashes, trimmed at the edges.
115
+ *
116
+ * ⚠️ ATX ONLY (`## Heading`). Setext (`Heading\n---`) is deliberately NOT counted as a
117
+ * heading: in this knowledge base `---` is a prose separator, and a paragraph it stands
118
+ * under without a blank line would become a level-2 heading per CommonMark. The former
119
+ * regexps knew only ATX, so this preserves the contract rather than narrowing it.
120
+ */
121
+ export function headings(text) {
122
+ if (md === null) return [];
123
+ const toks = md.parse(blankFrontmatter(text), {});
124
+ const starts = lineStarts(text);
125
+ const out = [];
126
+ for (let i = 0; i < toks.length; i++) {
127
+ const t = toks[i];
128
+ if (t.type !== "heading_open" || !t.map) continue;
129
+ if (!t.markup || !t.markup.startsWith("#")) continue; // setext — see the caveat above
130
+ const inline = toks[i + 1];
131
+ out.push({
132
+ depth: Number(t.tag.slice(1)),
133
+ text: inline?.type === "inline" ? inline.content.trim() : "",
134
+ line: t.map[0],
135
+ offset: starts[t.map[0]] ?? 0,
136
+ });
137
+ }
138
+ return out;
139
+ }
140
+
141
+ /** Offsets of the `---` thematic breaks (in this knowledge base — a prose separator). */
142
+ export function thematicBreaks(text) {
143
+ if (md === null) return [];
144
+ const starts = lineStarts(text);
145
+ return md
146
+ .parse(blankFrontmatter(text), {})
147
+ .filter((t) => t.type === "hr" && t.map)
148
+ .map((t) => starts[t.map[0]] ?? 0);
149
+ }
150
+
151
+ /**
152
+ * A section's body: from the line AFTER the heading matching `match`, up to the next
153
+ * heading of THE SAME OR A LOWER level. `{ heading, body }` or `null`.
154
+ *
155
+ * `match` — a regex over the heading text OR a predicate over `{depth,text,line,offset}`.
156
+ *
157
+ * Level-or-lower, not "exactly `##`": a `###` subsection inside the section does not
158
+ * end it — that is exactly how the former expressions with the `(?=\n## )` lookahead
159
+ * behaved. A regex cannot know this distinction in principle: it has no notion of level.
160
+ *
161
+ * `depth` — the CEILING on the level of the heading itself. Without it an `### Abstract`
162
+ * quoted in an appendix becomes the abstract — and the abstract is judged with a
163
+ * threshold of ZERO.
164
+ *
165
+ * `stopAtRule` additionally cuts the section off at the first `---`. That is NOT markdown
166
+ * structure but a paper convention, and it is preserved deliberately: the conclusion has
167
+ * a `---` before the appendices, and without that boundary a section with no following
168
+ * `##` would reach to the end of the file.
169
+ *
170
+ * 🔴 A MERGED VERSION. On 2026-08-11 this function appeared SIMULTANEOUSLY in two places:
171
+ * here (for `kb-lint`) and in `paper-lint.mjs` (richer — with the level ceiling, the
172
+ * heading in the result and `stopAtRule`). Both were written within one hour by two
173
+ * parallel passes, neither saw the other. Merged into the rich version: the weak one was
174
+ * a subset, and keeping both would have meant diverging in behaviour at the very next edit.
175
+ */
176
+ export function sectionBody(
177
+ text,
178
+ match,
179
+ { depth = 6, stopAtRule = false } = {},
180
+ ) {
181
+ const test = typeof match === "function" ? match : (h) => match.test(h.text);
182
+ const hs = headings(text);
183
+ const i = hs.findIndex((h) => h.depth <= depth && test(h));
184
+ if (i === -1) return null;
185
+ const h = hs[i];
186
+ const next = hs.slice(i + 1).find((n) => n.depth <= h.depth);
187
+ let end = next ? next.offset : text.length;
188
+ if (stopAtRule) {
189
+ const hr = thematicBreaks(text).find((o) => o > h.offset && o < end);
190
+ if (hr !== undefined) end = hr;
191
+ }
192
+ // The body starts on the line AFTER the heading: the heading itself is not part of the
193
+ // section, just as the former expressions' group did not capture it.
194
+ const nl = text.indexOf("\n", h.offset);
195
+ const start = nl === -1 || nl + 1 > end ? end : nl + 1;
196
+ return { heading: h, body: text.slice(start, end) };
197
+ }
198
+
199
+ /**
200
+ * Slicing the document into chunks "a heading + everything up to the next heading FROM
201
+ * THE SAME RANGE of levels". → `[{ heading, raw, body }]`.
202
+ *
203
+ * This is exactly what four scripts did as `split(/^(?=## )/m)` and
204
+ * `split(/^(?=#{2,3} )/m)`. The shape of the expression lied about the intent: "two or
205
+ * three hashes" is about CHARACTERS, while what is needed is the LEVEL, and `#{2,3}`
206
+ * also caught a hash inside a ```-block, that is, it cut a quotation from someone
207
+ * else's paper as a section of its own.
208
+ *
209
+ * `heading` — `null` on the zeroth element (the preamble before the first heading). It
210
+ * is ALWAYS there, including empty: the former `split` also always returned the chunk
211
+ * before the first separator as its zeroth element, and callers rely on that
212
+ * (`sub === chunk` in `artifact-coverage` meant "there are no subsections").
213
+ *
214
+ * `raw` — WITH the heading line (it is used to count `§` references and bold numbers,
215
+ * which occur in the heading itself too), `body` — without it (it is used to count words).
216
+ *
217
+ * 2026-08-11: moved here rather than copied into every script, because the chunk
218
+ * boundary is the one place where an off-by-one is easy, and such a mistake has already
219
+ * happened (`recordBlock` in `skill-corpus.mjs` returned a single "#" character for all
220
+ * 21 skills).
221
+ */
222
+ export function splitSections(text, { min = 2, max = 6 } = {}) {
223
+ const hs = headings(text).filter((h) => h.depth >= min && h.depth <= max);
224
+ const out = [];
225
+ const preEnd = hs.length ? hs[0].offset : text.length;
226
+ out.push({
227
+ heading: null,
228
+ raw: text.slice(0, preEnd),
229
+ body: text.slice(0, preEnd),
230
+ });
231
+ for (let i = 0; i < hs.length; i++) {
232
+ const h = hs[i];
233
+ const end = i + 1 < hs.length ? hs[i + 1].offset : text.length;
234
+ const nl = text.indexOf("\n", h.offset);
235
+ const bodyStart = nl === -1 || nl + 1 > end ? end : nl + 1;
236
+ out.push({
237
+ heading: h,
238
+ raw: text.slice(h.offset, end),
239
+ body: text.slice(bodyStart, end),
240
+ });
241
+ }
242
+ return out;
243
+ }
244
+
245
+ /**
246
+ * The text without the contents of ```-blocks (the fence lines themselves go too).
247
+ *
248
+ * Through the parser, not `/```[\s\S]*?```/g`: that pattern glues the end of one block
249
+ * to the start of the next when the number of fences is odd, knows nothing about `~~~`
250
+ * fences or indented fences, and silently eats the prose between blocks.
251
+ *
252
+ * `blank: true` — the block's lines are NOT deleted but REPLACED with empty ones. The
253
+ * difference is not cosmetic: deleting the lines GLUES the paragraph before the block to
254
+ * the paragraph after it, and a code block is a block boundary — on the other side a
255
+ * different paragraph starts. Measured 2026-08-11 on `<papers-root>/<paper>/README.md`:
256
+ * `prose-lint` counted 82 sentences there, and after the lines were deleted it counted
257
+ * 81 — the lead paragraph "**Reproduction commands:**" merged with the text AFTER the
258
+ * block into one sentence. That is exactly the failure against which `prose-lint` itself
259
+ * says "Paragraph and heading boundaries END a sentence", and it inflates the
260
+ * claims-per-sentence counter — the threshold at which the file emits findings. The
261
+ * former regex preserved the boundary (it left one newline on each side), so `blank` is
262
+ * also a preservation of the former behaviour where that behaviour was right.
263
+ */
264
+ /**
265
+ * The CONTENTS of fenced blocks — the other side of `stripFences`.
266
+ *
267
+ * Added 2026-09-08 for the check "a path as a command argument inside a ```-block"
268
+ * (the sixth form of a reference). A private regex for the fence there would have been
269
+ * the seventh instance of the class this knowledge base has already paid for:
270
+ * `/^```[\s\S]*?^```/gm` knows nothing about nested fences or about `~~~`, and the
271
+ * parser does — the `fence` token has a `content`.
272
+ *
273
+ * No parser — returns an empty list, and the caller must survive that: for hooks
274
+ * degradation is acceptable (see the module comment), while a silent fallback to a
275
+ * regex would mean a working degraded version that nobody notices.
276
+ */
277
+ export function fences(text) {
278
+ if (md === null) return [];
279
+ const out = [];
280
+ for (const t of md.parse(blankFrontmatter(text), {}))
281
+ if (t.type === "fence" || t.type === "code_block") out.push(t.content);
282
+ return out;
283
+ }
284
+
285
+ export function stripFences(text, { blank = false } = {}) {
286
+ if (md === null) return text;
287
+ const lines = text.split("\n");
288
+ const drop = new Set();
289
+ for (const t of md.parse(blankFrontmatter(text), {}))
290
+ if ((t.type === "fence" || t.type === "code_block") && t.map)
291
+ for (let i = t.map[0]; i < t.map[1]; i++) drop.add(i);
292
+ return blank
293
+ ? lines.map((l, i) => (drop.has(i) ? "" : l)).join("\n")
294
+ : lines.filter((_, i) => !drop.has(i)).join("\n");
295
+ }
296
+
297
+ /**
298
+ * For SCRIPTS, not for hooks: fails if there is no parser.
299
+ *
300
+ * For a linter degradation is acceptable — it is advisory and can say so out loud. A
301
+ * skill script hands numbers to a pipeline gate, and an empty list of headings there is
302
+ * indistinguishable from "the paper has no sections". A confident zero has already been
303
+ * read as a finding four times in this repo; here it is inexpressible.
304
+ */
305
+ export function requireMarkdown() {
306
+ if (MD_AVAILABLE()) return;
307
+ // 🔴 THE CURE DEPENDS ON THE DELIVERY CHANNEL, AND NAMING THE WRONG ONE WASTES THE ERROR.
308
+ // Measured 2026-09-19: `npm pack` strips `package-lock.json` unconditionally, and a plugin copy
309
+ // gets `npm ci` only when a lockfile is present in the fetched root — so a plugin installed from
310
+ // npm has NO `node_modules`, with no log entry anywhere. Telling that user to run `npm i at the
311
+ // repo root` points at a repository they do not have. The channel is readable from this file's
312
+ // own location, so the message can name it instead of guessing.
313
+ const fromPluginCache = import.meta.url.includes("/.claude/plugins/");
314
+ console.error(
315
+ "markdown-it does not resolve — this script parses markup with a parser and without it " +
316
+ "would produce a confident zero instead of an error.\n" +
317
+ (fromPluginCache
318
+ ? " Running from a PLUGIN copy, which is installed without dependencies. Install the npm\n" +
319
+ " package in the project as well (`npm i paperlint`) and call it through\n" +
320
+ " `npx paperlint`, which resolves from the project's own tree."
321
+ : " Cured by `npm i` at the repo root."),
322
+ );
323
+ process.exit(2);
324
+ }
325
+
326
+ /**
327
+ * GFM tables — rows, cells and the line range, WITH THE PARSER.
328
+ *
329
+ * The only consumer as of 2026-08-20 is `crosspost-article`, which needs to re-lay a
330
+ * table into a monospace block for a platform without tables (Medium). It lives here
331
+ * and not in the skill, by the rule "markup is parsed with a parser, and in one place":
332
+ * with a regex a table is told apart from a line of text with pipes only by its
333
+ * neighbours, and that is exactly the class that has already cost this repo 27 places.
334
+ *
335
+ * Returns `{ line, endLine, rows }`, where `rows[0]` is the header; the alignment row
336
+ * (`| --- |`) is thrown away, because in monospace output it has nothing to express.
337
+ */
338
+ /**
339
+ * The document's paragraphs — `paragraph_open` line ranges, WITH THE PARSER.
340
+ * `[{ line, endLine }]`.
341
+ *
342
+ * The only consumer as of 2026-08-21 is `crosspost-article`: the blog's sources are
343
+ * hard-wrapped at a column (an editorial habit, not markdown), and the platforms render
344
+ * a single `\n` inside a paragraph DIFFERENTLY — dev.to (Redcarpet) keeps it as a
345
+ * literal line break, and the paragraph arrives torn into short lines exactly at the
346
+ * source file's boundaries. Medium has no such defect not because the platform is more
347
+ * honest, but because the paste goes through "Import a story" — the importer takes the
348
+ * ALREADY RENDERED page of the site (where the breaks were collapsed by the browser long
349
+ * ago), not the raw markdown.
350
+ *
351
+ * The range is EXACTLY the one `t.map` gives on `paragraph_open` — that is, a code
352
+ * fence, a heading, a table, a list (in a TIGHT list the item is not wrapped in a
353
+ * `paragraph_open` at all, it has no paragraph of its own) fall outside this function
354
+ * automatically, by the construction of the parser, and not by a separate line-type
355
+ * check.
356
+ */
357
+ export function paragraphs(text) {
358
+ if (md === null) return [];
359
+ return md
360
+ .parse(blankFrontmatter(text), {})
361
+ .filter((t) => t.type === "paragraph_open" && t.map)
362
+ .map((t) => ({ line: t.map[0], endLine: t.map[1] }));
363
+ }
364
+
365
+ export function tables(text) {
366
+ if (md === null) return [];
367
+ const lines = blankFrontmatter(text).split("\n");
368
+ const out = [];
369
+ for (const t of md.parse(blankFrontmatter(text), {})) {
370
+ if (t.type !== "table_open" || !t.map) continue;
371
+ const rows = [];
372
+ for (let i = t.map[0]; i < t.map[1]; i++) {
373
+ const raw = lines[i];
374
+ if (/^\s*\|?[\s:|-]+\|?\s*$/.test(raw) && raw.includes("-")) continue; // alignment
375
+ rows.push(
376
+ raw
377
+ .replace(/^\s*\|/, "")
378
+ .replace(/\|\s*$/, "")
379
+ .split("|")
380
+ .map((c) => c.trim()),
381
+ );
382
+ }
383
+ out.push({ line: t.map[0], endLine: t.map[1], rows });
384
+ }
385
+ return out;
386
+ }
@@ -0,0 +1,227 @@
1
+ /**
2
+ * `runMutations` — three harness outcomes, not two.
3
+ *
4
+ * ── WHY THIS FILE SHOWED UP LAST, NOT FIRST ────────────────────────────
5
+ * The engine that renders a verdict for EVERY battery in the repository was, until 2026-09-15,
6
+ * checked by nothing. Every one of its defenses (a harness path pointing at nothing · a no-op
7
+ * mutation · retrying a non-kill · a baseline of already-red harnesses) was written AFTER it
8
+ * caught a real defect, and none of them had a test. So the next defect of the same class
9
+ * arrived in CI and was read as "two mutations survived".
10
+ *
11
+ * The third outcome — SKIP — is pinned down here. The fixtures are real: the skipping harness
12
+ * calls `skip()` FROM vigiles rather than faking it with exit code 77 by hand, because the
13
+ * latter would test my mental model of the protocol instead of the protocol itself.
14
+ */
15
+ import assert from "node:assert/strict";
16
+ import {
17
+ mkdtempSync,
18
+ writeFileSync,
19
+ readFileSync,
20
+ rmSync,
21
+ realpathSync,
22
+ } from "node:fs";
23
+ import { tmpdir } from "node:os";
24
+ import { join } from "node:path";
25
+ import { createRequire } from "node:module";
26
+ import { pathToFileURL } from "node:url";
27
+ import { recordCheck } from "vigiles";
28
+ import { runMutations } from "./mutation-driver.mjs";
29
+
30
+ // The fixture lives in a temp directory where `node` will not find `vigiles` by walking up
31
+ // the tree. The specifier is resolved HERE, by node's own resolver, and substituted in
32
+ // absolute — that way the skip is done by a real `skip()`, not an imitation via exit code 77.
33
+ // An imitation would test my mental model of the protocol instead of the protocol itself.
34
+ const VIGILES = pathToFileURL(
35
+ createRequire(import.meta.url).resolve("vigiles"),
36
+ ).href;
37
+
38
+ const SUBJECT = "export const MARKER = 'keep-me';\n";
39
+ const cases = [];
40
+
41
+ /** Temp root: a fixture subject + the harness that judges it. */
42
+ function fixture(harnessBody) {
43
+ const root = realpathSync(mkdtempSync(join(tmpdir(), "mutdrv-")));
44
+ const subject = join(root, "subject.mjs");
45
+ const harness = join(root, "probe.harness.mjs");
46
+ writeFileSync(subject, SUBJECT);
47
+ writeFileSync(harness, harnessBody(subject));
48
+ return { root, subject, harness };
49
+ }
50
+
51
+ const quiet = { log: console.log, error: console.error };
52
+ /** The verdict is read from the exit code and output, so the driver's output is intercepted. */
53
+ function silently(fn) {
54
+ const lines = [];
55
+ console.log = console.error = (...a) => lines.push(a.join(" "));
56
+ try {
57
+ return { code: fn(), out: lines.join("\n") };
58
+ } finally {
59
+ console.log = quiet.log;
60
+ console.error = quiet.error;
61
+ }
62
+ }
63
+
64
+ // ── 1. THE HARNESS SKIPS ⇒ the battery REFUSES, rather than declaring the mutation survived.
65
+ // Observed failure (run 34966606186): CI had no `claude` binary, the harness skipped,
66
+ // `vigiles test` returned 0, and the driver printed "🔴 SURVIVED" — a claim about the TEST
67
+ // that was really a claim about the ENVIRONMENT.
68
+ {
69
+ const { root, subject, harness } = fixture(
70
+ () =>
71
+ `import { skip } from ${JSON.stringify(VIGILES)};\nskip("fixture: the capability this tier observes is absent");\n`,
72
+ );
73
+ try {
74
+ const before = readFileSync(subject, "utf8");
75
+ const { code, out } = silently(() =>
76
+ runMutations({
77
+ root,
78
+ runner: "node",
79
+ cases: [
80
+ {
81
+ name: "marker removed",
82
+ harness,
83
+ expect: "MARKER",
84
+ disables: "fixture",
85
+ edits: [[subject, "keep-me", "gone"]],
86
+ },
87
+ ],
88
+ }),
89
+ );
90
+ assert.equal(code, 2, `a skip must REFUSE, not judge; got ${code}\n${out}`);
91
+ assert.doesNotMatch(
92
+ out,
93
+ /SURVIVED/,
94
+ "and not claim a survived mutation — that's a false diagnosis",
95
+ );
96
+ assert.match(
97
+ out,
98
+ /SKIP on clean source/,
99
+ "the refusal must name the cause",
100
+ );
101
+ assert.match(
102
+ out,
103
+ /probe\.harness\.mjs/,
104
+ "and name WHICH harness is skipping",
105
+ );
106
+ assert.equal(
107
+ readFileSync(subject, "utf8"),
108
+ before,
109
+ "the refusal must happen BEFORE any file is edited — the message arrives with a clean tree",
110
+ );
111
+ cases.push(
112
+ "harness skips → refusal before editing files, no verdict about the mutation",
113
+ );
114
+ } finally {
115
+ rmSync(root, { recursive: true, force: true });
116
+ }
117
+ }
118
+
119
+ // ── 2. THE HARNESS WORKS ⇒ the prior behavior is intact. Without this half, case 1 is
120
+ // indistinguishable from an engine that just always refuses.
121
+ {
122
+ const { root, subject, harness } = fixture(
123
+ (s) =>
124
+ `import assert from "node:assert/strict";\nimport { readFileSync } from "node:fs";\nassert.match(readFileSync(${JSON.stringify(s)}, "utf8"), /keep-me/, "MARKER is gone");\n`,
125
+ );
126
+ try {
127
+ const { code, out } = silently(() =>
128
+ runMutations({
129
+ root,
130
+ runner: "node",
131
+ cases: [
132
+ {
133
+ name: "marker removed",
134
+ harness,
135
+ expect: "MARKER is gone",
136
+ disables: "fixture",
137
+ edits: [[subject, "keep-me", "gone"]],
138
+ },
139
+ ],
140
+ }),
141
+ );
142
+ assert.equal(
143
+ code,
144
+ 0,
145
+ `a working harness must kill the mutation; got ${code}\n${out}`,
146
+ );
147
+ assert.match(
148
+ out,
149
+ /RED \(expected case\)/,
150
+ "and kill it with ITS OWN message, not just any red",
151
+ );
152
+ assert.equal(
153
+ readFileSync(subject, "utf8"),
154
+ SUBJECT,
155
+ "the source must be restored",
156
+ );
157
+ cases.push(
158
+ "harness works → mutation killed by its own assertion, source restored",
159
+ );
160
+ } finally {
161
+ rmSync(root, { recursive: true, force: true });
162
+ }
163
+ }
164
+
165
+ // ── 3. THE PATCH NEVER LANDED ⇒ that is NOT a survived mutation. The third outcome, and until
166
+ // 09-17 the engine dumped it into the second one: anything that was not `RED (expected` counted
167
+ // as survived — including NOT FOUND, AMBIGUOUS and NO-OP, where the file was never edited at all.
168
+ //
169
+ // 🔴 The two verdicts send the reader in OPPOSITE directions: survived means STRENGTHEN THE
170
+ // TEST, unusable means FIX THE MUTATION. Observed the same day: a `find` string went stale
171
+ // after the rule was rewritten, the engine reported "1 mutation survived", and the next step
172
+ // would have been rewriting a harness that was fine.
173
+ {
174
+ const { root, subject, harness } = fixture(
175
+ (s) =>
176
+ `import assert from "node:assert/strict";\nimport { readFileSync } from "node:fs";\nassert.match(readFileSync(${JSON.stringify(s)}, "utf8"), /keep-me/, "MARKER is gone");\n`,
177
+ );
178
+ try {
179
+ const { code, out } = silently(() =>
180
+ runMutations({
181
+ root,
182
+ runner: "node",
183
+ cases: [
184
+ {
185
+ name: "stale target",
186
+ harness,
187
+ expect: "MARKER is gone",
188
+ disables: "fixture",
189
+ // The string is not in the file — exactly the case of a stale target.
190
+ edits: [[subject, "no-such-string-anywhere", "x"]],
191
+ },
192
+ ],
193
+ }),
194
+ );
195
+ assert.equal(
196
+ code,
197
+ 1,
198
+ `an unusable mutation must REFUSE the run; got ${code}\n${out}`,
199
+ );
200
+ assert.match(
201
+ out,
202
+ /UNUSABLE/,
203
+ "the verdict must be named unusable, not survived",
204
+ );
205
+ assert.match(
206
+ out,
207
+ /NOT FOUND/,
208
+ "and name the CAUSE — what exactly was not found",
209
+ );
210
+ assert.doesNotMatch(
211
+ out,
212
+ /mutation\(s\) survived/,
213
+ "and must NOT claim a survived mutation: that would send someone to fix a working test",
214
+ );
215
+ cases.push(
216
+ 'patch never landed → UNUSABLE with a cause, not "mutation survived"',
217
+ );
218
+ } finally {
219
+ rmSync(root, { recursive: true, force: true });
220
+ }
221
+ }
222
+
223
+ // Report to the tool, or the surface counts as UNCOVERED despite a working test —
224
+ // "0 checks" and "no test" look identical in the ledger.
225
+ recordCheck(cases.length);
226
+ console.log(`mutation-driver: ${cases.length} outcome(s) pinned down:`);
227
+ for (const c of cases) console.log(` ok ${c}`);