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,1254 @@
1
+ /**
2
+ * grade-paper-writing-ablation.eval.mjs — WHICH PART OF A DESCRIPTION KILLS A SKILL?
3
+ *
4
+ * Run: node .claude/skills/paper-pipeline/grade-paper-writing-ablation.eval.mjs [--trials N] [--mode main|oracle|preflight|setupdiff] [--arms A0,A6]
5
+ *
6
+ * --mode preflight every guard + the arm-by-arm word-set deltas; spends NO tokens <- run this FIRST
7
+ * --mode setupdiff the harness-configuration diff against the parent + the row-by-row
8
+ * A0-vs-parent comparison; spends NO tokens
9
+ * --mode main 7 arms x 8 prompts x 3 trials = 168 harness runs
10
+ * --mode oracle 7 arms x 8 prompts x 1 closed-book pick = 56 calls
11
+ * --arms A6 run only these arms and MERGE them into the existing result JSON, so a late
12
+ * arm does not cost a re-run of the arms already measured
13
+ *
14
+ * ─────────────────────────────────────────────────────────────────────────────
15
+ * THE FINDING THIS EXISTS TO EXPLAIN
16
+ *
17
+ * `framing-vs-vocabulary.eval.mjs` (this directory, 2026-08-08) measured three skills against the
18
+ * same 36 competitors on the same fixture and found one of them effectively DEAD:
19
+ *
20
+ * grade-paper-writing 6 fired / 48 runs (12%) 25% even in AH, 17% in SH
21
+ * tighten-paper 25 fired / 48 runs (52%)
22
+ * argument-arc 34 fired / 48 runs (71%)
23
+ *
24
+ * Same harness, same trials, same roster. That file could name the anomaly and not test it; its
25
+ * closing list of unsettled questions says so ("WHY `grade-paper-writing` IS DEAD (point 6). Needs a
26
+ * description-length/negation arm."). This is that arm.
27
+ *
28
+ * 🔴 FOUR EXPLANATIONS ARE ALREADY RULED OUT AND THIS FILE MUST NOT RE-TEST THEM.
29
+ *
30
+ * NOT VOCABULARY. `--mode preflight` in the parent prints the lexical argmax for every prompt. On
31
+ * all four AH and all four SH prompts, `grade-paper-writing` IS the argmax, overlap 0.67–1.00, two
32
+ * of them at 1.00 — every content word the user typed is in the description. It fires 25% / 17%
33
+ * anyway. A prompt cannot be lexically closer than "contained entirely".
34
+ *
35
+ * NOT COMPETITION. 42 of its 48 runs were SILENT: nothing fired at all. The whole 144-run parent
36
+ * experiment produced ONE competitor misfire. Nothing is out-competing this skill because nothing
37
+ * is competing.
38
+ *
39
+ * NOT RECOVERABILITY. The parent's closed-book oracle, handed all 37 name+description pairs, picks
40
+ * `grade-paper-writing` 10 times out of 16 on these same prompts. The information IS in the text.
41
+ *
42
+ * NOT "LONGEST" AND NOT "MOST NEGATED". Checked and false. At 1174 chars it is the SECOND longest
43
+ * description in the roster (`study-accepted-papers` is 1270 and fires). Its four negation clauses
44
+ * are exactly as many as `tighten-paper`'s four, and `tighten-paper` fires at 52%.
45
+ *
46
+ * So the cause, if it is in the description at all, is a PROPERTY of the text, not its size, its
47
+ * lexical coverage, its rivals, or its legibility to a careful reader.
48
+ *
49
+ * ─────────────────────────────────────────────────────────────────────────────
50
+ * THE DESIGN: ABLATE THE DESCRIPTION, HOLD THE PROMPTS FIXED
51
+ *
52
+ * The parent varied the prompts against a fixed description. This inverts that: the eight prompts are
53
+ * copied VERBATIM from the parent's CASES (guarded — see preflight #2) and the target's description
54
+ * is mutated one factor at a time. Everything else — fixture, roster, trials, tools, concurrency — is
55
+ * the parent's, so every arm below is directly comparable to its measured 25% (AH) / 17% (SH).
56
+ *
57
+ * A0 baseline verbatim. The control.
58
+ * A1 short keep sentence 1 ("Use when asking …") and sentence 2 ("Grades WRITING CRAFT
59
+ * only — nine dimensions … the fix for each."). Delete everything after.
60
+ * 1174 -> ~530 chars. The blunt arm: does ANY deletion revive it?
61
+ * A2 no-subordination delete ONLY the two clauses announcing that this skill is a SUBROUTINE of
62
+ * other skills: "whose stall inventory … is the readability gate other skills
63
+ * consume (pc-panel-review, paper-adversarial-review, harden-paper)" and
64
+ * "harden-paper (which calls it)". Everything else stays, NOT-clauses included.
65
+ * 🎯 THE ARM THIS FILE WAS BUILT FOR. The description twice tells the reader
66
+ * that other skills own this one's output. A selector reading that may be
67
+ * deferring — declining to call a thing that presents itself as something
68
+ * another skill calls. No other skill in the roster says this about itself.
69
+ * A3 no-negation delete ONLY the sentence carrying the "NOT …" delegation clauses.
70
+ * A4 no-machinery delete ONLY the opaque internals and proper nouns: the nine-dimension
71
+ * parenthetical, the "(Peyton Jones, McEnerney, Gopen & Swan; exemplars …)"
72
+ * authorities, the PERSONA sentence and the blind-panel/claims-diff sentence.
73
+ * Keeps sentence 1 and the NOT-clauses.
74
+ * A5 renamed description BYTE-IDENTICAL to A0; the directory and `name:` become
75
+ * `paper-readability-review`. Tests whether the NAME is what is lost — whether
76
+ * "grade-paper-writing" reads as a niche in-house verb the selector skips.
77
+ * The only arm that changes nothing about the description text.
78
+ * A6 persona-only ADDED AFTER A0-A5 RAN, on request, and it is A1's MIRROR. The skill has two
79
+ * jobs — a nine-dimension rubric and a persona cold-read stall pass — and A1
80
+ * as specified keeps the rubric sentence and deletes the persona one, so the
81
+ * six-arm design had a rubric-only description and no persona-only twin. A6 is
82
+ * that twin: the opening sentence (minus the `"grade the writing"` trigger,
83
+ * which belongs to the half being removed) plus "Runs the PERSONA cold-read
84
+ * stall pass (a committed non-academic persona subagent, per-section)."
85
+ * Nothing else. 🔴 JUDGEMENT CALL, RECORDED: the persona sentence's trailing
86
+ * subordination clause ("whose stall inventory … other skills consume …") is
87
+ * DELETED here even though the brief did not name it, because A1 drops the
88
+ * subordination clause too (it lives in the sentences A1 deletes). Keeping it
89
+ * would make A1-vs-A6 differ in TWO factors instead of one, and the pair is
90
+ * the whole point of the arm. It also removes a dangling "not the rubric
91
+ * number" reference to a rubric this description no longer mentions.
92
+ *
93
+ * Then the ORACLE, exactly as the parent implements it: closed book, no harness, no skills installed,
94
+ * the arm's 37 name+description pairs pasted in, one pick, NONE allowed. 6 arms x 8 prompts = 48.
95
+ * It separates the two things an arm's movement could mean:
96
+ *
97
+ * oracle moves WITH the harness the mutation changed what is RECOVERABLE from the text
98
+ * oracle flat, harness moves the mutation changed what the HARNESS DOES with the same text
99
+ *
100
+ * The second is the interesting one and the parent already showed the harness sits 14–39pp below the
101
+ * oracle everywhere, so there is room for it.
102
+ *
103
+ * WHY ONLY AH + SH. The parent's AL and SL cells sit on the floor for EVERY skill (44% and 11% across
104
+ * all three, 11% with 86% SILENT for SL). An arm measured there would be measuring the floor, not the
105
+ * skill. AH and SH are where `grade-paper-writing` is anomalous — 25% and 17% against siblings' 60-90%
106
+ * in the same cells — so that is where a mutation can show.
107
+ *
108
+ * ─────────────────────────────────────────────────────────────────────────────
109
+ * WHAT EACH ARM CAN AND CANNOT DISTINGUISH
110
+ *
111
+ * A1 rises, others flat "something after sentence 2 is the problem" — and A1 cannot say
112
+ * WHICH thing, because it deletes four sentences at once. A1 is the
113
+ * detector, A2/A3/A4 are the localisers. A1 rising with all three
114
+ * localisers flat is a real and annoying outcome: it would mean the
115
+ * damage is distributed, or is length-after-all inside this one text.
116
+ * A2 rises the subordination clauses are load-bearing. This is the hypothesis;
117
+ * a rise here is the finding. It still does not show WHY — "the selector
118
+ * defers to the named owner" and "the clause is 150 chars of noise
119
+ * between the user's words and the verb" both predict it. Untestable here.
120
+ * A3 rises the NOT-clauses suppress. NOTE this is NOT the already-refuted
121
+ * "most negation clauses" claim: that was a COUNT compared across
122
+ * skills (4 vs tighten-paper's 4). This is a within-skill deletion.
123
+ * Equal counts across skills does not mean equal effect within one.
124
+ * A4 rises the machinery/proper nouns dilute. The weakest-motivated arm; it is
125
+ * here because it is the other obvious "what is unusual about this text".
126
+ * A5 rises the name, not the description, is what the selector rejects. If A5
127
+ * moves, every conclusion drawn from A1–A4 is about text that was never
128
+ * the binding constraint.
129
+ * ALL FLAT the cause is NOT IN THE DESCRIPTION TEXT. That is a real result and
130
+ * this file reports it as one. It would point at the parent's point 5
131
+ * (the uniform harness-vs-oracle deficit) rather than at this skill.
132
+ *
133
+ * 🔗 A1 AND A6 ARE A PAIR AND MUST BE READ AS ONE. A1 = "this skill is the rubric"; A6 = "this skill
134
+ * is the persona cold read". Same length class, same scope (opening sentence + one job sentence),
135
+ * one factor between them: WHICH job the description claims. The live question behind it is whether
136
+ * to SPLIT the skill in two.
137
+ *
138
+ * 🔴 WHAT THE PAIR CANNOT ESTABLISH, stated before it runs:
139
+ * - IT CANNOT SHOW THAT A SPLIT WOULD WORK. It measures two candidate DESCRIPTIONS of one skill,
140
+ * not two SKILLS. A real split changes the roster (38 entries, two of them adjacent in meaning),
141
+ * gives each half its own gate row in `pc-panel-review` and `harden-paper`, and leaves BOTH
142
+ * halves subordinate to the same three consumers. None of that is in this measurement.
143
+ * - THE PERSONA HALF'S VOCABULARY IS ALREADY KNOWN TO BE PRESENT AND ALREADY KNOWN TO FAIL, and
144
+ * this is an observation carried in, not something tested here: the prompt "the abstract reads
145
+ * like a wall of jargon" scores overlap 1.00 against the CURRENT description — which already
146
+ * contains the persona clause — and the parent measured that whole SH cell at 17%. So a high A6
147
+ * number is not evidence that "the persona half is the healthy one"; it would itself need
148
+ * explaining against that. A high A6 with a high A1 says the deletions helped, not that the
149
+ * persona did.
150
+ * - n = 24 EACH. A1-vs-A6 inherits caveat 1 below in full: the pair resolves a factor of two
151
+ * between the two halves and nothing finer.
152
+ *
153
+ * 🔴 THE ARMS ARE NOT ORTHOGONAL AND THAT COSTS THE INTERPRETATION.
154
+ * A1's deletion is a strict SUPERSET of A2's and of A3's: everything A2 and A3 remove, A1 also
155
+ * removes. It is NOT a superset of A4's — A4 cuts two parentheticals INSIDE the sentence A1 keeps,
156
+ * while sharing the PERSONA and blind-panel sentences with A1. So:
157
+ * - A1 < A2 + A3 (i.e. A1 rising LESS than either localiser) is evidence that the deletions
158
+ * interfere, and no arm here can decompose that.
159
+ * - A1 ≈ A2 does NOT establish that A2's clauses are the whole story; it is consistent with
160
+ * "any of the four deleted sentences would have done it".
161
+ * - A2, A3, A4 are mutually disjoint in what they delete, so THOSE three can be read additively
162
+ * against A0 — with the n-caveat below, which is the binding one.
163
+ * A clean factorial would need 2^4 = 16 arms x 24 runs = 384 runs, ~$54. Not run. This is a
164
+ * one-factor-at-a-time screen and must be read as one.
165
+ *
166
+ * ─────────────────────────────────────────────────────────────────────────────
167
+ * 🔴 WHAT THIS RUN CANNOT SETTLE — stated before it runs, not discovered after
168
+ *
169
+ * 1. n = 24 PER ARM (8 prompts x 3 trials). At a baseline near 20% the 95% interval is roughly
170
+ * ±16pp. THIS RESOLVES A FACTOR OF TWO. IT DOES NOT RESOLVE FIFTEEN POINTS. An arm at 21% against
171
+ * a baseline at 17% is noise and will be reported as noise. Only a move to ~50%+ — i.e. into the
172
+ * range where the sibling skills already live — is readable at this n. The per-cell splits
173
+ * (AH vs SH, 12 runs each) resolve almost nothing and exist as texture.
174
+ * 2. 🔴 DELETION IS NOT OVERLAP-NEUTRAL, AND THE PREFLIGHT PRINTS THE DAMAGE. Every deletion arm
175
+ * removes words, so each one also LOWERS the prompt-to-description lexical overlap the parent
176
+ * measured as the dominant factor (35pp). A1 in particular guts the vocabulary the SH prompts
177
+ * match against. This biases the deletion arms DOWNWARD: an arm that rises does so despite the
178
+ * handicap (which strengthens it), but an arm that is FLAT may be a real gain cancelled by lost
179
+ * overlap, and this design cannot separate those. A5 is the only arm free of this confound — its
180
+ * word set is identical by construction — which is a second reason it is worth its 24 runs.
181
+ * 3. ONE SKILL, ONE ROSTER, ONE MODEL. Whatever is found is a fact about this description in this
182
+ * roster. The parent's roster-size question (point 5 there) is untouched here.
183
+ * 4. SILENT IS NOT PROOF OF NON-CANDIDACY — the parent's caveat 2, unchanged. Tools are restricted
184
+ * to Skill and Read and bodies are stubbed, but nothing forces a selection, and a run that simply
185
+ * answers from the fixture is also SILENT.
186
+ * 5. THE ORACLE IS A MODEL, NOT A HUMAN, and shares the training distribution of the thing measured.
187
+ * It bounds what the DESCRIPTIONS support; it does not stand in for a person.
188
+ * 6. A RISE DOES NOT NAME A MECHANISM. Every arm is a deletion; deletions confound "removed a
189
+ * harmful signal" with "shortened the text" with "removed distracting tokens". The arms differ in
190
+ * WHAT is deleted, never in WHETHER something is. An additive arm (ADDING a subordination clause
191
+ * to a healthy skill and watching it fall) is the confirmatory experiment and is NOT run here —
192
+ * it is the honest next step if A2 moves.
193
+ * 7. NO INTERVENTION FOLLOWS FROM THIS FILE ALONE. Same rule the parent set for itself: a fix
194
+ * decided and validated on one run is overfitting. If an arm wins, the edit must be re-measured
195
+ * on prompts held out from THIS file too.
196
+ *
197
+ * ─────────────────────────────────────────────────────────────────────────────
198
+ * COST — nothing was cut to save it
199
+ *
200
+ * main 6 arms x 8 prompts x 3 trials = 144 runs
201
+ * oracle 6 arms x 8 prompts x 1 pick = 48 calls
202
+ * 192, the parent's exact budget
203
+ * At the siblings' measured ~$0.14/run that is ~$27 API-equivalent, billed to a subscription,
204
+ * $0 metered. The design as specified is the design that ran. AL and SL were never in it (see
205
+ * "WHY ONLY AH + SH" — they are floor cells, not a saving).
206
+ *
207
+ * 🔴 NOT ONE FILE UNDER `.claude/skills/` IS MODIFIED BY THIS RUN. Each arm is a full copy of the
208
+ * skills directory into the session scratchpad with ONE SKILL.md rewritten there; the copy's path is
209
+ * what `skillsDir` receives. The real `grade-paper-writing/SKILL.md` is read and never written.
210
+ *
211
+ * ─────────────────────────────────────────────────────────────────────────────
212
+ * 🔴 RESULT — 2026-08-08. 168 harness runs (7 arms), 56 oracle picks, 0 errored, + 24 replication runs.
213
+ * Raw logs: `repro/2026-08-08-grade-paper-writing-ablation-raw.log` (A0-A5 main)
214
+ * `repro/2026-08-08-grade-paper-writing-ablation-A6-raw.log` (A6 main)
215
+ * `repro/2026-08-08-grade-paper-writing-ablation-oracle.log` (A0-A5 oracle)
216
+ * `repro/2026-08-08-grade-paper-writing-ablation-A6-oracle.log` (A6 oracle)
217
+ * `repro/2026-08-08-grade-paper-writing-ablation-setupdiff.log` (the config diff, free)
218
+ * `repro/2026-08-08-parent-replication-gpw.log` (the parent's own code, re-run)
219
+ * Rows: `repro/2026-08-08-grade-paper-writing-ablation.json` (56 prompt-rows, buckets, overlap)
220
+ * `repro/2026-08-08-grade-paper-writing-ablation-oracle.json`
221
+ * `repro/2026-08-08-parent-replication-gpw.json`
222
+ *
223
+ * ─────────────────────────────────────────────────────────────────────────────
224
+ * 1. 🔴 THE HEADLINE IS NOT AN ARM. THE CONTROL DID NOT REPLICATE THE PARENT, AND THE PREMISE OF THIS
225
+ * WHOLE EXPERIMENT — "grade-paper-writing is effectively dead" — DOES NOT HOLD ON RE-MEASUREMENT.
226
+ *
227
+ * ⚠️ 2026-08-09: A0 IS TWO TOKENS OFF THE TEXT MEASURED BELOW. The scorecard row ids were
228
+ * renamed from two-letter codes to words (`Hn` → `claims`) because the two-letter space had
229
+ * collided — see `scripts/pipeline-edges.mjs`. The target's description carried one such
230
+ * reference, so "…mandates the Hn claim-preservation diff." became "…mandates the
231
+ * claims-preservation diff (row `claims`)." A0 is still read from disk and every guard still
232
+ * passes; the numbers below were measured on the pre-rename bytes and are left as recorded.
233
+ *
234
+ * measurement AH SH total
235
+ * parent, this morning 3/12 25% 2/12 17% 5/24 21%
236
+ * A0 here, hours later, verbatim description 7/12 58% 6/12 50% 13/24 54%
237
+ * the PARENT'S OWN CODE, re-run, real .claude/skills 9/12 75% 3/12 25% 12/24 50%
238
+ *
239
+ * The third row is the decisive one: `repro/2026-08-08-parent-replication.mjs` runs
240
+ * `framing-vs-vocabulary.eval.mjs` itself, with two asserted textual patches (filter the main loop
241
+ * to this skill and to AH+SH; redirect the output JSON), every guard, option, fixture and predicate
242
+ * untouched, against the REAL skills directory. It returns 50%, not 21%. So the gap is not in this
243
+ * file's setup — it is between two runs of the same code on the same inputs, hours apart.
244
+ *
245
+ * `--mode setupdiff` (free, logged) checked the alternative mechanically rather than by eye:
246
+ * · allowedTools, trials, concurrency, spacingSec, timeoutMs, minPrompts — identical strings;
247
+ * · the FIXTURE source block — byte-identical to the parent's;
248
+ * · the `fired` predicate and the `firedSkills` helper — identical modulo the id they match;
249
+ * · every arm dir: 37 skills, and every SKILL.md frontmatter sha256-identical to the real one
250
+ * except the single deliberate mutation. No copy altered a competitor's description.
251
+ * Row by row, the parent's 5/24 and A0's 13/24 differ on 3 of 8 prompts by ≥2 of 3 trials
252
+ * (+3, +2, +2) and one by +1; four prompts are unchanged. The shift is spread, not one prompt.
253
+ *
254
+ * WHAT THIS MEANS FOR WORK ALREADY DONE ON THE 12% FIGURE. `harden-paper` and `pc-panel-review`
255
+ * were edited earlier today with paragraphs citing "12% (6/48), measured 2026-08-08" as fact. The
256
+ * ADVICE in those paragraphs (invoke the skill by name rather than relying on selection) survives
257
+ * — a 50% selection rate is still a coin flip on a gate whose silent non-run looks like a pass —
258
+ * but the NUMBER does not, and prose that cites a measurement has to cite one that reproduces.
259
+ * 🔴 This file does not edit them. Flagging it is in scope; rewriting two SKILL.md files on the
260
+ * strength of one more run is the same mistake in the other direction.
261
+ *
262
+ * 2. THE ONE ARM OUTSIDE NOISE IS A5 — AND IT IS AN ARTIFACT, WHICH THE ORACLE MADE VISIBLE.
263
+ *
264
+ * arm what changed chars fired AH SH Δ silent oracle
265
+ * A0 baseline, verbatim 1174 54% 13/24 58% 50% — 46% 7/8
266
+ * A1 sentences 1-2 only (rubric) 528 50% 12/24 50% 50% -4pp 46% 7/8
267
+ * A2 no subordination clauses 992 50% 12/24 58% 42% -4pp 42% 8/8
268
+ * A3 no NOT-delegation sentence 965 54% 13/24 58% 50% 0pp 46% 7/8
269
+ * A4 no machinery / proper nouns 598 46% 11/24 50% 42% -8pp 42% 8/8
270
+ * A5 renamed, description IDENTICAL 1174 25% 6/24 33% 17% -29pp 71% 0/8
271
+ * A6 persona-only (A1's mirror) 225 42% 10/24 33% 50% -12pp 46% 8/8
272
+ *
273
+ * A5 is the only arm outside the interval (25% vs 54%; two-proportion z = 2.07, p ≈ 0.04 treating
274
+ * runs as independent — and they are not, see caveat 1, so read that p as an upper bound on the
275
+ * evidence). It is also the only arm whose SILENT share moves (71% vs 46%), which is the parent's
276
+ * discriminator doing exactly the job it was added for.
277
+ *
278
+ * 🔴 BUT A5 IS NOT THE CLEAN NAME TEST IT WAS DESIGNED AS, and the oracle is what caught it. The
279
+ * closed-book oracle scored A5 at 0/8 — and on 7 of those 8 picks it answered
280
+ * `grade-paper-writing`, A NAME THAT IS NOT IN THE ROSTER IT WAS SHOWN. It could do that because
281
+ * SIX OTHER DESCRIPTIONS in the roster name this skill: `cold-read-diff`, `draft-paper`,
282
+ * `harden-paper`, `paper-adversarial-review`, `pc-panel-review`, `tighten-paper`. Renaming the
283
+ * skill turns all six into dangling references to a skill that no longer exists, and the reader
284
+ * follows the cross-references rather than the entry in front of it. So A5 measures
285
+ * "rename a skill that six sibling descriptions call by its old name", NOT "the name in
286
+ * isolation". The design intended the second and delivered the first. That is still a real and
287
+ * useful finding about THIS roster — cross-references are load-bearing and a rename is not a
288
+ * local edit — but it is not evidence that `grade-paper-writing` is a bad name, and it must not
289
+ * be quoted as such. A clean name test would have to rewrite the six references in the same pass,
290
+ * which would break the strict-subset guard (it adds the new name to six other descriptions) and
291
+ * so needs a different design, not another arm here.
292
+ *
293
+ * 3. EVERY DESCRIPTION-TEXT ARM IS FLAT. THAT IS THE NEGATIVE RESULT AND IT IS THE ANSWER.
294
+ * A1 -4pp, A2 -4pp, A3 0pp, A4 -8pp, A6 -12pp, against an interval of roughly ±20pp at this
295
+ * baseline (n = 24, p ≈ 0.5) — and wider than that once the 8-prompt clustering is admitted.
296
+ * Not one of them is distinguishable from A0, and every one of them is NEGATIVE, i.e. no deletion
297
+ * helped even nominally. In particular:
298
+ * · A2, the arm this file was built for, moved -4pp. THE SUBORDINATION HYPOTHESIS IS NOT
299
+ * SUPPORTED. Announcing that other skills consume your output does not measurably suppress
300
+ * selection here. (It does move the oracle 7/8 → 8/8, which is a one-pick difference and
301
+ * means nothing on its own.)
302
+ * · A3 moved 0pp. The NOT-clauses are inert for selection, consistent with the already-known
303
+ * fact that `tighten-paper` carries four of them and fires fine.
304
+ * · A4 (-8pp) deleted half the description's vocabulary (overlap 0.85 → 0.44 on AH) and still
305
+ * landed inside noise, which is itself mildly interesting against the parent's 35pp
306
+ * vocabulary effect — but it is one arm at n = 24 and is not a claim.
307
+ * Read together with point 1: THE CAUSE IS NOT IN THE DESCRIPTION TEXT. There was, on the day,
308
+ * less to explain than the parent's number suggested, and what remains does not move when the
309
+ * text is cut in five different places.
310
+ *
311
+ * 4. THE A1 / A6 PAIR DOES NOT SEPARATE THE SKILL'S TWO HALVES. 50% (rubric-only) vs 42%
312
+ * (persona-only), Δ 8pp, deep inside noise; both oracles near-perfect (7/8 and 8/8). The pair
313
+ * cannot recommend a split and cannot rank the halves. Two honest observations around it:
314
+ * · A6 carries by far the largest vocabulary handicap of any arm (AH overlap 0.19 vs A1's 0.71,
315
+ * SH 0.35 vs 0.78 — it is a 225-character description) and still landed within noise of A1.
316
+ * If anything that favours the persona half, and it is one arm at n = 24, so it is a
317
+ * direction to test, not a result.
318
+ * · The carried-in observation stated in advance stands and cuts the other way: the persona
319
+ * clause is ALREADY in the shipping description and the SH prompt "the abstract reads like a
320
+ * wall of jargon" already scores overlap 1.00 against it. That single prompt scored, in
321
+ * TARGET-of-3: parent 0 · A0 0 · A1 0 · A2 1 · A3 3 · A4 2 · A5 0 · A6 3. A3 differs from A0
322
+ * by one deleted sentence and went 0 → 3; A5's description is BYTE-IDENTICAL to A0's and also
323
+ * went 0. That column is the instability of point 1 in miniature, and it is why no arm's
324
+ * 8pp is worth a mechanism.
325
+ *
326
+ * 5. WHAT DID REPRODUCE, LOUDLY: THE HARNESS-VS-ORACLE DEFICIT. The closed-book oracle scores 7/8 or
327
+ * 8/8 on every arm except the artifact one, i.e. the descriptions determine the answer for a
328
+ * reader with no other task; the harness delivers 42-54% on the same text. A 25-45pp gap, present
329
+ * in all six non-artifact arms, unchanged by any deletion. That is the parent's point 5 measured
330
+ * again on a different axis, and it is the finding these two files agree on. It is also the one
331
+ * that no description edit can fix, because six different descriptions produce it.
332
+ *
333
+ * ─────────────────────────────────────────────────────────────────────────────
334
+ * WHAT THIS RUN CANNOT SETTLE, AFTER THE RUN
335
+ *
336
+ * - 🔴 A CONFOUND NOT PRE-REGISTERED, AND IT SHOULD HAVE BEEN: THE ARMS RAN IN ORDER, NOT
337
+ * INTERLEAVED. A0 → A5 ran sequentially over roughly an hour, A6 later still. Point 1 proves the
338
+ * harness drifts by ~30pp between sessions on this exact prompt set, so drift is not hypothetical
339
+ * here — it is measured, and it aliases perfectly with arm order. Every Δ in the table above
340
+ * carries that. A correct version randomises or blocks arm order across trials; this one did not,
341
+ * and the flat result is the only reading robust to it (a drift explanation cannot manufacture
342
+ * flatness, only spurious differences).
343
+ * - WHY THE HARNESS DRIFTED. Point 1 measures the instability; nothing here explains it. Model
344
+ * routing, load, cache state and time of day are all uncontrolled and unlogged. A stability
345
+ * experiment — the same cell, the same day, N times spread over hours — is the obvious next run
346
+ * and is NOT this one.
347
+ * - WHETHER THE NAME MATTERS. A5 was supposed to answer this and instead found the cross-reference
348
+ * artifact (point 2). Open.
349
+ * - WHETHER A SPLIT WOULD HELP. Stated in advance and unchanged: A1/A6 measure two descriptions of
350
+ * one skill, not two skills with their own gate rows and their own consumers.
351
+ * - THE COMPETITOR SIGNAL IS THIN BUT NON-ZERO, AND IT WAS ZERO IN THE PARENT. 10 misfires across
352
+ * 168 runs (`tighten-paper` 7, `cold-read-diff` 3), concentrated in the arms that deleted text
353
+ * (A4 13%, A6 13%, A2 8%; A0 and A3 0%). Consistent with "deleting the NOT-clauses and the
354
+ * machinery lets neighbours take the run", too small to claim.
355
+ *
356
+ * COST, ACTUAL. 168 harness runs + 56 oracle picks + 24 replication runs = 248 model runs;
357
+ * ~$23.15 API-equivalent measured across the three logs ($17.10 + $2.29 + $3.76), billed to a
358
+ * subscription, $0 metered. Nothing in the design was cut to save money; A6 and the replication were
359
+ * ADDED after the six-arm design ran, and the setup diff was free.
360
+ *
361
+ * 🔴 NO SKILL.md WAS CHANGED BY THIS RUN EITHER. The two paragraphs citing the 12% figure are
362
+ * flagged in point 1 and left alone.
363
+ */
364
+
365
+ import { assertPromptDiversity, skip } from "vigiles";
366
+ import { paid_measureTriggerRate as measureTriggerRate } from "vigiles/eval";
367
+ import {
368
+ existsSync,
369
+ readFileSync,
370
+ writeFileSync,
371
+ mkdirSync,
372
+ cpSync,
373
+ rmSync,
374
+ renameSync,
375
+ realpathSync,
376
+ } from "node:fs";
377
+ import { join } from "node:path";
378
+ import { frontmatterBlock } from "../../lib/markdown.mjs";
379
+ import { parseFm } from "../../lib/skill-corpus.mjs";
380
+ import { installedSkills } from "./scripts/consumer.mjs";
381
+ import { execFileSync } from "node:child_process";
382
+ import { fileURLToPath } from "node:url";
383
+ import { createHash } from "node:crypto";
384
+
385
+ const ROOT = process.env.CLAUDE_PROJECT_DIR ?? process.cwd();
386
+ const SKILLS_DIR = join(ROOT, ".claude", "skills");
387
+ const REPRO = join(SKILLS_DIR, "paper-pipeline", "repro");
388
+ const PARENT = join(
389
+ SKILLS_DIR,
390
+ "paper-pipeline",
391
+ "framing-vs-vocabulary.eval.mjs",
392
+ );
393
+ const SCRATCH =
394
+ process.env.ABLATION_SCRATCH ??
395
+ "/tmp/claude-0/-home-user-mine/8268acb1-66a2-55ac-858e-2b9e3c81f84e/scratchpad/gpw-ablation";
396
+ const NS = "vigiles-loose-skills";
397
+ const TARGET = "grade-paper-writing";
398
+ const STEM = "2026-08-08-grade-paper-writing-ablation";
399
+
400
+ const argv = process.argv.slice(2);
401
+ const val = (n, d) => {
402
+ const i = argv.indexOf(`--${n}`);
403
+ return i >= 0 && argv[i + 1] ? argv[i + 1] : d;
404
+ };
405
+ const MODE = val("mode", "main");
406
+ const TRIALS = Number(val("trials", "3"));
407
+ const ONLY = (val("arms", "") || "")
408
+ .split(",")
409
+ .map((s) => s.trim())
410
+ .filter(Boolean);
411
+ if (!["main", "oracle", "preflight", "setupdiff"].includes(MODE))
412
+ throw new Error("--mode must be main | oracle | preflight | setupdiff");
413
+
414
+ // ── the fixture ──────────────────────────────────────────────────────────────
415
+ // Byte-identical to the parent's (and therefore to both of ITS siblings). If it drifts, a difference
416
+ // between this run and the 12% it exists to explain stops being attributable to the descriptions.
417
+ const FIXTURE = {
418
+ "paper/paper.md": [
419
+ "# Prose Isn't Policy: Measuring Whether Agent-Config Rules Are Enforceable",
420
+ "",
421
+ "## Abstract",
422
+ "Agent configuration files state rules in prose and assume the model obeys them. We compile a",
423
+ "corpus of real rules and measure what fraction can be mechanically enforced. We find that 84%",
424
+ "of rules in our corpus are enforceable, and that LLM-authored checkers for the remainder leak",
425
+ "silently in 84-96% of adversarial cases \\cite{greshake2023}.",
426
+ "",
427
+ "## 1 Introduction",
428
+ "Every agent harness ships a natural-language rulebook. Nothing checks it. This is the same",
429
+ "mistake as a code comment that claims an invariant no test enforces \\cite{thompson1984}.",
430
+ "",
431
+ "## 2 Method",
432
+ "We gather rules from public repositories, classify each by enforceability, and build a",
433
+ "two-stage adversarial gate that validates a synthesized rule against a blind gold set.",
434
+ "",
435
+ "## 3 Results",
436
+ "See Table 1. The headline number is 84%.",
437
+ "",
438
+ "## 4 Discussion",
439
+ "The result generalizes beyond our corpus in the sense that the mechanism is not corpus-specific,",
440
+ "though of course the specific percentages are, and it is important to note in this context that",
441
+ "the framing itself may be what carries, rather than the measurement.",
442
+ "",
443
+ "## 5 Threats to Validity",
444
+ "Our corpus is drawn from public repositories and may not represent private configurations.",
445
+ "",
446
+ "## 6 Related Work",
447
+ "TODO",
448
+ "",
449
+ "## 7 Conclusion",
450
+ "Prose is not policy.",
451
+ ].join("\n"),
452
+ "paper/repro/README.md":
453
+ "# Reproduction artifact\n\n`python3 paper_numbers.py` recomputes every bolded figure in paper.md.\n",
454
+ "paper/PIPELINE-STATUS.md":
455
+ "# Pipeline status\n\n| gate | state |\n|---|---|\n| numbers | pass |\n| structure | not run |\n| citations | not run |\n",
456
+ };
457
+
458
+ // ── the prompts ──────────────────────────────────────────────────────────────
459
+ // COPIED VERBATIM from framing-vs-vocabulary.eval.mjs, CASES[1] (grade-paper-writing), cells AH and
460
+ // SH. Not paraphrased, not extended, not re-ordered. Preflight #2 proves it by substring against the
461
+ // parent's source — inventing a prompt here would make this run incomparable to the 25%/17% it exists
462
+ // to explain, which is the only reason the numbers mean anything.
463
+ const PROMPTS = {
464
+ AH: [
465
+ "grade the writing craft in this draft and name the offending sentence in each dimension",
466
+ "run the blind multi-grader panel over the prose clarity and jargon discipline",
467
+ "score the title, the abstract and the intro architecture one to five",
468
+ "grade how well this reads, hedge stacking included, and give me the fix for each",
469
+ ],
470
+ SH: [
471
+ "the abstract reads like a wall of jargon",
472
+ "the intro architecture is fine but the prose clarity is not",
473
+ "there is hedge stacking everywhere and the point never lands",
474
+ "the writing does not read well even though the draft is finished",
475
+ ],
476
+ };
477
+ const CELLS = ["AH", "SH"];
478
+ // Next to this sat `CELL_LABEL = { AH: "ACTION / HIGH overlap", SH: "SITUATION / HIGH overlap" }`,
479
+ // which nobody called (2026-08-28). The report prints the bare keys `AH`/`SH`, that is, the
480
+ // expansion of the cells was written and never wired up. Deleted; to wire it up, put it in the
481
+ // table header below.
482
+ const allPrompts = CELLS.flatMap((k) =>
483
+ PROMPTS[k].map((p) => ({ cell: k, prompt: p })),
484
+ );
485
+
486
+ // ── the baseline description, read from disk (never hardcoded — it would drift) ──
487
+ if (!existsSync(join(SKILLS_DIR, TARGET, "SKILL.md")))
488
+ throw new Error(`${TARGET} is not installed under ${SKILLS_DIR}`);
489
+ // READING a field is a parser's job. The hand-rolled version below used a lookahead
490
+ // for "the next key" to find where a value ended, which is a guess about YAML that
491
+ // YAML does not have to honour: a folded (`>`) or literal (`|`) block, a quoted
492
+ // value containing a colon, or a value carried onto a continuation line all end
493
+ // somewhere else. `parseFm` is the corpus reader — strict, and loud about why.
494
+ const fmOf = (md) => {
495
+ const block = frontmatterBlock(md);
496
+ return block === null ? {} : parseFm(block, "ablation fixture");
497
+ };
498
+ const parseDesc = (md) =>
499
+ String(fmOf(md).description ?? "")
500
+ .replace(/\s+/g, " ")
501
+ .trim();
502
+ const parseName = (md) => fmOf(md).name;
503
+ const BASE_MD = readFileSync(join(SKILLS_DIR, TARGET, "SKILL.md"), "utf-8");
504
+ const A0 = parseDesc(BASE_MD);
505
+ if (parseName(BASE_MD) !== TARGET)
506
+ throw new Error(`${TARGET}/SKILL.md declares name: ${parseName(BASE_MD)}`);
507
+
508
+ // ── the mutations, as EXACT SUBSTRING DELETIONS from A0 ──────────────────────
509
+ // Deletion, never rewriting. A rewritten clause could introduce a word A0 does not contain, and
510
+ // "we added vocabulary" is a known-working fix (the parent measured vocabulary at 35pp) and NOT the
511
+ // question. Guard #5 below enforces that mechanically; these strings only make it easy to satisfy.
512
+ const CUT_A1_AT = "naming the offending sentence and the fix for each.";
513
+ const D_SUBORDINATION = [
514
+ " whose stall inventory — not the rubric number — is the readability gate other skills consume (pc-panel-review, paper-adversarial-review, harden-paper)",
515
+ "harden-paper (which calls it), ",
516
+ ];
517
+ const D_NEGATION = [
518
+ " NOT a content/defect review (paper-adversarial-review / pc-panel-review), NOT a structural cut plan (tighten-paper — run that first on a bloated draft), NOT venue-bar content strength (study-accepted-papers).",
519
+ ];
520
+ const D_MACHINERY = [
521
+ " (Title, Abstract, Intro architecture, Structure, Prose clarity, Jargon discipline, Landing-the-point, Figure economy, Honesty-without-hedge-stacking)",
522
+ " (Peyton Jones, McEnerney, Gopen & Swan; exemplars Trusting Trust, Carlini, Greshake)",
523
+ " Runs the PERSONA cold-read stall pass (a committed non-academic persona subagent, per-section) whose stall inventory — not the rubric number — is the readability gate other skills consume (pc-panel-review, paper-adversarial-review, harden-paper).",
524
+ " Defaults to a blind multi-grader panel; after fixes, mandates the claims-preservation diff (row `claims`).",
525
+ ];
526
+ // NOTE ON "FOUR NOT-CLAUSES": the description carries THREE uppercase "NOT …" delegation clauses
527
+ // (all in one sentence) plus one lowercase "not the rubric number", which is not a delegation clause
528
+ // at all — it is part of the subordination clause and is removed by A2, not by A3. A3 deletes the
529
+ // delegation sentence entire. This is stated because the count "4 negations" comes from a
530
+ // case-insensitive tally and would otherwise look like a missing deletion.
531
+
532
+ const applyCuts = (s, cuts) => {
533
+ let out = s;
534
+ for (const c of cuts) {
535
+ if (!out.includes(c))
536
+ throw new Error(
537
+ `deletion string not found in description: ${c.slice(0, 60)}…`,
538
+ );
539
+ out = out.replace(c, "");
540
+ }
541
+ return out.replace(/\s+/g, " ").trim();
542
+ };
543
+
544
+ // A6 keeps the persona sentence as the skill's whole job: the opening sentence MINUS the
545
+ // `"grade the writing"` trigger (that trigger names the rubric half), plus the persona clause with
546
+ // its subordination tail removed — see the header for why that tail goes.
547
+ const D_PERSONA_ONLY = [
548
+ ' / "grade the writing"',
549
+ " Grades WRITING CRAFT only — nine dimensions 1–5 (Title, Abstract, Intro architecture, Structure, Prose clarity, Jargon discipline, Landing-the-point, Figure economy, Honesty-without-hedge-stacking) against how the best-written papers read (Peyton Jones, McEnerney, Gopen & Swan; exemplars Trusting Trust, Carlini, Greshake), naming the offending sentence and the fix for each.",
550
+ " whose stall inventory — not the rubric number — is the readability gate other skills consume (pc-panel-review, paper-adversarial-review, harden-paper)",
551
+ " Defaults to a blind multi-grader panel; after fixes, mandates the claims-preservation diff (row `claims`).",
552
+ " NOT a content/defect review (paper-adversarial-review / pc-panel-review), NOT a structural cut plan (tighten-paper — run that first on a bloated draft), NOT venue-bar content strength (study-accepted-papers).",
553
+ " Compose with harden-paper (which calls it), draft-paper (generative counterpart), render-paper.",
554
+ ];
555
+
556
+ const ARMS = [
557
+ { id: "A0", label: "baseline (verbatim)", desc: A0 },
558
+ {
559
+ id: "A1",
560
+ label: "short (sentences 1-2 only)",
561
+ desc: A0.slice(0, A0.indexOf(CUT_A1_AT) + CUT_A1_AT.length),
562
+ },
563
+ { id: "A2", label: "no-subordination", desc: applyCuts(A0, D_SUBORDINATION) },
564
+ { id: "A3", label: "no-negation", desc: applyCuts(A0, D_NEGATION) },
565
+ { id: "A4", label: "no-machinery", desc: applyCuts(A0, D_MACHINERY) },
566
+ {
567
+ id: "A5",
568
+ label: "renamed (description identical)",
569
+ desc: A0,
570
+ rename: "paper-readability-review",
571
+ },
572
+ {
573
+ id: "A6",
574
+ label: "persona-only (A1's mirror)",
575
+ desc: applyCuts(A0, D_PERSONA_ONLY),
576
+ },
577
+ ];
578
+ for (const a of ARMS) a.skill = a.rename ?? TARGET;
579
+ const RUN_ARMS = ONLY.length ? ARMS.filter((a) => ONLY.includes(a.id)) : ARMS;
580
+ if (ONLY.length && RUN_ARMS.length !== ONLY.length)
581
+ throw new Error(`--arms named an arm that does not exist: ${ONLY.join(",")}`);
582
+
583
+ // ── PREFLIGHT ────────────────────────────────────────────────────────────────
584
+ // Every guard fails LOUDLY and BEFORE a token is spent. The failure this block exists to prevent is
585
+ // the parent's: a wrong id or a bad mutation makes `fired` permanently false and the run then prints
586
+ // a wall of confident 0.00s that read like a finding.
587
+
588
+ // 1. the roster is what the parent measured
589
+ // Through `installedSkills`, which follows the links `paperlint init` makes (rpp#62).
590
+ const installed = installedSkills(SKILLS_DIR);
591
+ const installedSet = new Set(installed);
592
+ if (installedSet.has(ARMS[5].rename))
593
+ throw new Error(
594
+ `A5's rename target ${ARMS[5].rename} already exists — it would collide`,
595
+ );
596
+
597
+ // 2. 🔴 THE PROMPTS ARE THE PARENT'S, PROVEN BY SUBSTRING. The whole comparability of this run to the
598
+ // 12% rests on it, and "I copied them carefully" is not a check.
599
+ if (!existsSync(PARENT)) throw new Error(`parent eval not found: ${PARENT}`);
600
+ const parentSrc = readFileSync(PARENT, "utf-8");
601
+ for (const { prompt } of allPrompts)
602
+ if (!parentSrc.includes(prompt))
603
+ throw new Error(
604
+ `PROMPT NOT VERBATIM FROM THE PARENT: "${prompt}". This file must not invent prompts — an ` +
605
+ `invented prompt makes every arm incomparable to the 25%/17% it exists to explain.`,
606
+ );
607
+ const seen = new Set();
608
+ for (const { prompt } of allPrompts) {
609
+ if (seen.has(prompt)) throw new Error(`duplicate prompt: "${prompt}"`);
610
+ seen.add(prompt);
611
+ }
612
+ for (const k of CELLS)
613
+ assertPromptDiversity(PROMPTS[k], {
614
+ minPrompts: 4,
615
+ minDistance: 0.3,
616
+ label: `${TARGET}:${k}`,
617
+ });
618
+
619
+ // 3. the fixture is the parent's, byte for byte
620
+ for (const [path, body] of Object.entries(FIXTURE))
621
+ if (!parentSrc.includes(body.split("\n")[0]))
622
+ throw new Error(`fixture drifted from the parent's: ${path}`);
623
+
624
+ // ── the vocabulary machinery, COPIED VERBATIM from the parent ────────────────
625
+ // Guard #4 below re-reads both files and fails if this block and the parent's have diverged. A second
626
+ // independent stemmer would mean the subset guard and the parent's overlap numbers measure different
627
+ // things while looking like the same metric.
628
+ const STOP = new Set(
629
+ (
630
+ "a an the and or but if then than that this these those there here it its is are was were be been being am " +
631
+ "do does did doing done have has had having i me my we our you your he she they them their of in on at to " +
632
+ "for with by from as into over under about after before between out up down off again more most some any " +
633
+ "no not nor only own same so too very can will just should now what which who whom whose when where why how " +
634
+ "me myself yourself each both few other such all one two three back keep still even yet get got give given " +
635
+ "make made want need know knew cannot could would might must let us like also because while against would"
636
+ ).split(/\s+/),
637
+ );
638
+ const stem = (w) =>
639
+ w
640
+ .replace(/(ies)$/, "y")
641
+ .replace(/(sses|shes|ches|xes)$/, (m) => m.slice(0, -2))
642
+ .replace(/(ing|ed|ly|s)$/, "");
643
+ const words = (s) =>
644
+ new Set(
645
+ s
646
+ .toLowerCase()
647
+ .replace(/[^a-zа-яё\s-]/gi, " ")
648
+ .split(/[\s-]+/)
649
+ .filter((w) => w.length >= 3 && !STOP.has(w))
650
+ .map(stem)
651
+ .filter((w) => w.length >= 3),
652
+ );
653
+
654
+ const descriptions = {}; // baseline roster; per-arm rosters swap the one entry (oracle mode)
655
+ for (const name of installed)
656
+ descriptions[name] = parseDesc(
657
+ readFileSync(join(SKILLS_DIR, name, "SKILL.md"), "utf-8"),
658
+ );
659
+
660
+ // 4. THE MACHINERY IS THE PARENT'S, PROVEN BY COMPARING BOTH SOURCES
661
+ const MARK_A = "const STOP = new Set(";
662
+ const MARK_B = "const descriptions = {};";
663
+ const block = (src) => {
664
+ const i = src.indexOf(MARK_A);
665
+ const j = src.indexOf(MARK_B, i);
666
+ if (i < 0 || j < 0) throw new Error("vocabulary machinery markers not found");
667
+ return src.slice(i, j).replace(/\s+/g, " ").trim();
668
+ };
669
+ const mySrc = readFileSync(fileURLToPath(import.meta.url), "utf-8");
670
+ if (block(mySrc) !== block(parentSrc))
671
+ throw new Error(
672
+ "VOCABULARY MACHINERY HAS DIVERGED from framing-vs-vocabulary.eval.mjs. The subset guard and the " +
673
+ "parent's overlap numbers must be the same metric; two stemmers that look alike are worse than one.",
674
+ );
675
+
676
+ const overlap = (prompt, desc) => {
677
+ const p = words(prompt);
678
+ if (p.size === 0) return 0;
679
+ const d = words(desc);
680
+ let hit = 0;
681
+ for (const w of p) if (d.has(w)) hit++;
682
+ return hit / p.size;
683
+ };
684
+
685
+ // 5. 🔴 THE SUBSET GUARD — the one that decides whether this experiment is measuring its factor.
686
+ // A1-A4 must each be a STRICT SUBSET of A0's stemmed content-word set: deletions only, no word added.
687
+ // A5 must be EQUAL: it changes the name, nothing else. An arm that ADDS a word is measuring "we added
688
+ // vocabulary", which the parent already measured at 35pp and which is a known-working fix, not this
689
+ // question.
690
+ const W0 = words(A0);
691
+ const deltas = {};
692
+ for (const a of ARMS) {
693
+ const w = words(a.desc);
694
+ const added = [...w].filter((x) => !W0.has(x));
695
+ const removed = [...W0].filter((x) => !w.has(x));
696
+ deltas[a.id] = { size: w.size, added, removed };
697
+ if (a.id === "A0") {
698
+ if (added.length || removed.length)
699
+ throw new Error("A0 is not the baseline — it differs from A0");
700
+ continue;
701
+ }
702
+ if (a.id === "A5") {
703
+ if (a.desc !== A0)
704
+ throw new Error("A5's description must be BYTE-IDENTICAL to A0");
705
+ if (added.length || removed.length)
706
+ throw new Error(
707
+ `A5's word set must EQUAL A0's (+${added.length}/-${removed.length})`,
708
+ );
709
+ continue;
710
+ }
711
+ if (added.length)
712
+ throw new Error(
713
+ `ARM ${a.id} ADDS ${added.length} content word(s) not in A0: ${added.join(", ")}. That arm would ` +
714
+ `measure added vocabulary, which is the parent's 35pp factor and a known-working fix — not the ` +
715
+ `question. Mutations must be pure deletions.`,
716
+ );
717
+ if (!removed.length)
718
+ throw new Error(`ARM ${a.id} removes nothing — it is a duplicate of A0`);
719
+ }
720
+ // A1's specific claim: a literal prefix of A0.
721
+ if (!A0.startsWith(ARMS[1].desc)) throw new Error("A1 is not a prefix of A0");
722
+
723
+ // 6. the harness itself
724
+ if (MODE !== "preflight") {
725
+ try {
726
+ execFileSync("claude", ["--version"], { stdio: "ignore" });
727
+ } catch {
728
+ skip(
729
+ "`claude` CLI not on PATH — the eval tier drives the real harness and cannot be faked",
730
+ );
731
+ }
732
+ }
733
+
734
+ // ── build one arm's skills dir (a COPY; the real one is never written) ───────
735
+ function buildArm(a) {
736
+ const dir = join(SCRATCH, `arm-${a.id}`);
737
+ rmSync(dir, { recursive: true, force: true });
738
+ mkdirSync(dir, { recursive: true });
739
+ // 🔴 ONE SKILL AT A TIME, FROM ITS REALPATH — not `cpSync(SKILLS_DIR, dir, { recursive })`.
740
+ // In a consumer every entry of SKILLS_DIR is a link `paperlint init` made (rpp#62), and `cpSync` copies
741
+ // a link as a link, rewritten to an ABSOLUTE path into the original; `dereference: true` does not
742
+ // change that for nested entries (measured, Node 22.22). The arm's `writeFileSync` below would
743
+ // then land in the real SKILL.md — "the real one is never written" would be false in exactly the
744
+ // setup this eval exists for. Copying each skill's resolved directory makes the arm a real copy.
745
+ for (const name of installed)
746
+ cpSync(realpathSync(join(SKILLS_DIR, name)), join(dir, name), {
747
+ recursive: true,
748
+ });
749
+ const srcDir = join(dir, TARGET);
750
+ const outDir = a.rename ? join(dir, a.rename) : srcDir;
751
+ let md = readFileSync(join(srcDir, "SKILL.md"), "utf-8");
752
+ // WRITING is the opposite case, and a parser is the wrong tool: `yaml.dump` would
753
+ // reformat the whole block, so the fixture would differ from the real file in ways
754
+ // the ablation is not measuring. The line is READ OUT of the file instead of
755
+ // assumed — the idiom skills.mutations.mjs already uses — and a missing line is an
756
+ // error rather than a silent no-op, because a mutation that does not apply scores
757
+ // as a pass over a fixture nobody changed.
758
+ const setLine = (text, key, value) => {
759
+ const line = new RegExp(`^${key}:.*\\n`, "m").exec(text); // kb-lint:markdown-regex-ok — editing a LINE, not parsing
760
+ if (!line)
761
+ throw new Error(
762
+ `FIXTURE TARGET NOT FOUND: no \`${key}:\` line in ${TARGET}/SKILL.md`,
763
+ );
764
+ return text.replace(line[0], `${key}: ${value}\n`);
765
+ };
766
+ md = setLine(md, "description", a.desc);
767
+ if (a.rename) {
768
+ md = setLine(md, "name", a.rename);
769
+ renameSync(srcDir, outDir);
770
+ }
771
+ writeFileSync(join(outDir, "SKILL.md"), md);
772
+ // verify what landed on disk, not what we meant to write
773
+ const back = readFileSync(join(outDir, "SKILL.md"), "utf-8");
774
+ if (parseDesc(back) !== a.desc)
775
+ throw new Error(
776
+ `arm ${a.id}: description did not round-trip through SKILL.md`,
777
+ );
778
+ if (parseName(back) !== a.skill)
779
+ throw new Error(
780
+ `arm ${a.id}: name is ${parseName(back)}, expected ${a.skill}`,
781
+ );
782
+ const n = installedSkills(dir).length;
783
+ if (n !== installed.length)
784
+ throw new Error(
785
+ `arm ${a.id}: ${n} skills installed, baseline has ${installed.length}`,
786
+ );
787
+ return dir;
788
+ }
789
+
790
+ // ── the labelling matrix, printed every run so the arms are auditable ────────
791
+ console.log(
792
+ `\n${installed.length} skills installed → ${installed.length - 1} competitors per run`,
793
+ );
794
+ console.log(
795
+ `\nARMS — description length, word-set delta vs A0, mean prompt overlap`,
796
+ );
797
+ console.log(
798
+ `${"arm".padEnd(4)}${"label".padEnd(34)}${"chars".padStart(7)}${"words".padStart(7)}${"−".padStart(6)}${"+".padStart(4)}${"ovAH".padStart(7)}${"ovSH".padStart(7)}`,
799
+ );
800
+ for (const a of ARMS) {
801
+ const d = deltas[a.id];
802
+ const ov = (k) =>
803
+ PROMPTS[k].reduce((s, p) => s + overlap(p, a.desc), 0) / PROMPTS[k].length;
804
+ console.log(
805
+ `${a.id.padEnd(4)}${a.label.padEnd(34)}${String(a.desc.length).padStart(7)}${String(d.size).padStart(7)}` +
806
+ `${String(d.removed.length).padStart(6)}${String(d.added.length).padStart(4)}` +
807
+ `${ov("AH").toFixed(2).padStart(7)}${ov("SH").toFixed(2).padStart(7)}`,
808
+ );
809
+ }
810
+ console.log(
811
+ `\n⚠ ovAH/ovSH FALL AS TEXT IS DELETED — caveat 2 in the header, quantified. The deletion arms carry\n` +
812
+ ` a vocabulary handicap against the parent's dominant factor. A rising deletion arm rose DESPITE it;\n` +
813
+ ` a flat one may be a real gain cancelled by it, and this design cannot tell. A5 is the only arm\n` +
814
+ ` with zero handicap (identical word set).`,
815
+ );
816
+
817
+ // ── SETUPDIFF — free, and it exists because a control that contradicts its parent is a setup bug
818
+ // until proven otherwise. Three checks the coordinator asked for, all mechanical:
819
+ // 1. the harness OPTIONS this file passes vs the ones the parent passes, pulled out of the parent's
820
+ // own source rather than eyeballed;
821
+ // 2. a checksum of EVERY arm dir's SKILL.md frontmatter against the real one — a copy that altered
822
+ // one competitor's description would change the roster and invalidate the comparison;
823
+ // 3. the parent's 24 grade-paper-writing AH+SH rows next to this file's 24 A0 rows, PROMPT BY
824
+ // PROMPT — a disagreement concentrated in one prompt means something different from one spread
825
+ // evenly across eight.
826
+ if (MODE === "setupdiff") {
827
+ const OPTS = [
828
+ "allowedTools",
829
+ "trials",
830
+ "concurrency",
831
+ "spacingSec",
832
+ "timeoutMs",
833
+ "minPrompts",
834
+ "fixture",
835
+ "skillsDir",
836
+ ];
837
+ const grab = (src, key) => {
838
+ const m = new RegExp(`\\b${key}:\\s*([^,\\n]+)`).exec(src);
839
+ return m ? m[1].trim() : "—";
840
+ };
841
+ console.log(
842
+ `\nHARNESS OPTIONS — this file vs the parent (extracted from both sources)`,
843
+ );
844
+ console.log(`${"option".padEnd(14)}${"ablation".padEnd(34)}parent`);
845
+ for (const k of OPTS)
846
+ console.log(
847
+ `${k.padEnd(14)}${grab(mySrc, k).slice(0, 32).padEnd(34)}${grab(parentSrc, k).slice(0, 40)}`,
848
+ );
849
+ const fxHash = (s) =>
850
+ createHash("sha256").update(JSON.stringify(s)).digest("hex").slice(0, 16);
851
+ console.log(`\nfixture sha256/16 ablation ${fxHash(FIXTURE)}`);
852
+ const pf = /const FIXTURE = \{[\s\S]*?\n\};/.exec(parentSrc)[0];
853
+ const mf = /const FIXTURE = \{[\s\S]*?\n\};/.exec(mySrc)[0];
854
+ console.log(
855
+ `FIXTURE source block identical to the parent's: ${pf === mf ? "YES" : "🔴 NO"}`,
856
+ );
857
+ // Normalise indentation AND the one expression that must differ (the id each matches), then compare.
858
+ const norm = (s) =>
859
+ s
860
+ .replace(/id\(c\.skill\)|wanted/g, "X")
861
+ .replace(/\s+/g, " ")
862
+ .trim();
863
+ const grabFired = (src) =>
864
+ (
865
+ /fired: \(t\) => \{[\s\S]*?\},\s*\n\s*fixture:/.exec(src)?.[0] ?? ""
866
+ ).replace(/fixture:$/, "");
867
+ const pFired = grabFired(parentSrc);
868
+ const mFired = grabFired(mySrc);
869
+ const same = pFired && norm(pFired) === norm(mFired);
870
+ console.log(
871
+ `fired predicate identical (modulo the id it matches): ${same ? "YES" : "🔴 NO"}`,
872
+ );
873
+ if (!same)
874
+ console.log(` parent: ${norm(pFired)}\n mine : ${norm(mFired)}`);
875
+ // ^-anchored: an unanchored match would find this very check line before the definition.
876
+ const grabHelper = (src) =>
877
+ /^const firedSkills = \(t\) => \[[\s\S]*?\n\];/m.exec(src)?.[0] ?? "";
878
+ console.log(
879
+ `firedSkills() helper identical: ${
880
+ grabHelper(parentSrc) && grabHelper(parentSrc) === grabHelper(mySrc)
881
+ ? "YES"
882
+ : "🔴 NO"
883
+ }`,
884
+ );
885
+
886
+ console.log(
887
+ `\nARM DIR INTEGRITY — sha256/16 of each SKILL.md frontmatter vs the real .claude/skills`,
888
+ );
889
+ const fmHash = (p) =>
890
+ createHash("sha256")
891
+ .update(/^---\n[\s\S]*?\n---/.exec(readFileSync(p, "utf-8"))?.[0] ?? "")
892
+ .digest("hex")
893
+ .slice(0, 16);
894
+ const realFm = Object.fromEntries(
895
+ installed.map((n) => [n, fmHash(join(SKILLS_DIR, n, "SKILL.md"))]),
896
+ );
897
+ for (const a of ARMS) {
898
+ const dir = buildArm(a);
899
+ const names = installedSkills(dir);
900
+ const diffs = [];
901
+ for (const n of names) {
902
+ const h = fmHash(join(dir, n, "SKILL.md"));
903
+ if (n === a.skill) continue; // the one deliberate mutation
904
+ if (realFm[n] !== h) diffs.push(n);
905
+ }
906
+ const missing = installed.filter((n) => n !== TARGET && !names.includes(n));
907
+ console.log(
908
+ ` ${a.id} ${names.length} skills · unintended frontmatter diffs: ${diffs.length ? `🔴 ${diffs.join(", ")}` : "none"}` +
909
+ (missing.length ? ` · MISSING: ${missing.join(", ")}` : ""),
910
+ );
911
+ }
912
+
913
+ const mineP = join(REPRO, `${STEM}.json`);
914
+ const parentP = join(REPRO, "2026-08-08-framing-vs-vocabulary.json");
915
+ if (existsSync(mineP) && existsSync(parentP)) {
916
+ const mine = JSON.parse(readFileSync(mineP, "utf-8")).results.filter(
917
+ (r) => r.arm === "A0",
918
+ );
919
+ const par = JSON.parse(readFileSync(parentP, "utf-8")).results.filter(
920
+ (r) => r.skill === TARGET && (r.cell === "AH" || r.cell === "SH"),
921
+ );
922
+ console.log(
923
+ `\nROW BY ROW — parent (${par.length} prompts) vs this file's A0 (${mine.length} prompts)`,
924
+ );
925
+ console.log(
926
+ `${"cell".padEnd(5)}${"parent".padStart(8)}${"A0".padStart(8)}${"Δ".padStart(7)} prompt`,
927
+ );
928
+ let pT = 0,
929
+ mT = 0;
930
+ for (const p of par) {
931
+ const m = mine.find((x) => x.prompt === p.prompt);
932
+ const pt = p.buckets.filter((b) => b === "TARGET").length;
933
+ const mt = m ? m.buckets.filter((b) => b === "TARGET").length : NaN;
934
+ pT += pt;
935
+ mT += mt;
936
+ console.log(
937
+ `${p.cell.padEnd(5)}${`${pt}/3`.padStart(8)}${`${mt}/3`.padStart(8)}${`${mt - pt > 0 ? "+" : ""}${mt - pt}`.padStart(7)} ${p.prompt.slice(0, 54)}`,
938
+ );
939
+ }
940
+ console.log(
941
+ `${"".padEnd(5)}${`${pT}/24`.padStart(8)}${`${mT}/24`.padStart(8)}${`+${mT - pT}`.padStart(7)} TOTAL`,
942
+ );
943
+ const moved = par.filter((p) => {
944
+ const m = mine.find((x) => x.prompt === p.prompt);
945
+ return (
946
+ m &&
947
+ Math.abs(
948
+ m.buckets.filter((b) => b === "TARGET").length -
949
+ p.buckets.filter((b) => b === "TARGET").length,
950
+ ) >= 2
951
+ );
952
+ }).length;
953
+ console.log(
954
+ `\n ${moved} of 8 prompts moved by ≥2 of 3 trials. Spread across many prompts ⇒ a global shift\n` +
955
+ ` (session, load, model routing). Concentrated in one or two ⇒ something prompt-specific.`,
956
+ );
957
+ } else {
958
+ console.log(
959
+ `\n(row-by-row skipped: need both ${STEM}.json and the parent's json)`,
960
+ );
961
+ }
962
+ console.log(`\nSETUPDIFF COMPLETE. No tokens spent.`);
963
+ process.exit(0);
964
+ }
965
+
966
+ if (MODE === "preflight") {
967
+ console.log(
968
+ `\nWORD-SET DELTAS (stemmed content words removed from A0 — none may be added)`,
969
+ );
970
+ for (const a of ARMS) {
971
+ const d = deltas[a.id];
972
+ console.log(`\n ${a.id} ${a.label}`);
973
+ console.log(
974
+ ` removed (${d.removed.length}): ${d.removed.join(" ") || "—"}`,
975
+ );
976
+ console.log(
977
+ ` added (${d.added.length}): ${d.added.join(" ") || "—"}`,
978
+ );
979
+ }
980
+ console.log(`\nRESULTING DESCRIPTIONS`);
981
+ for (const a of ARMS)
982
+ console.log(
983
+ `\n ── ${a.id} (${a.skill}, ${a.desc.length} chars)\n ${a.desc}`,
984
+ );
985
+ console.log(
986
+ `\nbuilding each arm's skills dir under ${SCRATCH} (no real file is written) …`,
987
+ );
988
+ for (const a of ARMS) console.log(` ${a.id} → ${buildArm(a)}`);
989
+ console.log(
990
+ `\nALL PREFLIGHT GUARDS PASSED. No tokens spent. Run --mode main next.`,
991
+ );
992
+ process.exit(0);
993
+ }
994
+
995
+ // ── ORACLE MODE ──────────────────────────────────────────────────────────────
996
+ // The parent's implementation, per arm: closed book, no harness, no skills installed, the arm's 37
997
+ // name+description pairs pasted in, one pick, NONE allowed.
998
+ if (MODE === "oracle") {
999
+ const oraclePath = join(REPRO, `${STEM}-oracle.json`);
1000
+ // MERGE, never clobber: an arm added later must not cost a re-run of the arms already paid for.
1001
+ const rows =
1002
+ ONLY.length && existsSync(oraclePath)
1003
+ ? JSON.parse(readFileSync(oraclePath, "utf-8")).filter(
1004
+ (r) => !ONLY.includes(r.arm),
1005
+ )
1006
+ : [];
1007
+ console.log(
1008
+ `\noracle: ${RUN_ARMS.length} arm(s) x ${allPrompts.length} closed-book picks over ${installed.length} descriptions\n`,
1009
+ );
1010
+ for (const a of RUN_ARMS) {
1011
+ const names = a.rename
1012
+ ? installed.map((s) => (s === TARGET ? a.rename : s))
1013
+ : installed;
1014
+ const nameSet = new Set(names);
1015
+ const desc = { ...descriptions, [a.skill]: a.desc };
1016
+ if (a.rename) delete desc[TARGET];
1017
+ const roster = names.map((s) => `- ${s}: ${desc[s]}`).join("\n");
1018
+ console.log(`=== ${a.id} ${a.label}`);
1019
+ for (const { cell, prompt } of allPrompts) {
1020
+ const q =
1021
+ `Below is a list of agent skills, each with the description its author wrote.\n\n${roster}\n\n` +
1022
+ `A user typed exactly this:\n\n"${prompt}"\n\n` +
1023
+ `Which ONE of the skills above should be invoked? Reply with the skill name alone, ` +
1024
+ `or the single word NONE if no skill clearly applies. No explanation.`;
1025
+ let pick; // no initializer: both try/catch branches assign it (2026-08-28)
1026
+ try {
1027
+ const out = execFileSync("claude", ["-p", q, "--model", "sonnet"], {
1028
+ encoding: "utf-8",
1029
+ timeout: 180000,
1030
+ maxBuffer: 1 << 24,
1031
+ });
1032
+ const t = out
1033
+ .trim()
1034
+ .split("\n")
1035
+ .pop()
1036
+ .trim()
1037
+ .replace(/[.`"']/g, "");
1038
+ pick = nameSet.has(t)
1039
+ ? t
1040
+ : /^none$/i.test(t)
1041
+ ? "NONE"
1042
+ : `?${t.slice(0, 40)}`;
1043
+ } catch (e) {
1044
+ pick = `ERROR:${String(e.message).slice(0, 60)}`;
1045
+ }
1046
+ const ok = pick === a.skill;
1047
+ rows.push({ arm: a.id, label: a.label, cell, prompt, pick, ok });
1048
+ console.log(
1049
+ ` ${cell} ${ok ? "HIT " : "miss"} ${pick.padEnd(26)} ${prompt.slice(0, 58)}`,
1050
+ );
1051
+ }
1052
+ }
1053
+ console.log(
1054
+ `\nORACLE ACCURACY BY ARM (closed book, ${installed.length} descriptions in context, 1 pick each)`,
1055
+ );
1056
+ console.log(
1057
+ `${"arm".padEnd(4)}${"label".padEnd(34)} hit AH SH none wrong`,
1058
+ );
1059
+ for (const a of ARMS.filter((a) => rows.some((r) => r.arm === a.id))) {
1060
+ const r = rows.filter((x) => x.arm === a.id);
1061
+ const c = (k) => r.filter((x) => x.cell === k);
1062
+ const h = (xs) => xs.filter((x) => x.ok).length;
1063
+ const none = r.filter((x) => x.pick === "NONE").length;
1064
+ console.log(
1065
+ `${a.id.padEnd(4)}${a.label.padEnd(34)} ${String(h(r)).padStart(2)}/${r.length} ${String(h(c("AH"))).padStart(2)}/4 ` +
1066
+ `${String(h(c("SH"))).padStart(2)}/4 ${String(none).padStart(4)} ${String(r.length - h(r) - none).padStart(5)}`,
1067
+ );
1068
+ }
1069
+ mkdirSync(REPRO, { recursive: true });
1070
+ rows.sort((x, y) => x.arm.localeCompare(y.arm));
1071
+ writeFileSync(oraclePath, JSON.stringify(rows, null, 2));
1072
+ console.log(`\nrows → repro/${STEM}-oracle.json`);
1073
+ console.log(
1074
+ `\nREAD AS AN UPPER BOUND, NOT A HUMAN, and read it AGAINST the harness arms: an arm where the\n` +
1075
+ `oracle moves WITH the harness changed what is recoverable from the text; an arm where the oracle\n` +
1076
+ `is flat and the harness moves changed what the harness DOES with the same recoverable text.`,
1077
+ );
1078
+ process.exit(0);
1079
+ }
1080
+
1081
+ // ── MAIN ─────────────────────────────────────────────────────────────────────
1082
+ // One measureTriggerRate call PER PROMPT, as the parent does: the predicate sees a Trace but not which
1083
+ // prompt produced it, so per-prompt attribution of WHAT FIRED INSTEAD is only available here.
1084
+ const firedSkills = (t) => [
1085
+ ...new Set(
1086
+ t.toolCalls
1087
+ .filter(
1088
+ (c) =>
1089
+ c.name === "Skill" &&
1090
+ !c.isError &&
1091
+ typeof c.input?.skill === "string",
1092
+ )
1093
+ .map((c) => c.input.skill),
1094
+ ),
1095
+ ];
1096
+
1097
+ console.log(
1098
+ `\nmode=main ${RUN_ARMS.length} arm(s) x ${allPrompts.length} prompts x ${TRIALS} trials = ` +
1099
+ `${RUN_ARMS.length * allPrompts.length * TRIALS} runs`,
1100
+ );
1101
+
1102
+ const mainPath = join(REPRO, `${STEM}.json`);
1103
+ // MERGE, never clobber — same rule as the oracle.
1104
+ const results =
1105
+ ONLY.length && existsSync(mainPath)
1106
+ ? JSON.parse(readFileSync(mainPath, "utf-8")).results.filter(
1107
+ (r) => !ONLY.includes(r.arm),
1108
+ )
1109
+ : [];
1110
+ for (const a of RUN_ARMS) {
1111
+ const dir = buildArm(a);
1112
+ const wanted = `${NS}:${a.skill}`;
1113
+ console.log(
1114
+ `\n${"=".repeat(78)}\n=== ${a.id} ${a.label} (${a.desc.length} chars, id ${wanted})`,
1115
+ );
1116
+ for (const { cell, prompt } of allPrompts) {
1117
+ const observed = [];
1118
+ const rep = await measureTriggerRate({
1119
+ name: `gpw-ablation:${a.id}:${cell}`,
1120
+ skillsDir: dir,
1121
+ prompts: [prompt],
1122
+ minPrompts: 1,
1123
+ fired: (t) => {
1124
+ const set = firedSkills(t);
1125
+ observed.push(set);
1126
+ return set.includes(wanted);
1127
+ },
1128
+ fixture: FIXTURE,
1129
+ trials: TRIALS,
1130
+ allowedTools: ["Skill", "Read"],
1131
+ concurrency: 3,
1132
+ spacingSec: 2,
1133
+ timeoutMs: 180000,
1134
+ });
1135
+ const bucket = observed.map((set) =>
1136
+ set.includes(wanted) ? "TARGET" : set.length > 0 ? "OTHER" : "SILENT",
1137
+ );
1138
+ const others = observed
1139
+ .flat()
1140
+ .filter((s) => s !== wanted)
1141
+ .map((s) => s.replace(`${NS}:`, ""));
1142
+ results.push({
1143
+ arm: a.id,
1144
+ label: a.label,
1145
+ skill: a.skill,
1146
+ descChars: a.desc.length,
1147
+ cell,
1148
+ prompt,
1149
+ trials: TRIALS,
1150
+ rate: rep.rate,
1151
+ errored: rep.errored ?? 0,
1152
+ overlap: Number(overlap(prompt, a.desc).toFixed(3)),
1153
+ buckets: bucket,
1154
+ othersFired: others,
1155
+ });
1156
+ const tally = (b) => bucket.filter((x) => x === b).length;
1157
+ console.log(
1158
+ ` ${cell} ${rep.rate.toFixed(2)} T${tally("TARGET")} O${tally("OTHER")} S${tally("SILENT")}` +
1159
+ ` ov=${overlap(prompt, a.desc).toFixed(2)} ${prompt.slice(0, 56)}` +
1160
+ (others.length
1161
+ ? `\n instead: ${[...new Set(others)].join(", ")}`
1162
+ : ""),
1163
+ );
1164
+ }
1165
+ }
1166
+
1167
+ // ── report ───────────────────────────────────────────────────────────────────
1168
+ const erroredTotal = results.reduce((x, r) => x + r.errored, 0);
1169
+ const stat = (arm, cell) => {
1170
+ const b = results
1171
+ .filter((r) => r.arm === arm && (!cell || r.cell === cell))
1172
+ .flatMap((r) => r.buckets);
1173
+ return {
1174
+ n: b.length,
1175
+ target: b.filter((x) => x === "TARGET").length,
1176
+ other: b.filter((x) => x === "OTHER").length,
1177
+ silent: b.filter((x) => x === "SILENT").length,
1178
+ };
1179
+ };
1180
+
1181
+ console.log(`\n${"=".repeat(78)}\nTHE ABLATION — target trigger rate by arm\n`);
1182
+ console.log(
1183
+ `${"arm".padEnd(4)}${"label".padEnd(34)}${"chars".padStart(7)}${"fired".padStart(12)}${"AH".padStart(8)}${"SH".padStart(8)}${"Δ vs A0".padStart(10)}`,
1184
+ );
1185
+ const base = stat("A0");
1186
+ const REPORTED = ARMS.filter((a) => results.some((r) => r.arm === a.id));
1187
+ for (const a of REPORTED) {
1188
+ const s = stat(a.id);
1189
+ const pc = (x) => `${((x.target / x.n) * 100).toFixed(0)}%`;
1190
+ const d = ((s.target / s.n - base.target / base.n) * 100).toFixed(0);
1191
+ console.log(
1192
+ `${a.id.padEnd(4)}${a.label.padEnd(34)}${String(a.desc.length).padStart(7)}` +
1193
+ `${`${pc(s)} (${s.target}/${s.n})`.padStart(12)}${pc(stat(a.id, "AH")).padStart(8)}${pc(stat(a.id, "SH")).padStart(8)}` +
1194
+ `${(a.id === "A0" ? "—" : `${d > 0 ? "+" : ""}${d}pp`).padStart(10)}`,
1195
+ );
1196
+ }
1197
+
1198
+ console.log(
1199
+ `\n${"=".repeat(78)}\nTARGET / OTHER / SILENT — the parent's discriminator\n`,
1200
+ );
1201
+ console.log(`${"arm".padEnd(4)}${"label".padEnd(34)} target other silent`);
1202
+ for (const a of REPORTED) {
1203
+ const s = stat(a.id);
1204
+ const p = (x) => `${((x / s.n) * 100).toFixed(0)}%`.padStart(7);
1205
+ console.log(
1206
+ `${a.id.padEnd(4)}${a.label.padEnd(34)} ${p(s.target)} ${p(s.other)} ${p(s.silent)}`,
1207
+ );
1208
+ }
1209
+ console.log(
1210
+ `\n OTHER = a competitor was selected and the target was not. The parent saw ONE in 144 runs.\n` +
1211
+ ` SILENT = nothing resolved. 42 of this skill's 48 parent runs were SILENT; that is the number\n` +
1212
+ ` an arm has to move. See caveat 4: a run that simply answered is also SILENT.`,
1213
+ );
1214
+
1215
+ const misfires = {};
1216
+ for (const r of results)
1217
+ for (const o of r.othersFired) misfires[o] = (misfires[o] ?? 0) + 1;
1218
+ const top = Object.entries(misfires)
1219
+ .sort((x, y) => y[1] - x[1])
1220
+ .slice(0, 12);
1221
+ if (top.length) {
1222
+ console.log(`\nWHICH competitors took the runs (all arms):`);
1223
+ for (const [s, n] of top) console.log(` ${String(n).padStart(3)} ${s}`);
1224
+ }
1225
+
1226
+ console.log(
1227
+ `\n🔴 n = ${base.n} PER ARM. At ~20% baseline the 95% interval is roughly ±16pp. A factor of two is\n` +
1228
+ ` readable; fifteen points is not. If every arm lands within noise of A0, the finding is that the\n` +
1229
+ ` cause is NOT IN THE DESCRIPTION TEXT — report that, do not pick the largest number.`,
1230
+ );
1231
+ if (erroredTotal > 0)
1232
+ console.log(
1233
+ `\n⚠ ${erroredTotal} run(s) ERRORED and are excluded — rates are over fewer trials than planned.`,
1234
+ );
1235
+
1236
+ mkdirSync(REPRO, { recursive: true });
1237
+ results.sort((x, y) => x.arm.localeCompare(y.arm));
1238
+ writeFileSync(
1239
+ mainPath,
1240
+ JSON.stringify(
1241
+ {
1242
+ trials: TRIALS,
1243
+ competitors: installed.length - 1,
1244
+ baselineDescChars: A0.length,
1245
+ results,
1246
+ },
1247
+ null,
1248
+ 2,
1249
+ ),
1250
+ );
1251
+ console.log(`\nper-run rows → repro/${STEM}.json`);
1252
+ console.log(
1253
+ `\nRun --mode oracle before reading any arm as a mechanism: a harness move with a matching oracle\nmove is a change in what the TEXT supports, not in what the harness does with it.`,
1254
+ );