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,891 @@
1
+ /**
2
+ * framing-vs-vocabulary.eval.mjs — WHY does a skill fire only when the user names its verb?
3
+ *
4
+ * Run: node .claude/skills/paper-pipeline/framing-vs-vocabulary.eval.mjs [--trials N] [--mode main|oracle|preflight]
5
+ *
6
+ * --mode preflight run every guard and print the labelling matrix; spends NO tokens <- run this FIRST
7
+ * --mode main the 2x2 (default): 3 skills x 4 cells x 4 prompts x 3 trials = 144 runs
8
+ * --mode oracle the ambiguity control: 48 single closed-book picks, no harness, no skills installed
9
+ *
10
+ * ─────────────────────────────────────────────────────────────────────────────
11
+ * THE FINDING THIS EXISTS TO EXPLAIN
12
+ *
13
+ * Two sibling evals measured the same three skills against the same 36 competitors with the same
14
+ * fixture and the same 3 trials, and disagreed by a factor of two to three:
15
+ *
16
+ * pipeline-language.eval.mjs prompts that NAME the action 78% EN / 60% RU (192 runs)
17
+ * description-language.eval.mjs prompts that describe a SITUATION 25% EN / 31% RU (72 runs)
18
+ *
19
+ * The second set was written deliberately to share no vocabulary with the descriptions, because a
20
+ * prompt reusing a description's own words would have measured how well the description was copied.
21
+ * So the skills mostly fire when the user happens to use the description's verb and mostly miss when
22
+ * the user describes their problem — which is how people actually type. Nobody established WHY, and
23
+ * the three candidate explanations call for three different fixes:
24
+ *
25
+ * A. VOCABULARY OVERLAP — selection is largely lexical; the oblique prompts share fewer words.
26
+ * Fix: more surface forms in descriptions.
27
+ * B. PROBLEM-vs-ACTION FRAMING — descriptions say what the SKILL DOES, the oblique prompts say
28
+ * what the USER HAS, and nothing bridges symptom to remedy.
29
+ * Fix: descriptions that name symptoms, not just actions.
30
+ * C. DIFFICULTY / AMBIGUITY — the oblique prompts are simply under-determined; several skills
31
+ * apply or none does, and any selector would do worse.
32
+ * Fix: nothing. There is no defect.
33
+ *
34
+ * 🔴 THE CONFOUND THAT MAKES THE EXISTING DATA UNABLE TO CHOOSE. In both sibling evals the two
35
+ * factors move together. Every "names the action" prompt is ALSO high-vocabulary-overlap; every
36
+ * "describes a situation" prompt is ALSO low-overlap. A and B are therefore the same variable in
37
+ * that data and no amount of re-reading those logs separates them.
38
+ *
39
+ * ─────────────────────────────────────────────────────────────────────────────
40
+ * THE DESIGN: CROSS THE TWO FACTORS, THEN LOOK AT WHAT FIRED INSTEAD
41
+ *
42
+ * Factor 1 — FRAMING.
43
+ * ACTION the prompt asks for the skill's DELIVERABLE. Imperative or explicit request.
44
+ * "produce a cut and fold plan for this draft"
45
+ * SITUATION the prompt STATES the user's condition and asks for nothing in particular.
46
+ * "this feels long and overly complex, and the middle drags"
47
+ *
48
+ * Factor 2 — VOCABULARY OVERLAP with the target skill's own description.
49
+ * HIGH reuses content words that appear verbatim in the description.
50
+ * LOW says the same thing in words the description does not contain.
51
+ *
52
+ * Four cells per skill, four fresh prompts each:
53
+ *
54
+ * HIGH overlap LOW overlap
55
+ * ACTION AH (the sibling's AL asks for the remedy in other words
56
+ * regime, ~78%)
57
+ * SITUATION SH states the symptom SL (the sibling's oblique regime, ~25%)
58
+ * in the description's
59
+ * own words
60
+ *
61
+ * The two off-diagonal cells are the whole experiment and they make the fork FALSIFIABLE:
62
+ *
63
+ * If A (lexical): AH high, SH high, AL low, SL low. Overlap explains the rows.
64
+ * If B (framing): AH high, AL high, SH low, SL low. Framing explains the columns.
65
+ * If both: AH high, AL and SH middling, SL low. Additive.
66
+ * If neither: all four cells alike — which points at C, or at something not measured here.
67
+ *
68
+ * 🔴 AND THE SIGNAL NEITHER SIBLING RECORDED: WHAT FIRED INSTEAD. Both siblings asked only "did the
69
+ * TARGET fire". The `Trace` carries every `Skill` call with its `input.skill`, so recording the full
70
+ * fired SET costs nothing but a predicate side effect. Each run then lands in one of three buckets:
71
+ *
72
+ * TARGET the intended skill resolved.
73
+ * OTHER some competitor resolved and the target did not -> a plausible-but-wrong pick.
74
+ * The prompt WAS routed; the selector just disagreed. This is what AMBIGUITY (C)
75
+ * looks like, and it is also what a lexically-nearer competitor (A) looks like.
76
+ * SILENT nothing resolved at all -> the target was never a candidate. This is what a
77
+ * description that does not cover the user's phrasing (A or B) looks like.
78
+ *
79
+ * OTHER and SILENT point in opposite directions and no existing log distinguishes them.
80
+ *
81
+ * THE THIRD LEG — the closed-book ORACLE (`--mode oracle`), the C control. C says the prompts are
82
+ * genuinely under-determined: not even a careful reader with all 37 descriptions in front of them
83
+ * could pick the intended skill. So we ask exactly that question, stripped of everything else: one
84
+ * call per prompt, no skills installed, no fixture, no tools, the 37 name+description pairs pasted
85
+ * in, "which ONE should be invoked, or NONE". That is a strictly EASIER task than the harness's —
86
+ * closed book, nothing else to do, no context budget evicting descriptions — so it is an UPPER BOUND
87
+ * on what is recoverable from the descriptions alone. If the oracle also fails on the SL prompts,
88
+ * the information is not in the descriptions and C survives. If the oracle succeeds where the
89
+ * harness fails, the information IS there and the harness is not using it, which kills C.
90
+ *
91
+ * ─────────────────────────────────────────────────────────────────────────────
92
+ * 🔴 WHAT THIS RUN CANNOT SETTLE — stated before it runs, not discovered after
93
+ *
94
+ * 1. OVERLAP IS MEASURED ON THE SURFACE. The vocabulary factor is a stemmed content-word match
95
+ * against the description string. If selection is semantic rather than lexical, an AL prompt
96
+ * ("welded into one") and an AH prompt ("merge") are the same prompt to the model and differ
97
+ * only to my metric. In that world the A/B contrast collapses and this design reports "framing"
98
+ * for something that is really shallow-semantic matching. This file cannot tell those apart and
99
+ * must not be read as though it could. It can still rule A OUT (if AL stays high), which is the
100
+ * asymmetry worth having.
101
+ * 2. SILENT IS NOT PROOF OF NON-CANDIDACY. A run where the model simply answers the question from
102
+ * the fixture, without reaching for any skill, is also SILENT. Tools are restricted to Skill and
103
+ * Read and the bodies are stubbed, but nothing forces a selection. Read SILENT as "no skill was
104
+ * selected", never as "the description was not in the running".
105
+ * 3. THE FRAMING LABEL IS A JUDGEMENT. The overlap factor is checked mechanically and the run dies
106
+ * if my labels do not survive it. Framing gets only a weak mechanical guard (action prompts must
107
+ * contain a request-form; situation prompts must not) — a guard against sloppiness, not a proof
108
+ * that the two cells differ in the way the hypothesis means.
109
+ * 4. ONE AUTHOR. The prompts were written by the same model being measured, which has read both
110
+ * sibling evals. Fresh SENTENCES are guaranteed mechanically. Fresh conceptual space is not.
111
+ * 5. n = 48 runs per cell (4 prompts x 3 trials x 3 skills). That resolves a factor of two. It does
112
+ * not resolve ten points, and the per-skill cells (12 runs) resolve almost nothing — read the
113
+ * cell totals, treat the per-skill split as texture.
114
+ * 6. ENGLISH ONLY, and that is a deliberate drop. See COST.
115
+ * 7. THE ORACLE IS A MODEL, NOT A HUMAN. It shares the training distribution of the thing being
116
+ * measured, so agreement between them is weaker evidence than agreement between a model and a
117
+ * person would be. It bounds what the DESCRIPTIONS support; it does not stand in for a human.
118
+ *
119
+ * ─────────────────────────────────────────────────────────────────────────────
120
+ * COST, AND WHAT WAS DROPPED TO FIT
121
+ *
122
+ * main 3 skills x 4 cells x 4 prompts x 3 trials = 144 runs
123
+ * oracle 48 prompts x 1 pick = 48 calls
124
+ * 192, under the 250 budget
125
+ * At the siblings' measured ~$0.14/run that is ~$27 API-equivalent, billed to a Claude
126
+ * subscription, $0 metered. Not wired into CI: this answers a question once.
127
+ *
128
+ * Dropped, in order of what it cost:
129
+ * - THE RUSSIAN ARM (would have doubled main to 288). The control arm already measured Russian on
130
+ * oblique prompts and found ru 31% / en 25% — no gap, both on the floor. Language is not the axis
131
+ * under test, and carrying it would have halved the n in every cell of the axis that is.
132
+ * - `irrelevantPrompts` / precision. The first eval measured 0% false positives on all eight cases;
133
+ * re-measuring it buys a number already known.
134
+ * - The other five skills from the language eval. Kept exactly the three the control arm measured,
135
+ * so these cells are comparable to that 25%/31% floor rather than to a new baseline.
136
+ * - Trials stayed at 3, matching both siblings, rather than rising to 5. Prompt-to-prompt variance
137
+ * dominates trial variance here (the control arm saw 0.00 and 1.00 inside one cell), so four
138
+ * prompts at three trials beats three prompts at four.
139
+ *
140
+ * ─────────────────────────────────────────────────────────────────────────────
141
+ * READING THE OUTPUT
142
+ *
143
+ * The four cell totals, then the TARGET/OTHER/SILENT split inside each. In that order. The per-skill
144
+ * rows exist so a single skill driving the whole effect is visible; they are not three findings.
145
+ *
146
+ * ─────────────────────────────────────────────────────────────────────────────
147
+ * 🔴 RESULT — 2026-08-08. 144 harness runs + 48 oracle picks, 3 trials, 36 competitors, 0 errored.
148
+ * Raw logs: `repro/2026-08-08-framing-vs-vocabulary-raw.log`,
149
+ * `repro/2026-08-08-framing-vs-vocabulary-oracle.log`
150
+ * Per-run rows: `repro/2026-08-08-framing-vs-vocabulary.json` (buckets, overlap, what fired instead)
151
+ * `repro/2026-08-08-framing-vs-vocabulary-oracle.json`
152
+ *
153
+ * HIGH overlap LOW overlap
154
+ * ACTION AH 64% (23/36) AL 44% (16/36)
155
+ * SITUATION SH 61% (22/36) SL 11% ( 4/36)
156
+ *
157
+ * main effect of VOCABULARY 35pp main effect of FRAMING 18pp
158
+ *
159
+ * 1. 🔴 B IS REFUTED AS STATED, AND IT WAS THE HYPOTHESIS WITH THE MOST INTUITIVE APPEAL.
160
+ * B says the descriptions describe what the SKILL DOES while the user describes what they HAVE,
161
+ * and nothing bridges symptom to remedy. If that were the barrier, SH — pure symptom statements,
162
+ * no request, no verb ("the abstract reads like a wall of jargon") — would sit on the floor.
163
+ * It sits at 61%, statistically indistinguishable from AH's 64%. Excluding the one dead skill
164
+ * (below) the two are identical to the run: 83% and 83%. A symptom routes perfectly well. The
165
+ * symptom-to-remedy bridge is not what is missing, and "descriptions that name symptoms" aims
166
+ * at a defect this run could not find. The descriptions ALREADY name symptoms — tighten-paper's
167
+ * lists "feels long / bloated / the middle drags" verbatim, and those are the SH prompts.
168
+ *
169
+ * 2. VOCABULARY IS THE DOMINANT FACTOR AND IT IS SUFFICIENT — but it is NOT necessary, and that is
170
+ * the part neither A nor B predicted. Overlap moves 35pp against framing's 18pp, and SH ≈ AH
171
+ * shows shared words alone carry a prompt with nothing else going for it. But A also predicts
172
+ * AL ≈ SL, both low. Observed: AL 44%, SL 11%. With no shared vocabulary, naming the deliverable
173
+ * is still worth 33pp. So neither factor is the mechanism on its own:
174
+ *
175
+ * THE SELECTOR FIRES WHEN IT HAS EITHER THE DESCRIPTION'S WORDS OR AN EXPLICIT REQUEST FOR
176
+ * THE DELIVERABLE. IT COLLAPSES ONLY WHEN IT HAS NEITHER.
177
+ *
178
+ * SL is the only cell with neither and the only cell that collapses. It is also how people type.
179
+ *
180
+ * 3. 🔴 THE WRONG-SKILL SIGNAL KILLS C-AS-AMBIGUITY OUTRIGHT: ONE misfire in 144 runs.
181
+ *
182
+ * cell target other silent
183
+ * AH 64% 0% 36%
184
+ * AL 44% 0% 56%
185
+ * SH 61% 0% 39%
186
+ * SL 11% 3% 86%
187
+ *
188
+ * The oblique prompts are not being lost to a plausible-but-wrong competitor. Nothing competes.
189
+ * 86% of SL runs resolve NO skill at all. Whatever is wrong, it is not 37 skills fighting over an
190
+ * under-determined request — the version of C that says "several skills plausibly apply" is dead.
191
+ * This is the signal neither sibling recorded, it cost nothing but a predicate side effect, and
192
+ * it is the single number that most changed the reading.
193
+ *
194
+ * 4. 🔴 BUT THE ORACLE RESCUES THE OTHER HALF OF C, AND THIS IS WHERE THE RUN OVERTURNED ITS OWN
195
+ * EXPECTATION. Closed book, all 37 descriptions in context, one pick, nothing else to do:
196
+ *
197
+ * cell oracle harness gap
198
+ * AH 12/12 (100%) 64% 36pp
199
+ * AL 8/12 (67%) 44% 22pp
200
+ * SH 12/12 (100%) 61% 39pp
201
+ * SL 3/12 (25%) 11% 14pp <- 6 of the 12 answered NONE
202
+ *
203
+ * A reader with every description in front of it and no other task ALSO collapses on SL. So the
204
+ * descriptions genuinely do not determine the answer there. C survives — but in a different form
205
+ * than it was posed. The SL prompts are not AMBIGUOUS (nothing competes, point 3); they are
206
+ * UNCOVERED (nothing matches). Those are different defects with different fixes, and no reading
207
+ * of the sibling logs could have told them apart.
208
+ *
209
+ * 5. WHAT NONE OF A, B OR C EXPLAINS, and it may be the bigger finding: the harness sits 14-39pp
210
+ * BELOW the oracle in EVERY cell, including the ones where the oracle is perfect. A third of AH
211
+ * runs — unambiguous, high-overlap, explicit requests that a closed-book reader gets 12 out of 12
212
+ * times — resolve no skill at all. That deficit is uniform across the 2x2, so it is not about
213
+ * framing or vocabulary; it is about selection under a live harness (context budget, 36 competing
214
+ * descriptions, an agent that can simply answer instead). This experiment measures its size and
215
+ * does not explain it.
216
+ *
217
+ * 6. ONE SKILL IS EFFECTIVELY DEAD AND IT DRAGS EVERY CELL. `grade-paper-writing` fired 6 times in
218
+ * 48 runs — 25% even in AH, 17% in SH — while the oracle picked it 10 times out of 16 from its
219
+ * description alone. Its description is unambiguous to a reader and almost never selected by the
220
+ * harness. It is also by far the longest description in the roster and the most loaded with
221
+ * "NOT this / NOT that" delegation clauses. That is a hypothesis, not a finding: this run does
222
+ * not vary description length and cannot test it. Excluding this skill, the 2x2 sharpens to
223
+ * AH 83% / AL 63% / SH 83% / SL 17% — same shape, same conclusion.
224
+ *
225
+ * ─────────────────────────────────────────────────────────────────────────────
226
+ * WHAT THIS STILL DOES NOT SETTLE, after the run
227
+ *
228
+ * - WHETHER "VOCABULARY" IS LEXICAL OR SHALLOW-SEMANTIC. Caveat 1 up top stands and the result
229
+ * does not resolve it. AL at 44% is well above SL's 11% despite both scoring ~0.03 on the
230
+ * surface metric, which is already evidence that something beyond string matching is running.
231
+ * Whether the HIGH cells win on strings or on meaning is untested.
232
+ * - THE HARNESS-VS-ORACLE DEFICIT (point 5). Size measured, cause not. Splitting it would need a
233
+ * roster-size sweep (does recall fall as competitors are added?) — a different experiment.
234
+ * - WHY `grade-paper-writing` IS DEAD (point 6). Needs a description-length/negation arm.
235
+ * - WHETHER A HUMAN WOULD PICK THE INTENDED SKILL ON THE SL PROMPTS. The oracle is a model from
236
+ * the same family and shares its blind spots. Caveat 7 stands: it bounds what the DESCRIPTIONS
237
+ * support, it does not stand in for a person.
238
+ * - RUSSIAN. Dropped by design; the interaction found here is untested in the language the owner
239
+ * actually types.
240
+ *
241
+ * 🔴 NO SKILL.md WAS CHANGED BY THIS RUN, DELIBERATELY. The obvious next move — add the missing
242
+ * surface forms to the three descriptions — must be measured on prompts held out from THIS file
243
+ * too, exactly as this file was held out from the two that motivated it. An intervention decided
244
+ * and validated on one run is the overfitting the sibling evals exist to prevent, and the fix
245
+ * indicated here (cover more of the user's vocabulary) is A's fix, which is the hypothesis this
246
+ * run promoted rather than the one it started from.
247
+ */
248
+
249
+ import { assertPromptDiversity, skip } from "vigiles";
250
+ import { paid_measureTriggerRate as measureTriggerRate } from "vigiles/eval";
251
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
252
+ import { join } from "node:path";
253
+ import { frontmatterBlock } from "../../lib/markdown.mjs";
254
+ import { parseFm } from "../../lib/skill-corpus.mjs";
255
+ import { installedSkills } from "./scripts/consumer.mjs";
256
+
257
+ // The name a SKILL.md DECLARES, parsed rather than matched. The old expression took
258
+ // `(\S+)` after `name:`, which silently truncates a quoted name and cannot see one
259
+ // carried onto a continuation line — and this is a guard whose whole job is to fail
260
+ // when the declared name disagrees with the directory.
261
+ const declaredName = (md) => {
262
+ const block = frontmatterBlock(md);
263
+ if (block === null) return undefined;
264
+ const v = parseFm(block, "skill fixture").name;
265
+ return typeof v === "string" ? v : undefined;
266
+ };
267
+
268
+ import { execFileSync } from "node:child_process";
269
+
270
+ const ROOT = process.env.CLAUDE_PROJECT_DIR ?? process.cwd();
271
+ const SKILLS_DIR = join(ROOT, ".claude", "skills");
272
+ const REPRO = join(SKILLS_DIR, "paper-pipeline", "repro");
273
+ const NS = "vigiles-loose-skills";
274
+ const id = (skill) => `${NS}:${skill}`;
275
+
276
+ const argv = process.argv.slice(2);
277
+ const val = (n, d) => {
278
+ const i = argv.indexOf(`--${n}`);
279
+ return i >= 0 && argv[i + 1] ? argv[i + 1] : d;
280
+ };
281
+ const MODE = val("mode", "main");
282
+ const TRIALS = Number(val("trials", "3"));
283
+ if (!["main", "oracle", "preflight"].includes(MODE))
284
+ throw new Error("--mode must be main | oracle | preflight");
285
+
286
+ // ── the fixture ──────────────────────────────────────────────────────────────
287
+ // Byte-identical to BOTH siblings. If it drifts, a difference between this run and theirs stops
288
+ // being attributable to the prompts.
289
+ const FIXTURE = {
290
+ "paper/paper.md": [
291
+ "# Prose Isn't Policy: Measuring Whether Agent-Config Rules Are Enforceable",
292
+ "",
293
+ "## Abstract",
294
+ "Agent configuration files state rules in prose and assume the model obeys them. We compile a",
295
+ "corpus of real rules and measure what fraction can be mechanically enforced. We find that 84%",
296
+ "of rules in our corpus are enforceable, and that LLM-authored checkers for the remainder leak",
297
+ "silently in 84-96% of adversarial cases \\cite{greshake2023}.",
298
+ "",
299
+ "## 1 Introduction",
300
+ "Every agent harness ships a natural-language rulebook. Nothing checks it. This is the same",
301
+ "mistake as a code comment that claims an invariant no test enforces \\cite{thompson1984}.",
302
+ "",
303
+ "## 2 Method",
304
+ "We gather rules from public repositories, classify each by enforceability, and build a",
305
+ "two-stage adversarial gate that validates a synthesized rule against a blind gold set.",
306
+ "",
307
+ "## 3 Results",
308
+ "See Table 1. The headline number is 84%.",
309
+ "",
310
+ "## 4 Discussion",
311
+ "The result generalizes beyond our corpus in the sense that the mechanism is not corpus-specific,",
312
+ "though of course the specific percentages are, and it is important to note in this context that",
313
+ "the framing itself may be what carries, rather than the measurement.",
314
+ "",
315
+ "## 5 Threats to Validity",
316
+ "Our corpus is drawn from public repositories and may not represent private configurations.",
317
+ "",
318
+ "## 6 Related Work",
319
+ "TODO",
320
+ "",
321
+ "## 7 Conclusion",
322
+ "Prose is not policy.",
323
+ ].join("\n"),
324
+ "paper/repro/README.md":
325
+ "# Reproduction artifact\n\n`python3 paper_numbers.py` recomputes every bolded figure in paper.md.\n",
326
+ "paper/PIPELINE-STATUS.md":
327
+ "# Pipeline status\n\n| gate | state |\n|---|---|\n| numbers | pass |\n| structure | not run |\n| citations | not run |\n",
328
+ };
329
+
330
+ // ── the 2x2 prompt matrix ────────────────────────────────────────────────────
331
+ // cell keys: AH action/high · AL action/low · SH situation/high · SL situation/low
332
+ const CASES = [
333
+ {
334
+ skill: "tighten-paper",
335
+ cells: {
336
+ AH: [
337
+ "produce a cut and fold plan for this draft — which sections do not earn their place",
338
+ "tell me which parts are TMI and belong in the appendix instead of the paper",
339
+ "reorder and merge whatever is needed so it is not too long for its contribution",
340
+ "give me a structural verdict on this drafted paper, cover to cover, as an editor would",
341
+ ],
342
+ AL: [
343
+ "decide what gets deleted from this manuscript and list it back to me",
344
+ "tell me which two chapters should be welded into one, and which should go last",
345
+ "I want a shortlist of paragraphs to delete so it fits on fewer pages",
346
+ "rank the chapters by how little they matter, worst first, so I know where to swing the axe",
347
+ ],
348
+ SH: [
349
+ "this feels long and overly complex, and the middle drags",
350
+ "after three review rounds it has collected a lot of hedge-mass",
351
+ "it does not deliver its point and nobody could skim it",
352
+ "it is too long for its contribution and hard to read cover to cover",
353
+ ],
354
+ SL: [
355
+ "people keep bailing out partway through and I cannot tell why",
356
+ "there is a nine page ceiling and I am well over it",
357
+ "my co-author says the whole thing sprawls and he lost the thread",
358
+ "every paragraph seems necessary to me and yet the whole is exhausting",
359
+ ],
360
+ },
361
+ },
362
+ {
363
+ skill: "grade-paper-writing",
364
+ cells: {
365
+ AH: [
366
+ "grade the writing craft in this draft and name the offending sentence in each dimension",
367
+ "run the blind multi-grader panel over the prose clarity and jargon discipline",
368
+ "score the title, the abstract and the intro architecture one to five",
369
+ "grade how well this reads, hedge stacking included, and give me the fix for each",
370
+ ],
371
+ AL: [
372
+ "mark this manuscript out of five on how it is put together at the sentence level",
373
+ "give me a scored report card on the language, with the worst example under each heading",
374
+ "rate how professional the phrasing sounds against papers people actually finish",
375
+ "assign a number to the quality of the English here and point at the ugliest example",
376
+ ],
377
+ SH: [
378
+ "the abstract reads like a wall of jargon",
379
+ "the intro architecture is fine but the prose clarity is not",
380
+ "there is hedge stacking everywhere and the point never lands",
381
+ "the writing does not read well even though the draft is finished",
382
+ ],
383
+ SL: [
384
+ "something about the way this is worded makes me wince and I cannot name it",
385
+ "my supervisor said the English gets in the way of the science",
386
+ "the sentences are correct but they feel like wet cardboard",
387
+ "I went through my own opening twice and still had to start over",
388
+ ],
389
+ },
390
+ },
391
+ {
392
+ skill: "argument-arc",
393
+ cells: {
394
+ AH: [
395
+ "build the one-sentence-per-section outline before the large rewrite",
396
+ "run the bottom-up inevitability pass over the argument architecture",
397
+ "repair the argument architecture — the same structural objection keeps repeating",
398
+ "check the name and number budget, the reader has too much to carry",
399
+ ],
400
+ AL: [
401
+ "give me a single line for each part of the manuscript, working backwards from the finish",
402
+ "make the ending feel unavoidable given everything before it, and tell me what to move",
403
+ "count how many invented labels and figures the audience must memorise, and trim the list",
404
+ "lay out the chain of reasoning as a list so I can see where a link is missing",
405
+ ],
406
+ SH: [
407
+ "the reader says we throw ideas at them",
408
+ "the same structural objection repeats in every round",
409
+ "there is no argument architecture here, just a sequence",
410
+ "we blew the name and number budget somewhere in the middle",
411
+ ],
412
+ SL: [
413
+ "each part is fine alone but together they do not add up to anything",
414
+ "my advisor cannot tell what the conclusion depends on",
415
+ "the ending arrives without having been earned",
416
+ "someone asked me what the point was and I gave three answers",
417
+ ],
418
+ },
419
+ },
420
+ ];
421
+
422
+ const CELLS = ["AH", "AL", "SH", "SL"];
423
+ const CELL_LABEL = {
424
+ AH: "ACTION / HIGH overlap",
425
+ AL: "ACTION / LOW overlap",
426
+ SH: "SITUATION / HIGH overlap",
427
+ SL: "SITUATION / LOW overlap",
428
+ };
429
+
430
+ // ── PREFLIGHT ────────────────────────────────────────────────────────────────
431
+ // Every guard below fails LOUDLY. A misspelled skill, a changed namespace or a mislabelled cell
432
+ // makes the `fired` predicate permanently false, and the run then reports a wall of confident 0.00s
433
+ // as though the descriptions were dead. That is the failure mode this block exists to prevent.
434
+
435
+ // 1. the skills exist and declare the names we test against
436
+ // Through `installedSkills`, which follows the links `paperlint init` makes (rpp#62).
437
+ const installed = installedSkills(SKILLS_DIR);
438
+ const installedSet = new Set(installed);
439
+ for (const c of CASES) {
440
+ if (!installedSet.has(c.skill))
441
+ throw new Error(`${c.skill} is not installed under ${SKILLS_DIR}`);
442
+ const fm = readFileSync(join(SKILLS_DIR, c.skill, "SKILL.md"), "utf-8");
443
+ const declared = declaredName(fm);
444
+ if (declared && declared !== c.skill)
445
+ throw new Error(`${c.skill}/SKILL.md declares name: ${declared}`);
446
+ }
447
+
448
+ const allPrompts = CASES.flatMap((c) =>
449
+ CELLS.flatMap((k) =>
450
+ c.cells[k].map((p) => ({ skill: c.skill, cell: k, prompt: p })),
451
+ ),
452
+ );
453
+
454
+ // 2. HELD OUT from both siblings — copied from description-language.eval.mjs, extended to cover the
455
+ // third sibling too. A prompt shared with a run that MOTIVATED this question is not held out.
456
+ const siblingText = [
457
+ "pipeline-language.eval.mjs",
458
+ "pipeline-firing.eval.mjs",
459
+ "description-language.eval.mjs",
460
+ ]
461
+ .map((f) => join(SKILLS_DIR, "paper-pipeline", f))
462
+ .filter(existsSync)
463
+ .map((f) => readFileSync(f, "utf-8"))
464
+ .join("\n");
465
+ for (const { prompt } of allPrompts)
466
+ if (siblingText.includes(prompt))
467
+ throw new Error(
468
+ `PROMPT NOT HELD OUT: "${prompt}" already appears in a sibling eval. This set must share no ` +
469
+ `sentence with the runs that motivated the question, or the cells measure copying.`,
470
+ );
471
+
472
+ // 3. no prompt reused across cells (a duplicate would silently merge two cells)
473
+ const seen = new Set();
474
+ for (const { prompt } of allPrompts) {
475
+ if (seen.has(prompt))
476
+ throw new Error(`duplicate prompt across cells: "${prompt}"`);
477
+ seen.add(prompt);
478
+ }
479
+
480
+ // 4. diversity, per cell
481
+ for (const c of CASES)
482
+ for (const k of CELLS)
483
+ assertPromptDiversity(c.cells[k], {
484
+ minPrompts: 4,
485
+ minDistance: 0.3,
486
+ label: `${c.skill}:${k}`,
487
+ });
488
+
489
+ // 5. 🔴 THE VOCABULARY FACTOR, CHECKED MECHANICALLY rather than asserted.
490
+ // Content-word overlap of a prompt with a skill description: stemmed, stopworded, prompt-normalised
491
+ // (what fraction of the prompt's content words the description contains). Prompt-normalised on
492
+ // purpose — the hypothesis is about whether the USER's words are in the description, not the reverse.
493
+ // ⚠ It is therefore biased toward LONG descriptions, which matters only for the argmax reported
494
+ // below, not for the HIGH/LOW contrast (that compares prompts against ONE fixed description).
495
+ const STOP = new Set(
496
+ (
497
+ "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 " +
498
+ "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 " +
499
+ "for with by from as into over under about after before between out up down off again more most some any " +
500
+ "no not nor only own same so too very can will just should now what which who whom whose when where why how " +
501
+ "me myself yourself each both few other such all one two three back keep still even yet get got give given " +
502
+ "make made want need know knew cannot could would might must let us like also because while against would"
503
+ ).split(/\s+/),
504
+ );
505
+ const stem = (w) =>
506
+ w
507
+ .replace(/(ies)$/, "y")
508
+ .replace(/(sses|shes|ches|xes)$/, (m) => m.slice(0, -2))
509
+ .replace(/(ing|ed|ly|s)$/, "");
510
+ const words = (s) =>
511
+ new Set(
512
+ s
513
+ .toLowerCase()
514
+ .replace(/[^a-zа-яё\s-]/gi, " ")
515
+ .split(/[\s-]+/)
516
+ .filter((w) => w.length >= 3 && !STOP.has(w))
517
+ .map(stem)
518
+ .filter((w) => w.length >= 3),
519
+ );
520
+
521
+ const descriptions = {};
522
+ for (const name of installed) {
523
+ const t = readFileSync(join(SKILLS_DIR, name, "SKILL.md"), "utf-8");
524
+ // Parsed, not matched: the old lookahead guessed where a value ended by looking
525
+ // for the next key, which is wrong for folded/literal blocks and for quoted values
526
+ // carrying a colon. This eval RANKS descriptions, so a truncated one moves a score
527
+ // rather than failing.
528
+ const block = frontmatterBlock(t);
529
+ if (block === null) continue;
530
+ descriptions[name] = String(parseFm(block, name).description ?? "")
531
+ .replace(/\s+/g, " ")
532
+ .trim();
533
+ }
534
+ const descWords = Object.fromEntries(
535
+ Object.entries(descriptions).map(([k, v]) => [k, words(v)]),
536
+ );
537
+
538
+ const overlap = (prompt, skill) => {
539
+ const p = words(prompt);
540
+ if (p.size === 0) return 0;
541
+ let hit = 0;
542
+ for (const w of p) if (descWords[skill]?.has(w)) hit++;
543
+ return hit / p.size;
544
+ };
545
+ /** Which installed skill's description this prompt lexically resembles most (see the length bias above). */
546
+ const lexArgmax = (prompt) =>
547
+ installed
548
+ .map((s) => ({ s, o: overlap(prompt, s) }))
549
+ .sort((a, b) => b.o - a.o)[0];
550
+
551
+ const cellOverlap = {};
552
+ for (const c of CASES)
553
+ for (const k of CELLS)
554
+ cellOverlap[`${c.skill}:${k}`] =
555
+ c.cells[k].reduce((a, p) => a + overlap(p, c.skill), 0) /
556
+ c.cells[k].length;
557
+
558
+ const MIN_CONTRAST = 0.15;
559
+ for (const c of CASES) {
560
+ const hi = (cellOverlap[`${c.skill}:AH`] + cellOverlap[`${c.skill}:SH`]) / 2;
561
+ const lo = (cellOverlap[`${c.skill}:AL`] + cellOverlap[`${c.skill}:SL`]) / 2;
562
+ if (hi - lo < MIN_CONTRAST)
563
+ throw new Error(
564
+ `VOCABULARY LABELS NOT MECHANICALLY SUPPORTED for ${c.skill}: HIGH cells mean overlap ` +
565
+ `${hi.toFixed(3)}, LOW cells ${lo.toFixed(3)}, contrast ${(hi - lo).toFixed(3)} < ${MIN_CONTRAST}. ` +
566
+ `The vocabulary factor is what this experiment varies; if the labels do not survive the ` +
567
+ `metric, the run cannot separate A from B and must not spend a token.`,
568
+ );
569
+ }
570
+
571
+ // 6. THE FRAMING FACTOR gets only a weak guard, and the header says so. Action prompts must carry a
572
+ // request form addressed to the assistant; situation prompts must not. This catches me writing a
573
+ // declarative sentence into an ACTION cell — it does not prove the two cells differ semantically.
574
+ const REQUEST =
575
+ /^(produce|tell|give|decide|rank|grade|run|score|mark|rate|assign|build|repair|check|make|count|lay|reorder|merge|cut|fold|list|show|find|write|explain)\b|\bi want\b|\bgive me\b|\btell me\b/i;
576
+ for (const c of CASES) {
577
+ for (const p of [...c.cells.AH, ...c.cells.AL])
578
+ if (!REQUEST.test(p))
579
+ throw new Error(`ACTION cell prompt carries no request form: "${p}"`);
580
+ for (const p of [...c.cells.SH, ...c.cells.SL])
581
+ if (REQUEST.test(p))
582
+ throw new Error(`SITUATION cell prompt reads as a request: "${p}"`);
583
+ }
584
+
585
+ // 7. the harness itself
586
+ if (MODE !== "preflight") {
587
+ try {
588
+ execFileSync("claude", ["--version"], { stdio: "ignore" });
589
+ } catch {
590
+ skip(
591
+ "`claude` CLI not on PATH — the eval tier drives the real harness and cannot be faked",
592
+ );
593
+ }
594
+ }
595
+
596
+ // ── the labelling matrix, printed every run so the factors are auditable ──────
597
+ console.log(
598
+ `\n${installed.length} skills installed → ${installed.length - 1} competitors per run`,
599
+ );
600
+ console.log(
601
+ `\nMEASURED VOCABULARY OVERLAP (prompt content-words found in the target description)`,
602
+ );
603
+ console.log(
604
+ `${"skill".padEnd(22)} ${CELLS.map((k) => k.padStart(6)).join("")} HIGH-LOW`,
605
+ );
606
+ for (const c of CASES) {
607
+ const hi = (cellOverlap[`${c.skill}:AH`] + cellOverlap[`${c.skill}:SH`]) / 2;
608
+ const lo = (cellOverlap[`${c.skill}:AL`] + cellOverlap[`${c.skill}:SL`]) / 2;
609
+ console.log(
610
+ `${c.skill.padEnd(22)} ${CELLS.map((k) => cellOverlap[`${c.skill}:${k}`].toFixed(2).padStart(6)).join("")}` +
611
+ ` +${(hi - lo).toFixed(2)}`,
612
+ );
613
+ }
614
+
615
+ if (MODE === "preflight") {
616
+ console.log(
617
+ `\nper-prompt overlap, and the skill each prompt most resembles lexically:`,
618
+ );
619
+ for (const c of CASES) {
620
+ console.log(`\n### ${c.skill}`);
621
+ for (const k of CELLS)
622
+ for (const p of c.cells[k]) {
623
+ const a = lexArgmax(p);
624
+ const flag = a.s === c.skill ? " <- target is argmax" : "";
625
+ console.log(
626
+ ` ${k} ${overlap(p, c.skill).toFixed(2)} argmax=${a.s}(${a.o.toFixed(2)})${flag}\n ${p}`,
627
+ );
628
+ }
629
+ }
630
+ console.log(
631
+ `\nALL PREFLIGHT GUARDS PASSED. No tokens spent. Run --mode main next.`,
632
+ );
633
+ process.exit(0);
634
+ }
635
+
636
+ // ── ORACLE MODE — the C control ──────────────────────────────────────────────
637
+ if (MODE === "oracle") {
638
+ const roster = installed.map((s) => `- ${s}: ${descriptions[s]}`).join("\n");
639
+ const rows = [];
640
+ console.log(
641
+ `\noracle: ${allPrompts.length} closed-book picks over ${installed.length} descriptions\n`,
642
+ );
643
+ for (const { skill, cell, prompt } of allPrompts) {
644
+ const q =
645
+ `Below is a list of agent skills, each with the description its author wrote.\n\n${roster}\n\n` +
646
+ `A user typed exactly this:\n\n"${prompt}"\n\n` +
647
+ `Which ONE of the skills above should be invoked? Reply with the skill name alone, ` +
648
+ `or the single word NONE if no skill clearly applies. No explanation.`;
649
+ let pick; // no initializer: both try/catch branches assign it (2026-08-28)
650
+ try {
651
+ const out = execFileSync("claude", ["-p", q, "--model", "sonnet"], {
652
+ encoding: "utf-8",
653
+ timeout: 180000,
654
+ maxBuffer: 1 << 24,
655
+ });
656
+ const t = out
657
+ .trim()
658
+ .split("\n")
659
+ .pop()
660
+ .trim()
661
+ .replace(/[.`"']/g, "");
662
+ pick = installedSet.has(t)
663
+ ? t
664
+ : /^none$/i.test(t)
665
+ ? "NONE"
666
+ : `?${t.slice(0, 40)}`;
667
+ } catch (e) {
668
+ pick = `ERROR:${String(e.message).slice(0, 60)}`;
669
+ }
670
+ const ok = pick === skill;
671
+ rows.push({ skill, cell, prompt, pick, ok });
672
+ console.log(
673
+ ` ${cell} ${ok ? "HIT " : "miss"} ${pick.padEnd(24)} ${prompt.slice(0, 62)}`,
674
+ );
675
+ }
676
+ console.log(
677
+ `\nORACLE ACCURACY BY CELL (closed book, 37 descriptions in context, 1 pick each)`,
678
+ );
679
+ console.log(`${"cell".padEnd(26)} hit none wrong`);
680
+ for (const k of CELLS) {
681
+ const r = rows.filter((x) => x.cell === k);
682
+ const hit = r.filter((x) => x.ok).length;
683
+ const none = r.filter((x) => x.pick === "NONE").length;
684
+ console.log(
685
+ `${CELL_LABEL[k].padEnd(26)} ${String(hit).padStart(2)}/${r.length} ${String(none).padStart(4)} ${String(r.length - hit - none).padStart(5)}`,
686
+ );
687
+ }
688
+ mkdirSync(REPRO, { recursive: true });
689
+ writeFileSync(
690
+ join(REPRO, "2026-08-08-framing-vs-vocabulary-oracle.json"),
691
+ JSON.stringify(rows, null, 2),
692
+ );
693
+ console.log(`\nrows → repro/2026-08-08-framing-vs-vocabulary-oracle.json`);
694
+ console.log(
695
+ `\nREAD THIS AS AN UPPER BOUND, NOT A HUMAN. The oracle's task is strictly easier than the ` +
696
+ `harness's (closed book, nothing else to do, no context budget). A cell the ORACLE also ` +
697
+ `misses is a cell whose descriptions do not determine the answer — that is C. A cell the ` +
698
+ `oracle gets and the harness does not is a cell where the information exists and the ` +
699
+ `selector is not using it — that is not C.`,
700
+ );
701
+ process.exit(0);
702
+ }
703
+
704
+ // ── MAIN: the 2x2 ────────────────────────────────────────────────────────────
705
+ // One measureTriggerRate call PER PROMPT. The predicate sees a Trace but not which prompt produced
706
+ // it, so per-prompt attribution of WHAT FIRED INSTEAD is only available at this granularity — and
707
+ // that attribution is the point of the run. The cost is 48 packagings of the skills dir, which is
708
+ // a file copy, not a token.
709
+ const firedSkills = (t) => [
710
+ ...new Set(
711
+ t.toolCalls
712
+ .filter(
713
+ (c) =>
714
+ c.name === "Skill" &&
715
+ !c.isError &&
716
+ typeof c.input?.skill === "string",
717
+ )
718
+ .map((c) => c.input.skill),
719
+ ),
720
+ ];
721
+
722
+ console.log(
723
+ `\nmode=main ${CASES.length} skills x 4 cells x 4 prompts x ${TRIALS} trials = ` +
724
+ `${CASES.length * 4 * 4 * TRIALS} runs`,
725
+ );
726
+
727
+ const results = [];
728
+ for (const c of CASES) {
729
+ for (const k of CELLS) {
730
+ console.log(`\n=== ${c.skill} [${CELL_LABEL[k]}]`);
731
+ for (const prompt of c.cells[k]) {
732
+ const observed = [];
733
+ const rep = await measureTriggerRate({
734
+ name: `framing-vs-vocabulary:${c.skill}:${k}`,
735
+ skillsDir: SKILLS_DIR,
736
+ prompts: [prompt],
737
+ minPrompts: 1,
738
+ fired: (t) => {
739
+ const set = firedSkills(t);
740
+ observed.push(set);
741
+ return set.includes(id(c.skill));
742
+ },
743
+ fixture: FIXTURE,
744
+ trials: TRIALS,
745
+ allowedTools: ["Skill", "Read"],
746
+ concurrency: 3,
747
+ spacingSec: 2,
748
+ timeoutMs: 180000,
749
+ });
750
+ // TARGET / OTHER / SILENT, per trial.
751
+ const bucket = observed.map((set) =>
752
+ set.includes(id(c.skill))
753
+ ? "TARGET"
754
+ : set.length > 0
755
+ ? "OTHER"
756
+ : "SILENT",
757
+ );
758
+ const others = observed
759
+ .flat()
760
+ .filter((s) => s !== id(c.skill))
761
+ .map((s) => s.replace(`${NS}:`, ""));
762
+ results.push({
763
+ skill: c.skill,
764
+ cell: k,
765
+ prompt,
766
+ trials: TRIALS,
767
+ rate: rep.rate,
768
+ errored: rep.errored ?? 0,
769
+ overlap: Number(overlap(prompt, c.skill).toFixed(3)),
770
+ lexArgmax: lexArgmax(prompt).s,
771
+ buckets: bucket,
772
+ othersFired: others,
773
+ });
774
+ const tally = (b) => bucket.filter((x) => x === b).length;
775
+ console.log(
776
+ ` ${rep.rate.toFixed(2)} T${tally("TARGET")} O${tally("OTHER")} S${tally("SILENT")}` +
777
+ ` ov=${overlap(prompt, c.skill).toFixed(2)} ${prompt.slice(0, 56)}` +
778
+ (others.length
779
+ ? `\n instead: ${[...new Set(others)].join(", ")}`
780
+ : ""),
781
+ );
782
+ }
783
+ }
784
+ }
785
+
786
+ // ── report ───────────────────────────────────────────────────────────────────
787
+ const erroredTotal = results.reduce((a, r) => a + r.errored, 0);
788
+ const cellStat = (k, skill) => {
789
+ const rs = results.filter(
790
+ (r) => r.cell === k && (!skill || r.skill === skill),
791
+ );
792
+ const b = rs.flatMap((r) => r.buckets);
793
+ return {
794
+ n: b.length,
795
+ target: b.filter((x) => x === "TARGET").length,
796
+ other: b.filter((x) => x === "OTHER").length,
797
+ silent: b.filter((x) => x === "SILENT").length,
798
+ };
799
+ };
800
+
801
+ console.log(`\n${"=".repeat(78)}\nTHE 2x2 — target trigger rate\n`);
802
+ console.log(
803
+ `${"".padEnd(22)}${"HIGH overlap".padStart(16)}${"LOW overlap".padStart(16)}`,
804
+ );
805
+ for (const [row, hk, lk] of [
806
+ ["ACTION", "AH", "AL"],
807
+ ["SITUATION", "SH", "SL"],
808
+ ]) {
809
+ const h = cellStat(hk),
810
+ l = cellStat(lk);
811
+ console.log(
812
+ `${row.padEnd(22)}` +
813
+ `${`${((h.target / h.n) * 100).toFixed(0)}% (${h.target}/${h.n})`.padStart(16)}` +
814
+ `${`${((l.target / l.n) * 100).toFixed(0)}% (${l.target}/${l.n})`.padStart(16)}`,
815
+ );
816
+ }
817
+ const M = Object.fromEntries(CELLS.map((k) => [k, cellStat(k)]));
818
+ const r = (k) => M[k].target / M[k].n;
819
+ console.log(
820
+ `\nmain effect of FRAMING (AH+AL) - (SH+SL) = ` +
821
+ `${(((M.AH.target + M.AL.target) / (M.AH.n + M.AL.n) - (M.SH.target + M.SL.target) / (M.SH.n + M.SL.n)) * 100).toFixed(0)}pp`,
822
+ );
823
+ console.log(
824
+ `main effect of VOCABULARY (AH+SH) - (AL+SL) = ` +
825
+ `${(((M.AH.target + M.SH.target) / (M.AH.n + M.SH.n) - (M.AL.target + M.SL.target) / (M.AL.n + M.SL.n)) * 100).toFixed(0)}pp`,
826
+ );
827
+ console.log(
828
+ `\n A (lexical) predicts AH high, SH high, AL low, SL low\n` +
829
+ ` B (framing) predicts AH high, AL high, SH low, SL low\n` +
830
+ ` observed AH ${(r("AH") * 100).toFixed(0)}% AL ${(r("AL") * 100).toFixed(0)}% ` +
831
+ `SH ${(r("SH") * 100).toFixed(0)}% SL ${(r("SL") * 100).toFixed(0)}%`,
832
+ );
833
+
834
+ console.log(`\n${"=".repeat(78)}\nWHAT FIRED INSTEAD — the C discriminator\n`);
835
+ console.log(`${"cell".padEnd(26)} target other silent`);
836
+ for (const k of CELLS) {
837
+ const s = M[k];
838
+ const p = (x) => `${((x / s.n) * 100).toFixed(0)}%`.padStart(6);
839
+ console.log(
840
+ `${CELL_LABEL[k].padEnd(26)} ${p(s.target)} ${p(s.other)} ${p(s.silent)}`,
841
+ );
842
+ }
843
+ console.log(
844
+ `\n OTHER = a competitor was selected and the target was not. The prompt routed somewhere;\n` +
845
+ ` the selector disagreed. Ambiguity (C) or a lexically nearer rival (A).\n` +
846
+ ` SILENT = nothing resolved. The target was never selected AND neither was anything else.\n` +
847
+ ` A description that does not cover the phrasing (A or B) — but see caveat 2:\n` +
848
+ ` a run that simply answered the question is also SILENT.`,
849
+ );
850
+
851
+ const misfires = {};
852
+ for (const rr of results)
853
+ for (const o of rr.othersFired) misfires[o] = (misfires[o] ?? 0) + 1;
854
+ const top = Object.entries(misfires)
855
+ .sort((a, b) => b[1] - a[1])
856
+ .slice(0, 12);
857
+ if (top.length) {
858
+ console.log(`\nWHICH competitors took the runs (all cells):`);
859
+ for (const [s, n] of top) console.log(` ${String(n).padStart(3)} ${s}`);
860
+ }
861
+
862
+ console.log(`\nper-skill, target rate by cell:`);
863
+ console.log(`${"skill".padEnd(22)}${CELLS.map((k) => k.padStart(8)).join("")}`);
864
+ for (const c of CASES)
865
+ console.log(
866
+ `${c.skill.padEnd(22)}` +
867
+ CELLS.map((k) => {
868
+ const s = cellStat(k, c.skill);
869
+ return `${((s.target / s.n) * 100).toFixed(0)}%`.padStart(8);
870
+ }).join(""),
871
+ );
872
+
873
+ if (erroredTotal > 0)
874
+ console.log(
875
+ `\n⚠ ${erroredTotal} run(s) ERRORED and are excluded — the rates above are over fewer trials than planned.`,
876
+ );
877
+
878
+ mkdirSync(REPRO, { recursive: true });
879
+ writeFileSync(
880
+ join(REPRO, "2026-08-08-framing-vs-vocabulary.json"),
881
+ JSON.stringify(
882
+ { trials: TRIALS, competitors: installed.length - 1, results },
883
+ null,
884
+ 2,
885
+ ),
886
+ );
887
+ console.log(`\nper-run rows → repro/2026-08-08-framing-vs-vocabulary.json`);
888
+ console.log(
889
+ `\nRun --mode oracle before concluding C is dead: the 2x2 alone cannot tell "the description was\n` +
890
+ `not a candidate" from "no description could have been", and that is exactly what C claims.`,
891
+ );