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.
- package/.github/dependabot.yml +72 -0
- package/.github/workflows/ci.yml +297 -0
- package/.github/workflows/dependabot-automerge.yml +70 -0
- package/.github/workflows/pr-title.yml +59 -0
- package/.github/workflows/release.yml +54 -0
- package/CLAUDE.md +598 -0
- package/CONTRIBUTING.md +159 -0
- package/LICENSE +21 -0
- package/README.md +240 -0
- package/action.harness.mjs +287 -0
- package/action.mutations.mjs +162 -0
- package/action.yml +138 -0
- package/bin/rpp.mjs +43 -0
- package/dist/action-ref.d.ts +12 -0
- package/dist/action-ref.d.ts.map +1 -0
- package/dist/action-ref.js +16 -0
- package/dist/action-ref.js.map +1 -0
- package/dist/adapters/banal/failure.d.ts +73 -0
- package/dist/adapters/banal/failure.d.ts.map +1 -0
- package/dist/adapters/banal/failure.js +58 -0
- package/dist/adapters/banal/failure.js.map +1 -0
- package/dist/adapters/banal/index.d.ts +17 -0
- package/dist/adapters/banal/index.d.ts.map +1 -0
- package/dist/adapters/banal/index.js +56 -0
- package/dist/adapters/banal/index.js.map +1 -0
- package/dist/adapters/banal/install.d.ts +26 -0
- package/dist/adapters/banal/install.d.ts.map +1 -0
- package/dist/adapters/banal/install.js +15 -0
- package/dist/adapters/banal/install.js.map +1 -0
- package/dist/adapters/banal/invocation.d.ts +48 -0
- package/dist/adapters/banal/invocation.d.ts.map +1 -0
- package/dist/adapters/banal/invocation.js +43 -0
- package/dist/adapters/banal/invocation.js.map +1 -0
- package/dist/adapters/banal/locate.d.ts +50 -0
- package/dist/adapters/banal/locate.d.ts.map +1 -0
- package/dist/adapters/banal/locate.js +34 -0
- package/dist/adapters/banal/locate.js.map +1 -0
- package/dist/adapters/banal/output.d.ts +27 -0
- package/dist/adapters/banal/output.d.ts.map +1 -0
- package/dist/adapters/banal/output.js +112 -0
- package/dist/adapters/banal/output.js.map +1 -0
- package/dist/adapters/banal/pin.d.ts +19 -0
- package/dist/adapters/banal/pin.d.ts.map +1 -0
- package/dist/adapters/banal/pin.js +15 -0
- package/dist/adapters/banal/pin.js.map +1 -0
- package/dist/adapters/banal/probe.d.ts +12 -0
- package/dist/adapters/banal/probe.d.ts.map +1 -0
- package/dist/adapters/banal/probe.js +27 -0
- package/dist/adapters/banal/probe.js.map +1 -0
- package/dist/adapters/banal/run.d.ts +89 -0
- package/dist/adapters/banal/run.d.ts.map +1 -0
- package/dist/adapters/banal/run.js +104 -0
- package/dist/adapters/banal/run.js.map +1 -0
- package/dist/adapters/banal/settings.d.ts +18 -0
- package/dist/adapters/banal/settings.d.ts.map +1 -0
- package/dist/adapters/banal/settings.js +29 -0
- package/dist/adapters/banal/settings.js.map +1 -0
- package/dist/adapters/banal/xml.d.ts +48 -0
- package/dist/adapters/banal/xml.d.ts.map +1 -0
- package/dist/adapters/banal/xml.js +67 -0
- package/dist/adapters/banal/xml.js.map +1 -0
- package/dist/adapters/curl/download.io.d.ts +14 -0
- package/dist/adapters/curl/download.io.d.ts.map +1 -0
- package/dist/adapters/curl/download.io.js +69 -0
- package/dist/adapters/curl/download.io.js.map +1 -0
- package/dist/adapters/curl/index.d.ts +6 -0
- package/dist/adapters/curl/index.d.ts.map +1 -0
- package/dist/adapters/curl/index.js +6 -0
- package/dist/adapters/curl/index.js.map +1 -0
- package/dist/adapters/memory/index.d.ts +43 -0
- package/dist/adapters/memory/index.d.ts.map +1 -0
- package/dist/adapters/memory/index.js +79 -0
- package/dist/adapters/memory/index.js.map +1 -0
- package/dist/adapters/node/files.io.d.ts +3 -0
- package/dist/adapters/node/files.io.d.ts.map +1 -0
- package/dist/adapters/node/files.io.js +31 -0
- package/dist/adapters/node/files.io.js.map +1 -0
- package/dist/adapters/node/host.io.d.ts +3 -0
- package/dist/adapters/node/host.io.d.ts.map +1 -0
- package/dist/adapters/node/host.io.js +14 -0
- package/dist/adapters/node/host.io.js.map +1 -0
- package/dist/adapters/node/index.d.ts +25 -0
- package/dist/adapters/node/index.d.ts.map +1 -0
- package/dist/adapters/node/index.js +14 -0
- package/dist/adapters/node/index.js.map +1 -0
- package/dist/adapters/node/process.io.d.ts +14 -0
- package/dist/adapters/node/process.io.d.ts.map +1 -0
- package/dist/adapters/node/process.io.js +41 -0
- package/dist/adapters/node/process.io.js.map +1 -0
- package/dist/adapters/node/workspace.io.d.ts +4 -0
- package/dist/adapters/node/workspace.io.d.ts.map +1 -0
- package/dist/adapters/node/workspace.io.js +33 -0
- package/dist/adapters/node/workspace.io.js.map +1 -0
- package/dist/adapters/pdfjs/fill.d.ts +42 -0
- package/dist/adapters/pdfjs/fill.d.ts.map +1 -0
- package/dist/adapters/pdfjs/fill.js +91 -0
- package/dist/adapters/pdfjs/fill.js.map +1 -0
- package/dist/build-engine.d.ts +48 -0
- package/dist/build-engine.d.ts.map +1 -0
- package/dist/build-engine.js +148 -0
- package/dist/build-engine.js.map +1 -0
- package/dist/build.d.ts +163 -0
- package/dist/build.d.ts.map +1 -0
- package/dist/build.js +575 -0
- package/dist/build.js.map +1 -0
- package/dist/cli.d.ts +151 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +951 -0
- package/dist/cli.js.map +1 -0
- package/dist/doctor.d.ts +42 -0
- package/dist/doctor.d.ts.map +1 -0
- package/dist/doctor.js +280 -0
- package/dist/doctor.js.map +1 -0
- package/dist/domain/geometry.d.ts +71 -0
- package/dist/domain/geometry.d.ts.map +1 -0
- package/dist/domain/geometry.js +35 -0
- package/dist/domain/geometry.js.map +1 -0
- package/dist/domain/host.d.ts +16 -0
- package/dist/domain/host.d.ts.map +1 -0
- package/dist/domain/host.js +8 -0
- package/dist/domain/host.js.map +1 -0
- package/dist/domain/page-layout.d.ts +34 -0
- package/dist/domain/page-layout.d.ts.map +1 -0
- package/dist/domain/page-layout.js +8 -0
- package/dist/domain/page-layout.js.map +1 -0
- package/dist/domain/paths.d.ts +5 -0
- package/dist/domain/paths.d.ts.map +1 -0
- package/dist/domain/paths.js +2 -0
- package/dist/domain/paths.js.map +1 -0
- package/dist/domain/result.d.ts +23 -0
- package/dist/domain/result.d.ts.map +1 -0
- package/dist/domain/result.js +10 -0
- package/dist/domain/result.js.map +1 -0
- package/dist/domain/sha256.d.ts +7 -0
- package/dist/domain/sha256.d.ts.map +1 -0
- package/dist/domain/sha256.js +14 -0
- package/dist/domain/sha256.js.map +1 -0
- package/dist/domain/text.d.ts +6 -0
- package/dist/domain/text.d.ts.map +1 -0
- package/dist/domain/text.js +7 -0
- package/dist/domain/text.js.map +1 -0
- package/dist/engine.d.ts +93 -0
- package/dist/engine.d.ts.map +1 -0
- package/dist/engine.js +119 -0
- package/dist/engine.js.map +1 -0
- package/dist/exit-code.d.ts +22 -0
- package/dist/exit-code.d.ts.map +1 -0
- package/dist/exit-code.js +10 -0
- package/dist/exit-code.js.map +1 -0
- package/dist/facts-file.d.ts +96 -0
- package/dist/facts-file.d.ts.map +1 -0
- package/dist/facts-file.js +134 -0
- package/dist/facts-file.js.map +1 -0
- package/dist/hooks-settings.d.ts +141 -0
- package/dist/hooks-settings.d.ts.map +1 -0
- package/dist/hooks-settings.js +306 -0
- package/dist/hooks-settings.js.map +1 -0
- package/dist/init.d.ts +201 -0
- package/dist/init.d.ts.map +1 -0
- package/dist/init.js +579 -0
- package/dist/init.js.map +1 -0
- package/dist/latex-log.d.ts +80 -0
- package/dist/latex-log.d.ts.map +1 -0
- package/dist/latex-log.js +187 -0
- package/dist/latex-log.js.map +1 -0
- package/dist/latex-loop.d.ts +129 -0
- package/dist/latex-loop.d.ts.map +1 -0
- package/dist/latex-loop.js +113 -0
- package/dist/latex-loop.js.map +1 -0
- package/dist/link-skills.d.ts +51 -0
- package/dist/link-skills.d.ts.map +1 -0
- package/dist/link-skills.js +199 -0
- package/dist/link-skills.js.map +1 -0
- package/dist/new-paper.d.ts +48 -0
- package/dist/new-paper.d.ts.map +1 -0
- package/dist/new-paper.js +110 -0
- package/dist/new-paper.js.map +1 -0
- package/dist/pdf-facts.d.ts +44 -0
- package/dist/pdf-facts.d.ts.map +1 -0
- package/dist/pdf-facts.js +239 -0
- package/dist/pdf-facts.js.map +1 -0
- package/dist/pdf-geometry.d.ts +170 -0
- package/dist/pdf-geometry.d.ts.map +1 -0
- package/dist/pdf-geometry.js +158 -0
- package/dist/pdf-geometry.js.map +1 -0
- package/dist/ports/download.d.ts +9 -0
- package/dist/ports/download.d.ts.map +1 -0
- package/dist/ports/download.js +2 -0
- package/dist/ports/download.js.map +1 -0
- package/dist/ports/files.d.ts +11 -0
- package/dist/ports/files.d.ts.map +1 -0
- package/dist/ports/files.js +2 -0
- package/dist/ports/files.js.map +1 -0
- package/dist/ports/measure-geometry.d.ts +8 -0
- package/dist/ports/measure-geometry.d.ts.map +1 -0
- package/dist/ports/measure-geometry.js +2 -0
- package/dist/ports/measure-geometry.js.map +1 -0
- package/dist/ports/process.d.ts +45 -0
- package/dist/ports/process.d.ts.map +1 -0
- package/dist/ports/process.js +2 -0
- package/dist/ports/process.js.map +1 -0
- package/dist/ports/tool-installer.d.ts +29 -0
- package/dist/ports/tool-installer.d.ts.map +1 -0
- package/dist/ports/tool-installer.js +2 -0
- package/dist/ports/tool-installer.js.map +1 -0
- package/dist/ports/workspace.d.ts +18 -0
- package/dist/ports/workspace.d.ts.map +1 -0
- package/dist/ports/workspace.js +2 -0
- package/dist/ports/workspace.js.map +1 -0
- package/dist/rules-config.d.ts +34 -0
- package/dist/rules-config.d.ts.map +1 -0
- package/dist/rules-config.js +132 -0
- package/dist/rules-config.js.map +1 -0
- package/dist/structure.d.ts +34 -0
- package/dist/structure.d.ts.map +1 -0
- package/dist/structure.js +149 -0
- package/dist/structure.js.map +1 -0
- package/dist/tex-requirements.d.ts +43 -0
- package/dist/tex-requirements.d.ts.map +1 -0
- package/dist/tex-requirements.js +127 -0
- package/dist/tex-requirements.js.map +1 -0
- package/dist/toolchain.d.ts +159 -0
- package/dist/toolchain.d.ts.map +1 -0
- package/dist/toolchain.js +542 -0
- package/dist/toolchain.js.map +1 -0
- package/dist/types.d.ts +110 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/docs/configuration.md +235 -0
- package/docs/e2e.md +152 -0
- package/docs/incidents.md +59 -0
- package/docs/install.md +170 -0
- package/docs/optional-rules.md +107 -0
- package/docs/package-shape-options.md +262 -0
- package/docs/prior-art/README.md +76 -0
- package/docs/prior-art/blocking-vs-advisory.md +83 -0
- package/docs/prior-art/content-delivery.md +124 -0
- package/docs/prior-art/multi-mode-tools.md +106 -0
- package/docs/prior-art/nondeterministic-checks.md +99 -0
- package/docs/prior-art/package-location.md +422 -0
- package/docs/prior-art/paper-folder-scaffolding.md +538 -0
- package/docs/prior-art/readme-structure.md +69 -0
- package/docs/prior-art/repro/README.md +92 -0
- package/docs/prior-art/repro/claim1-allowedtools.mjs +66 -0
- package/docs/prior-art/repro/claim1-at2.mjs +40 -0
- package/docs/prior-art/repro/claim1-crosschannel.mjs +54 -0
- package/docs/prior-art/repro/claim1-frontmatter.mjs +76 -0
- package/docs/prior-art/repro/claim1-hook-payload-reporter.mjs +10 -0
- package/docs/prior-art/repro/claim1-plugin-frontmatter.mjs +27 -0
- package/docs/prior-art/repro/claim1-plugin-skill.mjs +52 -0
- package/docs/prior-art/repro/claim1-project-skill.mjs +81 -0
- package/docs/prior-art/repro/claim2-marketplace-flat-asclaimed.json +1 -0
- package/docs/prior-art/repro/claim2-marketplace-negative-control.json +1 -0
- package/docs/prior-art/repro/claim2-marketplace-nested-exact.json +9 -0
- package/docs/prior-art/repro/claim2-marketplace-nested-noversion.json +9 -0
- package/docs/prior-art/repro/claim2-marketplace-nested-range.json +1 -0
- package/docs/prior-art/repro/claim3-imports.mjs +50 -0
- package/docs/prior-art/repro/claim4-find-package-json.mjs +8 -0
- package/docs/prior-art/repro/claim4-package-dir.mjs +39 -0
- package/docs/prior-art/repro/claim4-parent-arg.mjs +17 -0
- package/docs/prior-art/repro/claim4-resolve-apis.mjs +21 -0
- package/docs/prior-art/repro/claim4-setup-consumers.mjs +45 -0
- package/docs/prior-art/repro/claim4-yarn-pnp.mjs +70 -0
- package/docs/prior-art/repro/claim5-bin-launch.mjs +39 -0
- package/docs/prior-art/repro/claim5-exports-mutation.mjs +57 -0
- package/docs/prior-art/repro/claim5-resolved-location-and-bin.mjs +33 -0
- package/docs/prior-art/repro/claim6-candidate-ambiguity.mjs +17 -0
- package/docs/prior-art/repro/claim6-doc-path-candidates.mjs +27 -0
- package/docs/prior-art/test-tooling.md +131 -0
- package/docs/rules.md +58 -0
- package/docs/texlive-install-decision.md +230 -0
- package/docs/toolchain.md +152 -0
- package/eslint-rules/doc-fields.harness.mjs +336 -0
- package/eslint-rules/doc-fields.mjs +186 -0
- package/eslint-rules/doc-fields.mutations.mjs +96 -0
- package/eslint-rules/install-path-literals.harness.mjs +121 -0
- package/eslint-rules/install-path-literals.mjs +108 -0
- package/eslint-rules/install-path-literals.mutations.mjs +62 -0
- package/eslint-rules/latex-language.harness.mjs +599 -0
- package/eslint-rules/latex-language.mjs +591 -0
- package/eslint-rules/latex-language.mutations.mjs +196 -0
- package/eslint-rules/paper-research-question.harness.mjs +146 -0
- package/eslint-rules/paper-research-question.mjs +180 -0
- package/eslint-rules/paper-research-question.mutations.mjs +127 -0
- package/eslint-rules/paper-stages.harness.mjs +356 -0
- package/eslint-rules/paper-stages.mjs +455 -0
- package/eslint-rules/paper-stages.mutations.mjs +157 -0
- package/eslint-rules/paper-typography.harness.mjs +291 -0
- package/eslint-rules/paper-typography.mjs +313 -0
- package/eslint-rules/paper-typography.mutations.mjs +131 -0
- package/eslint-rules/papers.harness.mjs +259 -0
- package/eslint-rules/papers.mjs +166 -0
- package/eslint-rules/papers.mutations.mjs +186 -0
- package/eslint-rules/pdf-last-page-balance.harness.mjs +206 -0
- package/eslint-rules/pdf-last-page-balance.mjs +208 -0
- package/eslint-rules/review-findings-cause.harness.mjs +228 -0
- package/eslint-rules/review-findings-cause.mjs +135 -0
- package/eslint-rules/review-findings-cause.mutations.mjs +72 -0
- package/eslint-rules/temp-root-realpath.harness.mjs +176 -0
- package/eslint-rules/temp-root-realpath.mjs +129 -0
- package/eslint-rules/temp-root-realpath.mutations.mjs +99 -0
- package/eslint-rules/tex-build.harness.mjs +753 -0
- package/eslint-rules/tex-build.mjs +322 -0
- package/eslint-rules/tex-build.mutations.mjs +258 -0
- package/eslint.config.mjs +521 -0
- package/fixtures/build-e2e/acmart/PIPELINE-STATUS.md +3 -0
- package/fixtures/build-e2e/acmart/paper.tex +11 -0
- package/fixtures/build-e2e/acmart/venue.json +1 -0
- package/fixtures/build-e2e/broken/PIPELINE-STATUS.md +3 -0
- package/fixtures/build-e2e/broken/paper.tex +7 -0
- package/fixtures/build-e2e/cite/PIPELINE-STATUS.md +3 -0
- package/fixtures/build-e2e/cite/build.sh +5 -0
- package/fixtures/build-e2e/cite/paper.tex +10 -0
- package/fixtures/build-e2e/cite/refs.bib +9 -0
- package/fixtures/build-e2e/empty/PIPELINE-STATUS.md +3 -0
- package/fixtures/build-e2e/empty/paper.tex +6 -0
- package/fixtures/build-e2e/fallback/PIPELINE-STATUS.md +3 -0
- package/fixtures/build-e2e/fallback/paper.tex +11 -0
- package/fixtures/build-e2e/guards/PIPELINE-STATUS.md +3 -0
- package/fixtures/build-e2e/guards/paper.tex +10 -0
- package/fixtures/build-e2e/no-source/PIPELINE-STATUS.md +3 -0
- package/fixtures/build-e2e/unbalanced/PIPELINE-STATUS.md +3 -0
- package/fixtures/build-e2e/unbalanced/paper.tex +28 -0
- package/fixtures/build-e2e/unbalanced/refs.bib +269 -0
- package/fixtures/install-path-literals/clean.fixture.mjs +3 -0
- package/fixtures/install-path-literals/clean.md +15 -0
- package/fixtures/install-path-literals/defect.fixture.mjs +3 -0
- package/fixtures/install-path-literals/defect.md +14 -0
- package/fixtures/latex-language/clean.tex +50 -0
- package/fixtures/latex-language/defect.tex +52 -0
- package/fixtures/paper-research-question/comment-only/PIPELINE-STATUS.md +9 -0
- package/fixtures/paper-research-question/comment-only/paper.tex +7 -0
- package/fixtures/paper-research-question/declared-not-in-paper/PIPELINE-STATUS.md +10 -0
- package/fixtures/paper-research-question/declared-not-in-paper/paper.tex +6 -0
- package/fixtures/paper-research-question/draft/PIPELINE-STATUS.md +6 -0
- package/fixtures/paper-research-question/draft/paper.tex +2 -0
- package/fixtures/paper-research-question/markdown-no-rq/PIPELINE-STATUS.md +9 -0
- package/fixtures/paper-research-question/markdown-no-rq/paper.md +4 -0
- package/fixtures/paper-research-question/shipped-no-rq/PIPELINE-STATUS.md +12 -0
- package/fixtures/paper-research-question/shipped-no-rq/paper.tex +3 -0
- package/fixtures/paper-research-question/shipped-with-rq/PIPELINE-STATUS.md +10 -0
- package/fixtures/paper-research-question/shipped-with-rq/paper.tex +2 -0
- package/fixtures/paper-stages/authors-ran/PIPELINE-STATUS.md +16 -0
- package/fixtures/paper-stages/marker-in-prose/PIPELINE-STATUS.md +17 -0
- package/fixtures/paper-stages/nofile/PIPELINE-STATUS.md +8 -0
- package/fixtures/paper-stages/noheader/PIPELINE-STATUS.md +1 -0
- package/fixtures/paper-stages/noheader/versions/2026-07-22-submitted.pdf +0 -0
- package/fixtures/paper-stages/nothing/PIPELINE-STATUS.md +3 -0
- package/fixtures/paper-stages/ok/PIPELINE-STATUS.md +9 -0
- package/fixtures/paper-stages/ok/versions/2026-07-22-submitted.pdf +0 -0
- package/fixtures/paper-stages/stale/PIPELINE-STATUS.md +1 -0
- package/fixtures/paper-stages/stale/versions/2026-07-22-submitted.STALE-WRONG-FILE.pdf +0 -0
- package/fixtures/paper-stages/twice/PIPELINE-STATUS.md +14 -0
- package/fixtures/paper-stages/twice/versions/2026-08-06-submitted.pdf +0 -0
- package/fixtures/paper-stages/twice/versions/2026-10-24-submitted.pdf +0 -0
- package/fixtures/paper-stages/undeclared/PIPELINE-STATUS.md +8 -0
- package/fixtures/paper-stages/undeclared/versions/2026-07-22-submitted.pdf +0 -0
- package/fixtures/paper-stages/undeclared/versions/2026-08-29-camera-ready.pdf +0 -0
- package/fixtures/paper-stages/wrongsize/PIPELINE-STATUS.md +8 -0
- package/fixtures/paper-stages/wrongsize/versions/2026-07-22-submitted.pdf +0 -0
- package/fixtures/paper-typography/clean-paper/paper.tex +29 -0
- package/fixtures/paper-typography/messy-paper/paper.tex +27 -0
- package/fixtures/pdf-facts/README.md +22 -0
- package/fixtures/pdf-facts/corrupt-font.pdf +0 -0
- package/fixtures/pdf-facts/encrypted.pdf +0 -0
- package/fixtures/pdf-facts/hidden-text.pdf +0 -0
- package/fixtures/pdf-facts/hidden-text.tex +28 -0
- package/fixtures/pdf-facts/t3-all.pdf +0 -0
- package/fixtures/pdf-facts/t3-all.tex +8 -0
- package/fixtures/pdf-facts/t3-mixed.pdf +0 -0
- package/fixtures/pdf-facts/t3-mixed.tex +9 -0
- package/fixtures/pdf-facts/ttf.pdf +2240 -1
- package/fixtures/pdf-facts/ttf.tex +6 -0
- package/fixtures/real-markdown-paper/baseline.json +24 -0
- package/fixtures/real-markdown-paper/baseline.mjs +48 -0
- package/fixtures/render-paper/build-clean.sh +25 -0
- package/fixtures/render-paper/build-defect.sh +15 -0
- package/fixtures/review-findings-cause/clean.md +17 -0
- package/fixtures/review-findings-cause/defect.md +14 -0
- package/fixtures/review-findings-cause/old-debt.md +14 -0
- package/fixtures/review-findings-cause/quiet-in-fence.md +16 -0
- package/fixtures/tex-build/clean.tex +21 -0
- package/fixtures/tex-build/defect.tex +24 -0
- package/fixtures/tex-build/frontmatter-clean.tex +25 -0
- package/fixtures/tex-build/frontmatter-defect.tex +23 -0
- package/fixtures/toolchain-mirror/catalog.txt +5 -0
- package/fixtures/toolchain-mirror/install-tl +27 -0
- package/fixtures/toolchain-mirror/release-texlive.txt +3 -0
- package/fixtures/toolchain-mirror/release-year +1 -0
- package/fixtures/toolchain-mirror/stub-kpsewhich +8 -0
- package/fixtures/toolchain-mirror/stub-pdflatex +3 -0
- package/fixtures/toolchain-mirror/stub-tlmgr +44 -0
- package/hooks/hooks.harness.mjs +713 -0
- package/hooks/hooks.mutations.mjs +337 -0
- package/hooks/paper-edit-guard.hook.d.mts +13 -0
- package/hooks/paper-edit-guard.hook.mjs +457 -0
- package/hooks/paper-skills-nudge.hook.mjs +136 -0
- package/hooks/paper-status-gates.hook.mjs +156 -0
- package/hooks/paper-status-gates.sh +91 -0
- package/lib/agent-cli-version.harness.mjs +165 -0
- package/lib/agent-cli-version.mjs +106 -0
- package/lib/agent-cli-version.mutations.mjs +109 -0
- package/lib/markdown.mjs +386 -0
- package/lib/mutation-driver.harness.mjs +227 -0
- package/lib/mutation-driver.mjs +397 -0
- package/lib/mutation-driver.mutations.mjs +68 -0
- package/lib/paper-config.d.mts +34 -0
- package/lib/paper-config.harness.mjs +286 -0
- package/lib/paper-config.mjs +142 -0
- package/lib/paper-config.mutations.mjs +143 -0
- package/lib/skill-checks.mjs +701 -0
- package/lib/skill-corpus.mjs +403 -0
- package/lib/skill-eval-fixture.mjs +63 -0
- package/lib/skill-eval-kit.mjs +257 -0
- package/lib/skill-trigger-cases.harness.mjs +170 -0
- package/lib/skill-trigger-cases.mjs +446 -0
- package/lib/skill-trigger-cases.mutations.mjs +65 -0
- package/lib/trigger-ledger.mjs +215 -0
- package/package.json +97 -0
- package/plugin/.claude-plugin/plugin.json +8 -0
- package/plugin/hooks/hooks.json +30 -0
- package/scripts/check.harness.mjs +177 -0
- package/scripts/check.mjs +239 -0
- package/scripts/check.mutations.mjs +110 -0
- package/scripts/eslint-report-guard.mjs +82 -0
- package/scripts/exclusive.mjs +138 -0
- package/scripts/harness-api.frozen.json +76 -0
- package/scripts/harness-api.test.ts +175 -0
- package/scripts/layer-legacy-frozen.d.mts +28 -0
- package/scripts/layer-legacy-frozen.mjs +152 -0
- package/scripts/layer-legacy-frozen.test.ts +115 -0
- package/scripts/layer-legacy.frozen.json +50 -0
- package/scripts/mutation-batteries-frozen.harness.mjs +204 -0
- package/scripts/mutation-batteries-frozen.mjs +238 -0
- package/scripts/mutation-batteries.frozen.json +117 -0
- package/scripts/release-config.test.ts +90 -0
- package/scripts/rules-are-content-only.harness.mjs +113 -0
- package/scripts/rules-are-content-only.mjs +138 -0
- package/scripts/rules-are-content-only.mutations.mjs +81 -0
- package/scripts/rules-see-files.harness.mjs +115 -0
- package/scripts/rules-see-files.mjs +99 -0
- package/scripts/rules-see-files.mutations.mjs +131 -0
- package/scripts/run-mutations.mjs +100 -0
- package/scripts/semantic-release-plugins.d.ts +16 -0
- package/skills/README.md +15 -0
- package/skills/analyze-sibling-paper/SKILL.md +170 -0
- package/skills/analyze-sibling-paper/SKILL.md.spec.ts +186 -0
- package/skills/analyze-sibling-paper/analyze-sibling-paper.eval.mjs +19 -0
- package/skills/analyze-sibling-paper/analyze-sibling-paper.harness.mjs +23 -0
- package/skills/argument-arc/SKILL.md +177 -0
- package/skills/argument-arc/SKILL.md.spec.ts +192 -0
- package/skills/argument-arc/argument-arc.eval.mjs +19 -0
- package/skills/argument-arc/argument-arc.harness.mjs +23 -0
- package/skills/build-benchmark/SKILL.md +213 -0
- package/skills/build-benchmark/SKILL.md.spec.ts +220 -0
- package/skills/build-benchmark/build-benchmark.eval.mjs +19 -0
- package/skills/build-benchmark/build-benchmark.harness.mjs +23 -0
- package/skills/build-benchmark/references/adversarial-cold-repro.md +68 -0
- package/skills/camera-ready/SKILL.md +148 -0
- package/skills/camera-ready/SKILL.md.spec.ts +164 -0
- package/skills/camera-ready/camera-ready.eval.mjs +19 -0
- package/skills/camera-ready/camera-ready.harness.mjs +23 -0
- package/skills/cold-read-diff/SKILL.md +160 -0
- package/skills/cold-read-diff/SKILL.md.spec.ts +166 -0
- package/skills/cold-read-diff/cold-read-diff.eval.mjs +19 -0
- package/skills/cold-read-diff/cold-read-diff.harness.mjs +23 -0
- package/skills/draft-paper/SKILL.md +152 -0
- package/skills/draft-paper/SKILL.md.spec.ts +169 -0
- package/skills/draft-paper/draft-paper.eval.mjs +19 -0
- package/skills/draft-paper/draft-paper.harness.mjs +23 -0
- package/skills/extend-paper/SKILL.md +99 -0
- package/skills/extend-paper/SKILL.md.spec.ts +116 -0
- package/skills/extend-paper/extend-paper.eval.mjs +19 -0
- package/skills/extend-paper/extend-paper.harness.mjs +23 -0
- package/skills/find-venue/SKILL.md +128 -0
- package/skills/find-venue/SKILL.md.spec.ts +145 -0
- package/skills/find-venue/find-venue.eval.mjs +19 -0
- package/skills/find-venue/find-venue.harness.mjs +23 -0
- package/skills/grade-paper-writing/SKILL.md +436 -0
- package/skills/grade-paper-writing/SKILL.md.spec.ts +453 -0
- package/skills/grade-paper-writing/fixtures/control_gopen.txt +1 -0
- package/skills/grade-paper-writing/fixtures/control_human_paper.txt +1 -0
- package/skills/grade-paper-writing/fixtures/rewrite.txt +1 -0
- package/skills/grade-paper-writing/fixtures/specimen.txt +1 -0
- package/skills/grade-paper-writing/fixtures/structure-checks.md +22 -0
- package/skills/grade-paper-writing/grade-paper-writing.eval.mjs +19 -0
- package/skills/grade-paper-writing/grade-paper-writing.harness.mjs +23 -0
- package/skills/grade-paper-writing/prose-lint.mjs +713 -0
- package/skills/harden-paper/SKILL.md +318 -0
- package/skills/harden-paper/SKILL.md.spec.ts +336 -0
- package/skills/harden-paper/check-numbers.sh +33 -0
- package/skills/harden-paper/check-release-claims.sh +35 -0
- package/skills/harden-paper/fixtures/uncited-assertions-sample.md +43 -0
- package/skills/harden-paper/fixtures/uncited-assertions-sample.tex +77 -0
- package/skills/harden-paper/harden-paper.eval.mjs +19 -0
- package/skills/harden-paper/harden-paper.harness.mjs +23 -0
- package/skills/map-prior-work/SKILL.md +211 -0
- package/skills/map-prior-work/SKILL.md.spec.ts +227 -0
- package/skills/map-prior-work/map-prior-work.eval.mjs +19 -0
- package/skills/map-prior-work/map-prior-work.harness.mjs +23 -0
- package/skills/osf-artifact-upload/SKILL.md +52 -0
- package/skills/osf-artifact-upload/SKILL.md.spec.ts +59 -0
- package/skills/osf-artifact-upload/osf-artifact-upload.eval.mjs +22 -0
- package/skills/osf-artifact-upload/osf-artifact-upload.harness.mjs +103 -0
- package/skills/paper-adversarial-review/SKILL.md +126 -0
- package/skills/paper-adversarial-review/SKILL.md.spec.ts +142 -0
- package/skills/paper-adversarial-review/paper-adversarial-review.eval.mjs +19 -0
- package/skills/paper-adversarial-review/paper-adversarial-review.harness.mjs +23 -0
- package/skills/paper-pipeline/PIPELINE-MAP.md +371 -0
- package/skills/paper-pipeline/SKILL.md +499 -0
- package/skills/paper-pipeline/SKILL.md.spec.ts +517 -0
- package/skills/paper-pipeline/description-language.eval.mjs +347 -0
- package/skills/paper-pipeline/framing-vs-vocabulary.eval.mjs +891 -0
- package/skills/paper-pipeline/grade-paper-writing-ablation.eval.mjs +1254 -0
- package/skills/paper-pipeline/paper-pipeline.eval.mjs +22 -0
- package/skills/paper-pipeline/paper-pipeline.harness.mjs +143 -0
- package/skills/paper-pipeline/pipeline-firing.baseline.json +270 -0
- package/skills/paper-pipeline/pipeline-firing.eval.mjs +664 -0
- package/skills/paper-pipeline/pipeline-language.eval.mjs +672 -0
- package/skills/paper-pipeline/references/acceptance-gate.md +329 -0
- package/skills/paper-pipeline/references/acl-venue-rules.md +142 -0
- package/skills/paper-pipeline/references/anonymization.md +68 -0
- package/skills/paper-pipeline/references/artifact-checklist.md +93 -0
- package/skills/paper-pipeline/references/body-vs-appendix.md +97 -0
- package/skills/paper-pipeline/references/credit-criteria.md +69 -0
- package/skills/paper-pipeline/references/occupancy-2026-08-06-probe/README.md +35 -0
- package/skills/paper-pipeline/references/occupancy-2026-08-06-probe/run_retext.mjs +24 -0
- package/skills/paper-pipeline/references/occupancy-2026-08-06-probe/sentences.txt +11 -0
- package/skills/paper-pipeline/references/occupancy-2026-08-06-probe/test_sentences.py +25 -0
- package/skills/paper-pipeline/references/occupancy-2026-08-06-prose-checkers.md +538 -0
- package/skills/paper-pipeline/references/occupancy-2026-08-06-reproducible-tooling.md +431 -0
- package/skills/paper-pipeline/references/occupancy-2026-08-06-staleness-and-orchestration.md +592 -0
- package/skills/paper-pipeline/references/pipeline-status-template.md +162 -0
- package/skills/paper-pipeline/references/review-ratchet.md +36 -0
- package/skills/paper-pipeline/references/sweep-2026-08-09-ideal-pipeline.md +585 -0
- package/skills/paper-pipeline/references/writing-craft.md +448 -0
- package/skills/paper-pipeline/repro/2026-08-07-description-language-control.log +63 -0
- package/skills/paper-pipeline/repro/2026-08-07-fork-check.log +52 -0
- package/skills/paper-pipeline/repro/2026-08-07-fork-check2.log +33 -0
- package/skills/paper-pipeline/repro/2026-08-07-language-eval-pilot.log +33 -0
- package/skills/paper-pipeline/repro/2026-08-07-language-eval-raw.log +166 -0
- package/skills/paper-pipeline/repro/2026-08-08-framing-vs-vocabulary-oracle.json +338 -0
- package/skills/paper-pipeline/repro/2026-08-08-framing-vs-vocabulary-oracle.log +118 -0
- package/skills/paper-pipeline/repro/2026-08-08-framing-vs-vocabulary-raw.log +245 -0
- package/skills/paper-pipeline/repro/2026-08-08-framing-vs-vocabulary.json +776 -0
- package/skills/paper-pipeline/repro/2026-08-08-grade-paper-writing-ablation-A6-oracle.log +53 -0
- package/skills/paper-pipeline/repro/2026-08-08-grade-paper-writing-ablation-A6-raw.log +89 -0
- package/skills/paper-pipeline/repro/2026-08-08-grade-paper-writing-ablation-oracle.json +450 -0
- package/skills/paper-pipeline/repro/2026-08-08-grade-paper-writing-ablation-oracle.log +136 -0
- package/skills/paper-pipeline/repro/2026-08-08-grade-paper-writing-ablation-raw.log +242 -0
- package/skills/paper-pipeline/repro/2026-08-08-grade-paper-writing-ablation-setupdiff.log +59 -0
- package/skills/paper-pipeline/repro/2026-08-08-grade-paper-writing-ablation.json +1032 -0
- package/skills/paper-pipeline/repro/2026-08-08-parent-replication-gpw.json +139 -0
- package/skills/paper-pipeline/repro/2026-08-08-parent-replication-gpw.log +98 -0
- package/skills/paper-pipeline/repro/2026-08-08-parent-replication.mjs +92 -0
- package/skills/paper-pipeline/repro/README.md +129 -0
- package/skills/paper-pipeline/repro/analyze-language-eval.py +116 -0
- package/skills/paper-pipeline/scripts/README.md +344 -0
- package/skills/paper-pipeline/scripts/announce.mjs +67 -0
- package/skills/paper-pipeline/scripts/artifact-coverage.harness.mjs +496 -0
- package/skills/paper-pipeline/scripts/artifact-coverage.mjs +397 -0
- package/skills/paper-pipeline/scripts/artifact-coverage.mutations.mjs +218 -0
- package/skills/paper-pipeline/scripts/check-provenance.mjs +184 -0
- package/skills/paper-pipeline/scripts/consumer.d.mts +32 -0
- package/skills/paper-pipeline/scripts/consumer.harness.mjs +562 -0
- package/skills/paper-pipeline/scripts/consumer.mjs +535 -0
- package/skills/paper-pipeline/scripts/consumer.mutations.mjs +190 -0
- package/skills/paper-pipeline/scripts/extract-ref-facts.harness.mjs +457 -0
- package/skills/paper-pipeline/scripts/extract-ref-facts.mjs +656 -0
- package/skills/paper-pipeline/scripts/extract-ref-facts.mutations.mjs +54 -0
- package/skills/paper-pipeline/scripts/fixtures/clean/PIPELINE-STATUS.md +51 -0
- package/skills/paper-pipeline/scripts/fixtures/dirty/PIPELINE-STATUS.md +52 -0
- package/skills/paper-pipeline/scripts/fixtures/dirty/paper.md +6 -0
- package/skills/paper-pipeline/scripts/fixtures/real-bib/refs.bib +153 -0
- package/skills/paper-pipeline/scripts/generated-code.harness.mjs +466 -0
- package/skills/paper-pipeline/scripts/generated-code.mjs +338 -0
- package/skills/paper-pipeline/scripts/generated-code.mutations.mjs +254 -0
- package/skills/paper-pipeline/scripts/ledger.mjs +623 -0
- package/skills/paper-pipeline/scripts/ledger.selftest.mjs +286 -0
- package/skills/paper-pipeline/scripts/pipeline-check.harness.mjs +389 -0
- package/skills/paper-pipeline/scripts/pipeline-check.mjs +737 -0
- package/skills/paper-pipeline/scripts/pipeline-check.mutations.mjs +54 -0
- package/skills/paper-pipeline/scripts/pipeline-edges.mjs +169 -0
- package/skills/paper-pipeline/scripts/population-map.harness.mjs +178 -0
- package/skills/paper-pipeline/scripts/population-map.mjs +181 -0
- package/skills/paper-pipeline/scripts/population-map.mutations.mjs +65 -0
- package/skills/paper-pipeline/scripts/population-map.selftest.mjs +122 -0
- package/skills/paper-pipeline/scripts/provenance.harness.mjs +240 -0
- package/skills/paper-pipeline/scripts/provenance.mutations.mjs +59 -0
- package/skills/paper-pipeline/scripts/round-diff.harness.mjs +881 -0
- package/skills/paper-pipeline/scripts/round-diff.mjs +576 -0
- package/skills/paper-pipeline/scripts/round-diff.mutations.mjs +276 -0
- package/skills/paper-pipeline/scripts/run-mechanical.mjs +633 -0
- package/skills/paper-pipeline/scripts/status.mjs +295 -0
- package/skills/paper-status/SKILL.md +183 -0
- package/skills/paper-status/SKILL.md.spec.ts +190 -0
- package/skills/paper-status/paper-status.eval.mjs +22 -0
- package/skills/paper-status/paper-status.harness.mjs +25 -0
- package/skills/pc-panel-review/SKILL.md +263 -0
- package/skills/pc-panel-review/SKILL.md.spec.ts +280 -0
- package/skills/pc-panel-review/pc-panel-review.eval.mjs +19 -0
- package/skills/pc-panel-review/pc-panel-review.harness.mjs +23 -0
- package/skills/plan-paper-timeline/SKILL.md +182 -0
- package/skills/plan-paper-timeline/SKILL.md.spec.ts +200 -0
- package/skills/plan-paper-timeline/fixtures/fake-google-calendar.mjs +239 -0
- package/skills/plan-paper-timeline/plan-paper-timeline.effects.harness.mjs +431 -0
- package/skills/plan-paper-timeline/plan-paper-timeline.effects.mutations.mjs +65 -0
- package/skills/plan-paper-timeline/plan-paper-timeline.eval.mjs +19 -0
- package/skills/plan-paper-timeline/plan-paper-timeline.harness.mjs +23 -0
- package/skills/render-paper/SKILL.md +159 -0
- package/skills/render-paper/SKILL.md.spec.ts +166 -0
- package/skills/render-paper/check-render.sh +419 -0
- package/skills/render-paper/checkers-requirements.txt +55 -0
- package/skills/render-paper/ensure-checkers.sh +69 -0
- package/skills/render-paper/extract-pdf-facts.harness.mjs +166 -0
- package/skills/render-paper/extract-pdf-facts.mjs +144 -0
- package/skills/render-paper/render-paper.eval.mjs +19 -0
- package/skills/render-paper/render-paper.harness.mjs +339 -0
- package/skills/research-ideate/SKILL.md +136 -0
- package/skills/research-ideate/SKILL.md.spec.ts +152 -0
- package/skills/research-ideate/research-ideate.eval.mjs +19 -0
- package/skills/research-ideate/research-ideate.harness.mjs +23 -0
- package/skills/skill-contract.mutations.mjs +179 -0
- package/skills/study-accepted-papers/SKILL.md +206 -0
- package/skills/study-accepted-papers/SKILL.md.spec.ts +223 -0
- package/skills/study-accepted-papers/study-accepted-papers.eval.mjs +19 -0
- package/skills/study-accepted-papers/study-accepted-papers.harness.mjs +23 -0
- package/skills/submit-paper/SKILL.md +182 -0
- package/skills/submit-paper/SKILL.md.spec.ts +199 -0
- package/skills/submit-paper/check-deanon.sh +149 -0
- package/skills/submit-paper/references/publishers/acm.md +92 -0
- package/skills/submit-paper/references/venues/agenticdev.jsonc +108 -0
- package/skills/submit-paper/references/venues/agenticdev.md +139 -0
- package/skills/submit-paper/references/venues/agenticdev.tex +19 -0
- package/skills/submit-paper/references/venues/aisec.jsonc +101 -0
- package/skills/submit-paper/references/venues/aisec.md +105 -0
- package/skills/submit-paper/references/venues/paper-guards.tex +41 -0
- package/skills/submit-paper/references/venues/realm.jsonc +81 -0
- package/skills/submit-paper/references/venues/realm.md +155 -0
- package/skills/submit-paper/references/venues/tex-base.jsonc +50 -0
- package/skills/submit-paper/references/venues/venue-profile.schema.json +74 -0
- package/skills/submit-paper/submit-paper.eval.mjs +19 -0
- package/skills/submit-paper/submit-paper.harness.mjs +23 -0
- package/skills/sweep-design-space/SKILL.md +269 -0
- package/skills/sweep-design-space/SKILL.md.spec.ts +285 -0
- package/skills/sweep-design-space/sweep-design-space.eval.mjs +19 -0
- package/skills/sweep-design-space/sweep-design-space.harness.mjs +23 -0
- package/skills/tighten-paper/SKILL.md +368 -0
- package/skills/tighten-paper/SKILL.md.spec.ts +384 -0
- package/skills/tighten-paper/structure.mjs +371 -0
- package/skills/tighten-paper/tighten-paper.eval.mjs +19 -0
- package/skills/tighten-paper/tighten-paper.harness.mjs +23 -0
- package/skills/verify-citations/SKILL.md +328 -0
- package/skills/verify-citations/SKILL.md.spec.ts +345 -0
- package/skills/verify-citations/scripts/bib-authors.mjs +479 -0
- package/skills/verify-citations/scripts/bib-authors.test.mjs +175 -0
- package/skills/verify-citations/scripts/verify-cites.mjs +1108 -0
- package/skills/verify-citations/scripts/verify-cites.test.mjs +735 -0
- package/skills/verify-citations/verify-citations.eval.mjs +19 -0
- package/skills/verify-citations/verify-citations.harness.mjs +23 -0
- package/src/CLAUDE.md +51 -0
- package/src/action-ref.test.ts +26 -0
- package/src/action-ref.ts +15 -0
- package/src/adapters/banal/failure.test.ts +63 -0
- package/src/adapters/banal/failure.ts +118 -0
- package/src/adapters/banal/index.test.ts +119 -0
- package/src/adapters/banal/index.ts +100 -0
- package/src/adapters/banal/install.test.ts +20 -0
- package/src/adapters/banal/install.ts +41 -0
- package/src/adapters/banal/invocation.test.ts +74 -0
- package/src/adapters/banal/invocation.ts +95 -0
- package/src/adapters/banal/locate.test.ts +52 -0
- package/src/adapters/banal/locate.ts +84 -0
- package/src/adapters/banal/output.test.ts +140 -0
- package/src/adapters/banal/output.ts +141 -0
- package/src/adapters/banal/pin.ts +30 -0
- package/src/adapters/banal/probe.ts +35 -0
- package/src/adapters/banal/run.test.ts +191 -0
- package/src/adapters/banal/run.ts +244 -0
- package/src/adapters/banal/settings.test.ts +31 -0
- package/src/adapters/banal/settings.ts +55 -0
- package/src/adapters/banal/xml.test.ts +111 -0
- package/src/adapters/banal/xml.ts +112 -0
- package/src/adapters/curl/download.io.ts +73 -0
- package/src/adapters/curl/download.test.ts +55 -0
- package/src/adapters/curl/index.ts +5 -0
- package/src/adapters/memory/index.ts +131 -0
- package/src/adapters/node/files.io.ts +39 -0
- package/src/adapters/node/files.test.ts +28 -0
- package/src/adapters/node/host.io.ts +15 -0
- package/src/adapters/node/index.ts +36 -0
- package/src/adapters/node/process.io.ts +49 -0
- package/src/adapters/node/process.test.ts +46 -0
- package/src/adapters/node/workspace.io.ts +40 -0
- package/src/adapters/node/workspace.test.ts +58 -0
- package/src/adapters/pdfjs/fill.test.ts +111 -0
- package/src/adapters/pdfjs/fill.ts +141 -0
- package/src/build-engine.harness.mjs +314 -0
- package/src/build-engine.ts +219 -0
- package/src/build.harness.mjs +631 -0
- package/src/build.mutations.mjs +195 -0
- package/src/build.ts +793 -0
- package/src/cli.harness.mjs +2007 -0
- package/src/cli.mutations.mjs +448 -0
- package/src/cli.ts +1189 -0
- package/src/doctor.harness.mjs +396 -0
- package/src/doctor.mutations.mjs +175 -0
- package/src/doctor.ts +356 -0
- package/src/domain/geometry.ts +108 -0
- package/src/domain/host.ts +23 -0
- package/src/domain/page-layout.ts +32 -0
- package/src/domain/paths.ts +5 -0
- package/src/domain/result.test.ts +26 -0
- package/src/domain/result.ts +29 -0
- package/src/domain/sha256.test.ts +12 -0
- package/src/domain/sha256.ts +21 -0
- package/src/domain/text.ts +11 -0
- package/src/engine.harness.mjs +252 -0
- package/src/engine.ts +176 -0
- package/src/exit-code.test.ts +21 -0
- package/src/exit-code.ts +38 -0
- package/src/facts-file.test.ts +240 -0
- package/src/facts-file.ts +241 -0
- package/src/hooks-settings.harness.mjs +386 -0
- package/src/hooks-settings.mutations.mjs +116 -0
- package/src/hooks-settings.ts +434 -0
- package/src/init.ts +900 -0
- package/src/latex-log.harness.mjs +226 -0
- package/src/latex-log.ts +234 -0
- package/src/latex-loop.harness.mjs +449 -0
- package/src/latex-loop.ts +211 -0
- package/src/link-skills.harness.mjs +273 -0
- package/src/link-skills.mutations.mjs +136 -0
- package/src/link-skills.ts +258 -0
- package/src/new-paper.harness.mjs +216 -0
- package/src/new-paper.mutations.mjs +79 -0
- package/src/new-paper.ts +158 -0
- package/src/pdf-facts.harness.mjs +188 -0
- package/src/pdf-facts.ts +327 -0
- package/src/pdf-geometry.harness.mjs +254 -0
- package/src/pdf-geometry.ts +300 -0
- package/src/ports/download.ts +10 -0
- package/src/ports/files.ts +11 -0
- package/src/ports/measure-geometry.ts +8 -0
- package/src/ports/process.ts +46 -0
- package/src/ports/tool-installer.ts +33 -0
- package/src/ports/workspace.ts +20 -0
- package/src/rules-config.harness.mjs +114 -0
- package/src/rules-config.ts +178 -0
- package/src/structure.harness.mjs +179 -0
- package/src/structure.mutations.mjs +83 -0
- package/src/structure.ts +166 -0
- package/src/tex-requirements.harness.mjs +238 -0
- package/src/tex-requirements.ts +181 -0
- package/src/toolchain.harness.mjs +651 -0
- package/src/toolchain.ts +755 -0
- package/src/types.ts +106 -0
- package/templates/paper/PIPELINE-STATUS.md +72 -0
- package/templates/paper/paper.md +4 -0
- package/templates/paper/paper.tex +8 -0
- 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
|
+
);
|