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,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* paper-status-gates — a PostToolUse react: editing a paper's source surfaces that paper's
|
|
3
|
+
* readiness verdict and its UNRUN GATES, unprompted.
|
|
4
|
+
*
|
|
5
|
+
* A ☐ against study-accepted / pc-panel / harden / verify-citations is a HOLE, not an
|
|
6
|
+
* "anti-churn" skip: a session that skipped three of them had the panel come back with real
|
|
7
|
+
* must-fixes. Advisory, never blocks, always exits 0 — a `react`'s type has no `deny`.
|
|
8
|
+
*
|
|
9
|
+
* ── WHY IT WAS WORTH SURFACING AT ALL ───────────────────────────────────────
|
|
10
|
+
* The checker PARSED the verdict line (to test it was not a bare percentage) and never PRINTED
|
|
11
|
+
* it, so the one fact its author kept asking for by hand was the one thing the tooling reliably
|
|
12
|
+
* withheld. A status file nobody is shown is a status file nobody reads.
|
|
13
|
+
*
|
|
14
|
+
* ── THE PURE / IMPURE LINE, AND WHERE IT REALLY FALLS ───────────────────────
|
|
15
|
+
* An earlier header claimed this half "READS A FILE and interpolates its contents, and a vigiles
|
|
16
|
+
* `react` is a pure function — it cannot", and called that the honest boundary of the closed
|
|
17
|
+
* vocabulary. That reading was one step too pessimistic. The boundary is real, but it falls
|
|
18
|
+
* between DECIDING and DOING, not between "hook" and "shell": deciding whether this edit
|
|
19
|
+
* concerns a paper, and WHICH one, is pure and belongs here; reading that paper's status file
|
|
20
|
+
* and running the checker is a tool, and `run()` is how a react invokes one.
|
|
21
|
+
*
|
|
22
|
+
* ── WHY THE DIRECTORY NAME IS VALIDATED BEFORE IT REACHES A COMMAND ─────────
|
|
23
|
+
* `run()` takes a string AND IT GOES THROUGH A SHELL — measured 2026-09-12: a `$(…)` inside a
|
|
24
|
+
* `run()` command substitutes. This one embeds a path taken from the event, so the name is
|
|
25
|
+
* extracted by an anchored pattern and accepted only if it is `[A-Za-z0-9._-]+` — no slash, no
|
|
26
|
+
* space, no shell metacharacter can survive. The shell predecessor got this incidentally, via a
|
|
27
|
+
* `grep -oE`; here it is explicit, because "incidentally safe" is how it stops being safe.
|
|
28
|
+
*
|
|
29
|
+
* 🔴 THE `(?:^|\/)` IS NOT DECORATION, AND LEAVING IT OUT KILLED THIS HOOK ONCE. The first
|
|
30
|
+
* version anchored the papers root at `^`, which passed every test in the harness — because the
|
|
31
|
+
* harness's own edit helper builds a RELATIVE path. The live harness sends an ABSOLUTE one.
|
|
32
|
+
* Measured against the real runtime:
|
|
33
|
+
*
|
|
34
|
+
* file_path "<root>/<paper>/paper.md" → fires
|
|
35
|
+
* file_path "/abs/path/to/repo/<root>/<paper>/…" → SILENT
|
|
36
|
+
*
|
|
37
|
+
* So the hook would have been dead in production and green in the tests: precisely the
|
|
38
|
+
* false-confidence class this whole exercise is about, reintroduced while converting a hook away
|
|
39
|
+
* from it. The shell version was accidentally immune — it grepped the raw payload with no anchor
|
|
40
|
+
* at all. The harness now sends BOTH spellings for every case.
|
|
41
|
+
*
|
|
42
|
+
* The boundary is still required (`^` or `/`), so a directory merely ENDING in the root's last
|
|
43
|
+
* segment cannot smuggle a match.
|
|
44
|
+
*
|
|
45
|
+
* ── WHAT THIS HOOK'S OUTPUT DOES *NOT* DO, measured and named ───────────────
|
|
46
|
+
* ⚠️ A `run()` reaction's output is passed through RAW — 0 bytes of `hookSpecificOutput` on
|
|
47
|
+
* stdout, the tool's own text on stderr (measured 2026-09-12: 1 184 bytes of stderr, 0 of
|
|
48
|
+
* stdout). Only a `notice` on an injectable event becomes `additionalContext`. So this hook's
|
|
49
|
+
* findings reach a HUMAN reading the transcript's debug output, not the model's context. That is
|
|
50
|
+
* a property of the reaction KIND, not a bug here, and it is the reason the static half lives in
|
|
51
|
+
* `paper-skills-nudge` as a `notice` instead: that one does land.
|
|
52
|
+
*
|
|
53
|
+
* ── WHY `.mjs` AND NOT A `.hook.ts` SPEC ────────────────────────────────────
|
|
54
|
+
* See `paper-edit-guard.hook.mjs`: a `.ts` hook reached through `node_modules` does not load,
|
|
55
|
+
* and a consumer-side thin spec importing this logic is refused by `vigiles compile` and can
|
|
56
|
+
* never be re-stamped. Both halves measured there.
|
|
57
|
+
*/
|
|
58
|
+
import {
|
|
59
|
+
experimental_defineReact,
|
|
60
|
+
tools,
|
|
61
|
+
provide,
|
|
62
|
+
run,
|
|
63
|
+
nothing,
|
|
64
|
+
} from "vigiles/hook";
|
|
65
|
+
|
|
66
|
+
/** The key every carrier of this package reads its consumer-specific settings from. */
|
|
67
|
+
export const CONFIG_KEY = "paperlint";
|
|
68
|
+
/** The key's name before 2.0.0 — still read, a copy of `lib/paper-config.mjs`. */
|
|
69
|
+
export const LEGACY_CONFIG_KEY = "research-paper-pipeline";
|
|
70
|
+
/** The default. A consumer that declares nothing is assumed to keep papers in `papers/`. */
|
|
71
|
+
export const DEFAULT_PAPERS_ROOT = "papers";
|
|
72
|
+
/**
|
|
73
|
+
* The field under CONFIG_KEY that names the papers directory, and its old name. A copy of the
|
|
74
|
+
* constants in `lib/paper-config.mjs` (a hook may import nothing but `vigiles/hook`);
|
|
75
|
+
* `lib/paper-config.harness.mjs` checks that the copies match.
|
|
76
|
+
*/
|
|
77
|
+
export const PAPERS_DIR_FIELD = "papersDir";
|
|
78
|
+
export const OLD_PAPERS_DIR_FIELD = "papers";
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* The declared papers root, or `null` when it is unusable.
|
|
82
|
+
*
|
|
83
|
+
* ⚠️ `null` means SILENCE, not refusal — same asymmetry as `paper-skills-nudge` and for the same
|
|
84
|
+
* reason: this is advisory, and a `react` cannot deny anyway. The gate (`paper-edit-guard`) is
|
|
85
|
+
* the one that must refuse on an unreadable declaration, because there a silent default would
|
|
86
|
+
* pass the very writes it exists to stop.
|
|
87
|
+
*
|
|
88
|
+
* 🔴 `declared === undefined`, NOT `declared ?? DEFAULT` — `"papersDir": null` is a keystroke, not
|
|
89
|
+
* an absence.
|
|
90
|
+
*/
|
|
91
|
+
const papersRoot = (rawPkg) => {
|
|
92
|
+
let declared;
|
|
93
|
+
try {
|
|
94
|
+
const pkg = JSON.parse(rawPkg);
|
|
95
|
+
const settings = pkg?.[CONFIG_KEY] ?? pkg?.[LEGACY_CONFIG_KEY];
|
|
96
|
+
// Old field name: stay silent rather than fall back to the default directory.
|
|
97
|
+
if (settings && Object.hasOwn(settings, OLD_PAPERS_DIR_FIELD)) return null;
|
|
98
|
+
declared = settings?.[PAPERS_DIR_FIELD];
|
|
99
|
+
} catch {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
const root = declared === undefined ? DEFAULT_PAPERS_ROOT : declared;
|
|
103
|
+
if (typeof root !== "string" || root.length === 0) return null;
|
|
104
|
+
return root.replace(/\/+$/, "");
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* A paper SOURCE under `<root>/<dir>/`, with `<dir>` captured.
|
|
109
|
+
*
|
|
110
|
+
* 🔴 THE ROOT IS REGEX-ESCAPED. It is a path from a config file, and `.` is both a legal
|
|
111
|
+
* directory character and a regex wildcard: an unescaped root like `docs.v2/papers` would match
|
|
112
|
+
* `docsXv2/papers` too. Cheap to get right, and the failure it prevents is a hook firing about
|
|
113
|
+
* the wrong tree.
|
|
114
|
+
*/
|
|
115
|
+
const paperSourceRe = (root) =>
|
|
116
|
+
new RegExp(
|
|
117
|
+
`(?:^|/)${root.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}/([A-Za-z0-9._-]+)/(?:[^/]*\\.tex|(?:.*/)?(?:paper|draft)\\.md)$`,
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* The tool invocation, with the script located rather than assumed.
|
|
122
|
+
*
|
|
123
|
+
* 🔴 `require.resolve` INSTEAD OF A HARD-CODED `node_modules/<pkg>/…` PATH. This package must
|
|
124
|
+
* not spell its own install location: a consumer may install it under an alias, and a
|
|
125
|
+
* pnpm/yarn-berry layout is not `node_modules/<name>` at all. `require.resolve` asks the
|
|
126
|
+
* resolver the same question Node itself answers, and it works for a non-JS extension —
|
|
127
|
+
* measured 2026-09-12 against a `.md` and a `.sh` in an installed copy. It runs in a `$(…)`
|
|
128
|
+
* because `run()` goes through a shell (also measured).
|
|
129
|
+
*/
|
|
130
|
+
const surface = (dir) =>
|
|
131
|
+
`bash "$(node -p "require.resolve('${CONFIG_KEY}/hooks/paper-status-gates.sh')")" --surface ${dir}`;
|
|
132
|
+
|
|
133
|
+
export default experimental_defineReact({
|
|
134
|
+
on: "PostToolUse",
|
|
135
|
+
match: tools("Edit", "Write", "MultiEdit"),
|
|
136
|
+
// 🔴 THE PATH IS ANCHORED TO THE PROJECT ROOT, and that is not decoration. vigiles runs the
|
|
137
|
+
// provider "via execSync in the hook's cwd", and the hook process's cwd is the consumer's
|
|
138
|
+
// own wiring — a string this hook cannot see. A bare `cat package.json` therefore reads from
|
|
139
|
+
// whatever directory the Bash tool last moved to, and a `cd` into a subdirectory with no
|
|
140
|
+
// manifest breaks the read.
|
|
141
|
+
//
|
|
142
|
+
// ⚠️ THE FAILURE HERE IS SILENT, which makes it more dangerous than its neighbor's.
|
|
143
|
+
// `paper-edit-guard` on PreToolUse DENIES loudly and visibly when the declaration is
|
|
144
|
+
// unreadable. This hook on PostToolUse just returns `nothing()`, i.e. simply stops firing:
|
|
145
|
+
// the `cat` chain fails → empty string → `JSON.parse("")` throws → `papersRoot` returns null
|
|
146
|
+
// → silence. And silence is exactly what a nudge's success state looks like, so a dead hook
|
|
147
|
+
// is indistinguishable from a working one.
|
|
148
|
+
needs: [provide("pkg", 'cat "${CLAUDE_PROJECT_DIR:-.}/package.json"')],
|
|
149
|
+
react: (e) => {
|
|
150
|
+
const root = papersRoot(e.ctx.pkg);
|
|
151
|
+
if (root === null) return nothing();
|
|
152
|
+
const m = paperSourceRe(root).exec(e.path.raw.replace(/^\.\//, ""));
|
|
153
|
+
if (m === null) return nothing();
|
|
154
|
+
return run(surface(m[1]));
|
|
155
|
+
},
|
|
156
|
+
});
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# paper-status-gates.sh — NOT A HOOK. This is a TOOL invoked by `paper-status-gates.hook.mjs`,
|
|
3
|
+
# which owns the event routing and decides WHICH paper (if any) an edit concerned.
|
|
4
|
+
#
|
|
5
|
+
# --surface <paper-dir> print the readiness verdict + run the pipeline checker
|
|
6
|
+
#
|
|
7
|
+
# 🔴 WHAT LEFT THIS FILE AND WHY IT MATTERS. It used to read its payload with
|
|
8
|
+
# `INPUT="${CLAUDE_TOOL_INPUT:-}"` plus a `timeout 1 cat` fallback and then grep that blob for a
|
|
9
|
+
# paper path. `$CLAUDE_TOOL_INPUT` is NOT set by the harness — the payload arrives on stdin — and
|
|
10
|
+
# that single idiom left THREE hooks silently dead for twelve days. None of it survives here:
|
|
11
|
+
# this file receives an already-validated directory NAME as an argument, and it is registered as
|
|
12
|
+
# a hook nowhere, so it cannot contract that disease again.
|
|
13
|
+
#
|
|
14
|
+
# Advisory (stderr), never blocks, always exits 0.
|
|
15
|
+
set -euo pipefail
|
|
16
|
+
|
|
17
|
+
[ "${1:-}" = "--surface" ] || {
|
|
18
|
+
echo "paper-status-gates.sh: a tool, not a hook. Use --surface <paper-dir>." >&2
|
|
19
|
+
exit 0
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
pdir_name="${2:-}"
|
|
23
|
+
[ -n "$pdir_name" ] || exit 0
|
|
24
|
+
# Defence in depth: the hook already anchored this to [A-Za-z0-9._-]+, and it is re-checked here
|
|
25
|
+
# so the file is safe to call directly too.
|
|
26
|
+
case "$pdir_name" in
|
|
27
|
+
*[!A-Za-z0-9._-]* | "" | . | ..) exit 0 ;;
|
|
28
|
+
esac
|
|
29
|
+
|
|
30
|
+
# ── WHERE THE CONSUMER'S THINGS ARE — TWO CARRIERS, READ, NOT ASSUMED ────────
|
|
31
|
+
# This file used to hard-code both of these paths, which made it a tool with exactly one possible
|
|
32
|
+
# user. Both now come from the one declaration every other carrier of this package reads:
|
|
33
|
+
#
|
|
34
|
+
# "paperlint": { "papersDir": "docs/papers", "scripts": "tools/pipeline" }
|
|
35
|
+
#
|
|
36
|
+
# (The key's name before 2.0.0, "research-paper-pipeline", is still read.)
|
|
37
|
+
#
|
|
38
|
+
# ⚠️ `node -p` RATHER THAN grep/sed ON package.json. A JSON value is not a line of text: it can
|
|
39
|
+
# be quoted, escaped, or spread across lines, and a pattern that gets it right today gets it
|
|
40
|
+
# wrong on the first reformat — silently, by producing a root that matches nothing. Node is
|
|
41
|
+
# already a hard dependency of the checker this script runs, so there is no new requirement.
|
|
42
|
+
ROOT_DIR="${CLAUDE_PROJECT_DIR:-.}"
|
|
43
|
+
read_key() { # $1 = key, $2 = default
|
|
44
|
+
node -p "((p) => p['paperlint'] ?? p['research-paper-pipeline'] ?? {})(require('$ROOT_DIR/package.json'))['$1'] ?? '$2'" \
|
|
45
|
+
2>/dev/null || echo "$2"
|
|
46
|
+
}
|
|
47
|
+
# The field used to be called "papers". Every other reader refuses the old name; this script only
|
|
48
|
+
# gives advice, so it stays silent instead of falling back to the default directory.
|
|
49
|
+
[ -z "$(read_key papers '')" ] || exit 0
|
|
50
|
+
papers_root="$(read_key papersDir papers)"
|
|
51
|
+
scripts_root="$(read_key scripts .claude/skills/paper-pipeline/scripts)"
|
|
52
|
+
|
|
53
|
+
# 🔴 AN UNUSABLE ROOT EXITS, IT DOES NOT BUILD A PATH FROM IT. `node -p` prints `undefined` for a
|
|
54
|
+
# key whose value is literally `null` (`?? ` only catches null/undefined AFTER the object lookup,
|
|
55
|
+
# and a `null` value reaches the default — but a malformed package.json makes the whole command
|
|
56
|
+
# fail and the `|| echo` hands back the default). Either way, an empty or nonsense value would
|
|
57
|
+
# produce `/<paper>/PIPELINE-STATUS.md` rooted at the filesystem, which does not exist, and this
|
|
58
|
+
# script would exit 0 looking like a paper with nothing to report. Named rather than risked.
|
|
59
|
+
case "$papers_root" in
|
|
60
|
+
"" | undefined | null) exit 0 ;;
|
|
61
|
+
esac
|
|
62
|
+
|
|
63
|
+
pdir="$papers_root/$pdir_name"
|
|
64
|
+
STATUS="$ROOT_DIR/$pdir/PIPELINE-STATUS.md"
|
|
65
|
+
[ -f "$STATUS" ] || exit 0
|
|
66
|
+
|
|
67
|
+
# 🔴 SURFACE THE VERDICT, UNPROMPTED — see the header: the checker parsed this line and never
|
|
68
|
+
# printed it, so the single fact anyone actually wanted was the one the tooling withheld.
|
|
69
|
+
VERDICT=$(grep -m1 -E '^\*\*Readiness verdict:\*\*' "$STATUS" 2>/dev/null || true)
|
|
70
|
+
if [ -n "$VERDICT" ]; then
|
|
71
|
+
echo "📊 $(echo "$VERDICT" | sed -E 's/\*\*//g' | cut -c1-200)" >&2
|
|
72
|
+
else
|
|
73
|
+
echo "📊 PIPELINE-STATUS.md has NO '**Readiness verdict:**' line — the scorecard cannot say" >&2
|
|
74
|
+
echo " whether this paper is submittable, which is the one question it exists to answer." >&2
|
|
75
|
+
fi
|
|
76
|
+
|
|
77
|
+
CHECK="$ROOT_DIR/$scripts_root/pipeline-check.mjs"
|
|
78
|
+
if [ -f "$CHECK" ]; then
|
|
79
|
+
# The real checker: stale continuous passes, gates missing their declared inputs, a study that
|
|
80
|
+
# ran before the claim was stated, submit-access inside the moderation window, a verdict that is
|
|
81
|
+
# only a percentage, unrun gates. Advisory, always exit 0.
|
|
82
|
+
node "$CHECK" "$ROOT_DIR/$pdir" >&2 || true
|
|
83
|
+
else
|
|
84
|
+
# Fallback for a checkout without the skill tree — the old crude surface.
|
|
85
|
+
gates=$(grep -E '☐' "$STATUS" 2>/dev/null | grep -iE 'study-accepted|pc-panel|harden|verify.?citations' || true)
|
|
86
|
+
if [ -n "$gates" ]; then
|
|
87
|
+
echo "⚠️ UNRUN GATES in $pdir/PIPELINE-STATUS.md (☐ — run them, don't 'anti-churn'-skip):" >&2
|
|
88
|
+
echo "$gates" | sed -E 's/\|.*//; s/^\| */ /' >&2
|
|
89
|
+
fi
|
|
90
|
+
fi
|
|
91
|
+
exit 0
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `observeAgentCli` — both halves, every case staged through an INJECTED spawner.
|
|
3
|
+
*
|
|
4
|
+
* The case that matters most is 4: a probe that discards its output. It cannot be staged at
|
|
5
|
+
* all against a real `spawnSync`, because there is no way to ask the installed binary to print
|
|
6
|
+
* nothing — which is precisely why the observation had to leave the harness that spawns it
|
|
7
|
+
* before it could be tested (issue #7).
|
|
8
|
+
*/
|
|
9
|
+
import assert from "node:assert/strict";
|
|
10
|
+
import { recordCheck } from "vigiles";
|
|
11
|
+
import { observeAgentCli, CHARACTERIZED_CLI } from "./agent-cli-version.mjs";
|
|
12
|
+
|
|
13
|
+
/** A spawner that records how it was called, so the CALL SHAPE is assertable, not just the result. */
|
|
14
|
+
const spawner = (result) => {
|
|
15
|
+
const seen = [];
|
|
16
|
+
const fn = (program, args, options) => {
|
|
17
|
+
seen.push({ program, args, options });
|
|
18
|
+
return result;
|
|
19
|
+
};
|
|
20
|
+
fn.seen = seen;
|
|
21
|
+
return fn;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const cases = [];
|
|
25
|
+
|
|
26
|
+
// ── 1. BINARY ABSENT: no throw, and `present` says so — the skip path of the caller.
|
|
27
|
+
{
|
|
28
|
+
const spawnSync = spawner({ status: 127, stdout: "", stderr: "not found" });
|
|
29
|
+
const o = observeAgentCli({ spawnSync });
|
|
30
|
+
assert.equal(
|
|
31
|
+
o.present,
|
|
32
|
+
false,
|
|
33
|
+
"a non-zero probe means the binary is not usable",
|
|
34
|
+
);
|
|
35
|
+
assert.equal(o.version, "", "and there is no version to report");
|
|
36
|
+
assert.match(
|
|
37
|
+
o.note,
|
|
38
|
+
/not installed/,
|
|
39
|
+
"the note must say WHY there is nothing to observe",
|
|
40
|
+
);
|
|
41
|
+
cases.push("CLI absent → present:false, no throw, note explains");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// ── 2. 🔴 DRIFT IS NAMED, NOT INFERRED. The measured case from issue #7: the machine carries
|
|
45
|
+
// 2.1.273 while the assertions were written against 2.1.227.
|
|
46
|
+
{
|
|
47
|
+
const spawnSync = spawner({
|
|
48
|
+
status: 0,
|
|
49
|
+
stdout: "2.1.273 (Claude Code)\n",
|
|
50
|
+
stderr: "",
|
|
51
|
+
});
|
|
52
|
+
const o = observeAgentCli({ spawnSync });
|
|
53
|
+
assert.equal(o.present, true);
|
|
54
|
+
assert.equal(
|
|
55
|
+
o.version,
|
|
56
|
+
"2.1.273",
|
|
57
|
+
"the version must be PARSED out of the line, not left raw",
|
|
58
|
+
);
|
|
59
|
+
assert.equal(
|
|
60
|
+
o.raw,
|
|
61
|
+
"2.1.273 (Claude Code)",
|
|
62
|
+
"and the full line kept for the report",
|
|
63
|
+
);
|
|
64
|
+
assert.equal(
|
|
65
|
+
o.characterized,
|
|
66
|
+
CHARACTERIZED_CLI,
|
|
67
|
+
"the characterized number rides along as a value",
|
|
68
|
+
);
|
|
69
|
+
assert.equal(
|
|
70
|
+
o.drifted,
|
|
71
|
+
true,
|
|
72
|
+
"2.1.273 ≠ 2.1.227 is drift and must be computed, not eyeballed",
|
|
73
|
+
);
|
|
74
|
+
assert.ok(
|
|
75
|
+
o.note.includes("2.1.273") && o.note.includes(CHARACTERIZED_CLI),
|
|
76
|
+
`the note must name BOTH numbers so a reader sees them diverge; got: ${o.note}`,
|
|
77
|
+
);
|
|
78
|
+
cases.push(
|
|
79
|
+
"drifted CLI → version parsed, drifted:true, note names BOTH numbers",
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// ── 3. NO DRIFT: the same machinery must stay quiet when the numbers agree, or `drifted`
|
|
84
|
+
// would be a constant rather than a comparison.
|
|
85
|
+
{
|
|
86
|
+
const spawnSync = spawner({
|
|
87
|
+
status: 0,
|
|
88
|
+
stdout: `${CHARACTERIZED_CLI} (Claude Code)\n`,
|
|
89
|
+
stderr: "",
|
|
90
|
+
});
|
|
91
|
+
const o = observeAgentCli({ spawnSync });
|
|
92
|
+
assert.equal(o.drifted, false, "matching versions are not drift");
|
|
93
|
+
assert.equal(o.version, CHARACTERIZED_CLI);
|
|
94
|
+
cases.push("CLI at the characterized version → drifted:false");
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// ── 4. 🔴 THE DEFECT ITSELF: exit 0, nothing on stdout — what `stdio: "ignore"` produced.
|
|
98
|
+
// Reporting `characterized against ""` would be issue #7 wearing new clothes, so this
|
|
99
|
+
// must THROW rather than return an empty observation.
|
|
100
|
+
{
|
|
101
|
+
const spawnSync = spawner({ status: 0, stdout: null, stderr: "" });
|
|
102
|
+
assert.throws(
|
|
103
|
+
() => observeAgentCli({ spawnSync }),
|
|
104
|
+
/is not an observation/,
|
|
105
|
+
"exit 0 with no version on stdout must fail loudly, not be reported as a version",
|
|
106
|
+
);
|
|
107
|
+
cases.push(
|
|
108
|
+
'exit 0 + empty stdout (the `stdio: "ignore"` shape) → throws, does not report a blank',
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// ── 5. THE CALL SHAPE, asserted directly. Case 4 catches a discarding probe only because the
|
|
113
|
+
// double returns nothing; a real `stdio: "ignore"` would ALSO have to be caught if some
|
|
114
|
+
// other path happened to fill stdout. So the options handed to the spawner are checked.
|
|
115
|
+
{
|
|
116
|
+
const spawnSync = spawner({
|
|
117
|
+
status: 0,
|
|
118
|
+
stdout: "2.1.273 (Claude Code)\n",
|
|
119
|
+
stderr: "",
|
|
120
|
+
});
|
|
121
|
+
observeAgentCli({ spawnSync });
|
|
122
|
+
const { args, options } = spawnSync.seen[0];
|
|
123
|
+
assert.deepEqual(
|
|
124
|
+
args,
|
|
125
|
+
["--version"],
|
|
126
|
+
"the observation must ask for the version",
|
|
127
|
+
);
|
|
128
|
+
assert.equal(
|
|
129
|
+
options.encoding,
|
|
130
|
+
"utf8",
|
|
131
|
+
"without an encoding the output is a Buffer nobody reads",
|
|
132
|
+
);
|
|
133
|
+
assert.notEqual(
|
|
134
|
+
options.stdio,
|
|
135
|
+
"ignore",
|
|
136
|
+
'`stdio: "ignore"` is the discarded-output defect itself',
|
|
137
|
+
);
|
|
138
|
+
cases.push("the probe is spawned with encoding:utf8 and never stdio:ignore");
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// ── 6. AND THE REAL BINARY, when this machine has one. Cases 1-5 are about the module; this
|
|
142
|
+
// one is about reality — an observer that only ever meets a double proves nothing about
|
|
143
|
+
// the output format of the tool it exists to read.
|
|
144
|
+
{
|
|
145
|
+
const { spawnSync } = await import("node:child_process");
|
|
146
|
+
const o = observeAgentCli({ spawnSync });
|
|
147
|
+
if (o.present) {
|
|
148
|
+
assert.match(
|
|
149
|
+
o.version,
|
|
150
|
+
/^\d+\.\d+\.\d+$/,
|
|
151
|
+
`the real CLI's version must parse; raw was ${JSON.stringify(o.raw)}`,
|
|
152
|
+
);
|
|
153
|
+
cases.push(
|
|
154
|
+
`real \`claude\` binary present → observed ${o.version} (characterized ${o.characterized}${o.drifted ? ", DRIFTED" : ""})`,
|
|
155
|
+
);
|
|
156
|
+
} else {
|
|
157
|
+
cases.push(
|
|
158
|
+
"real `claude` binary absent on this machine → absent path exercised for real",
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
recordCheck(cases.length);
|
|
164
|
+
console.log(`observeAgentCli: ${cases.length} cases:`);
|
|
165
|
+
for (const c of cases) console.log(` ok ${c}`);
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OBSERVE the `claude` CLI version instead of merely probing for the binary.
|
|
3
|
+
*
|
|
4
|
+
* ── THE DEFECT THIS EXISTS FOR (issue #7, measured) ─────────────────────────────
|
|
5
|
+
* `plan-paper-timeline.effects.harness.mjs` is a CHARACTERIZATION test: its job is to go red
|
|
6
|
+
* when the upstream CLI changes under it. It named the version it characterized — 2.1.227 —
|
|
7
|
+
* in prose, and then threw away the version it actually saw:
|
|
8
|
+
*
|
|
9
|
+
* spawnSync("claude", ["--version"], { stdio: "ignore" }) // ← only `status` was read
|
|
10
|
+
*
|
|
11
|
+
* `stdio: "ignore"` discards the output, so `claude --version` ran as a PRESENCE probe. The
|
|
12
|
+
* string it printed was never read, never reported and never compared with the prose. Measured
|
|
13
|
+
* on this machine: the binary is **2.1.273**, the docblock says **2.1.227** — forty-six patch
|
|
14
|
+
* releases of drift, invisible to a reader of a green log.
|
|
15
|
+
*
|
|
16
|
+
* 🔴 WHY NOT PIN THE VERSION. A pin defeats the test: it is SUPPOSED to see upstream move. The
|
|
17
|
+
* repair is not to stop the drift but to stop the SILENCE about it — observe what is already
|
|
18
|
+
* spawned, carry the characterized number as a VALUE beside it, and let a reader see the two
|
|
19
|
+
* diverge instead of inferring it. CI installs `@anthropic-ai/claude-code` unpinned on purpose.
|
|
20
|
+
*
|
|
21
|
+
* ── WHY A MODULE AND NOT THREE LINES IN THE HARNESS ─────────────────────────────
|
|
22
|
+
* The three lines the issue suggests are correct and untestable where they were proposed. The
|
|
23
|
+
* harness that would exercise them spawns the real CLI, so its assertions about the observation
|
|
24
|
+
* depend on which version the machine happens to carry — and the one case that matters most,
|
|
25
|
+
* "the probe discards its output", cannot be staged at all when the spawn is hard-wired. With
|
|
26
|
+
* the spawner INJECTED, every case becomes deterministic: absent binary, drifted version,
|
|
27
|
+
* matching version, and the discarding probe itself.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* The version the effects harness's characterization was written against.
|
|
32
|
+
*
|
|
33
|
+
* 🔴 A VALUE, NOT A SENTENCE, and that is the whole point of this constant. The number lived in
|
|
34
|
+
* a docblock, where nothing could compare it with anything. Here it is compared on every run.
|
|
35
|
+
* Moving it means deliberately re-characterizing — which is the edit that should be conscious.
|
|
36
|
+
*/
|
|
37
|
+
export const CHARACTERIZED_CLI = "2.1.227";
|
|
38
|
+
|
|
39
|
+
/** What `claude --version` prints: `2.1.273 (Claude Code)`. */
|
|
40
|
+
const VERSION_IN = /\b(\d+\.\d+\.\d+)\b/;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* @typedef {object} CliObservation
|
|
44
|
+
* @property {boolean} present the binary answered `--version` with status 0
|
|
45
|
+
* @property {string} raw the full line it printed, trimmed
|
|
46
|
+
* @property {string} version the dotted version parsed out of it
|
|
47
|
+
* @property {string} characterized the version the assertions were written against
|
|
48
|
+
* @property {boolean} drifted observed and characterized differ
|
|
49
|
+
* @property {string} note one line naming BOTH numbers, for the report and for failures
|
|
50
|
+
*/
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* @param {object} deps
|
|
54
|
+
* @param {Function} deps.spawnSync injected, so every case below is stageable
|
|
55
|
+
* @param {string} [deps.program]
|
|
56
|
+
* @param {string} [deps.characterized]
|
|
57
|
+
* @returns {CliObservation}
|
|
58
|
+
*/
|
|
59
|
+
export function observeAgentCli({
|
|
60
|
+
spawnSync,
|
|
61
|
+
program = "claude",
|
|
62
|
+
characterized = CHARACTERIZED_CLI,
|
|
63
|
+
}) {
|
|
64
|
+
// 🔴 `encoding: "utf8"` IS THE FIX. With `stdio: "ignore"` — the shape this replaces — the
|
|
65
|
+
// child's output goes nowhere and `probe.stdout` comes back null, so everything downstream
|
|
66
|
+
// would be describing a string that was never read.
|
|
67
|
+
const probe = spawnSync(program, ["--version"], { encoding: "utf8" });
|
|
68
|
+
|
|
69
|
+
if (probe.status !== 0)
|
|
70
|
+
return {
|
|
71
|
+
present: false,
|
|
72
|
+
raw: "",
|
|
73
|
+
version: "",
|
|
74
|
+
characterized,
|
|
75
|
+
drifted: false,
|
|
76
|
+
note: `the \`${program}\` CLI is not installed, so no run can be observed`,
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const raw = (probe.stdout ?? "").trim();
|
|
80
|
+
const version = VERSION_IN.exec(raw)?.[1] ?? "";
|
|
81
|
+
|
|
82
|
+
// ⚠️ EXIT 0 AND NOTHING ON STDOUT IS NOT A CLEAN OBSERVATION — it is exactly what the discarded
|
|
83
|
+
// probe looked like, and reporting `characterized against ""` would be the same lie in a new
|
|
84
|
+
// place. This is rule 4 of CLAUDE.md applied to the check that enforces it.
|
|
85
|
+
if (version === "")
|
|
86
|
+
throw new Error(
|
|
87
|
+
`\`${program} --version\` exited 0 but printed no version on stdout (got ${JSON.stringify(raw)}` +
|
|
88
|
+
`${probe.stderr ? `, stderr ${JSON.stringify(String(probe.stderr).trim().slice(0, 200))}` : ""}). ` +
|
|
89
|
+
`A presence probe is not an observation: this is the \`stdio: "ignore"\` shape that issue #7 ` +
|
|
90
|
+
`is about, and a version nobody read must not be reported as one.`,
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
const drifted = version !== characterized;
|
|
94
|
+
return {
|
|
95
|
+
present: true,
|
|
96
|
+
raw,
|
|
97
|
+
version,
|
|
98
|
+
characterized,
|
|
99
|
+
drifted,
|
|
100
|
+
note: drifted
|
|
101
|
+
? `observed \`${program}\` ${version} — the assertions here were characterized against ` +
|
|
102
|
+
`${characterized}. They still hold, which is itself the result: a characterization test that ` +
|
|
103
|
+
`silently still holds is indistinguishable from one nobody re-checked.`
|
|
104
|
+
: `observed \`${program}\` ${version} — the version these assertions were characterized against.`,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Battery for `observeAgentCli`. Five mutations, five different assertions.
|
|
3
|
+
*
|
|
4
|
+
* 🔴 The first one is the issue #7 defect itself, restored verbatim: bring back
|
|
5
|
+
* `stdio: "ignore"`. It is the whole point of this battery. Before the observation was pulled
|
|
6
|
+
* out into its own module, there was NOWHERE to place this mutation: the spawn was baked into
|
|
7
|
+
* the harness, which brings up the real CLI, and "ask the binary to print nothing" is not
|
|
8
|
+
* something you can do. A green battery against a baked-in spawn would have been a finding
|
|
9
|
+
* about the test, not about the defense.
|
|
10
|
+
*/
|
|
11
|
+
import { fileURLToPath } from "node:url";
|
|
12
|
+
import { dirname, join, resolve } from "node:path";
|
|
13
|
+
import { runMutations } from "./mutation-driver.mjs";
|
|
14
|
+
|
|
15
|
+
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
16
|
+
const ROOT = resolve(HERE, "..");
|
|
17
|
+
const MOD = join(HERE, "agent-cli-version.mjs");
|
|
18
|
+
const HARNESS = join(HERE, "agent-cli-version.harness.mjs");
|
|
19
|
+
|
|
20
|
+
process.exit(
|
|
21
|
+
runMutations({
|
|
22
|
+
root: ROOT,
|
|
23
|
+
runner: "node",
|
|
24
|
+
cases: [
|
|
25
|
+
{
|
|
26
|
+
name: '🔴 revert to `stdio: "ignore"` — the issue #7 defect verbatim',
|
|
27
|
+
harness: HARNESS,
|
|
28
|
+
// ⚠️ The assertion this trips is NOT the one I was aiming for, and this was corrected
|
|
29
|
+
// BY RUNNING IT, not from memory. The double hands back stdout regardless of the
|
|
30
|
+
// options, so the observation succeeds, and the first thing to go red is the
|
|
31
|
+
// `encoding` assertion. The `stdio` assertion itself is hit by the next case.
|
|
32
|
+
expect: "without an encoding the output is a Buffer nobody reads",
|
|
33
|
+
disables:
|
|
34
|
+
"the observation itself — the version is printed into the void again, and the tier characterizes nothing",
|
|
35
|
+
edits: [
|
|
36
|
+
[
|
|
37
|
+
MOD,
|
|
38
|
+
'spawnSync(program, ["--version"], { encoding: "utf8" })',
|
|
39
|
+
'spawnSync(program, ["--version"], { stdio: "ignore" })',
|
|
40
|
+
],
|
|
41
|
+
],
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
name: '`stdio: "ignore"` added ALONGSIDE encoding — output still goes nowhere',
|
|
45
|
+
harness: HARNESS,
|
|
46
|
+
expect: "is the discarded-output defect itself",
|
|
47
|
+
disables:
|
|
48
|
+
"the second call-shape assertion: `encoding` is present, but the stream is still killed",
|
|
49
|
+
edits: [
|
|
50
|
+
[
|
|
51
|
+
MOD,
|
|
52
|
+
'spawnSync(program, ["--version"], { encoding: "utf8" })',
|
|
53
|
+
'spawnSync(program, ["--version"], { encoding: "utf8", stdio: "ignore" })',
|
|
54
|
+
],
|
|
55
|
+
],
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
name: "empty stdout at exit code 0 stops being a failure",
|
|
59
|
+
harness: HARNESS,
|
|
60
|
+
expect: "must fail loudly, not be reported as a version",
|
|
61
|
+
disables:
|
|
62
|
+
"rule 4 — `exit 0` with empty output reads as a clean measurement again",
|
|
63
|
+
edits: [[MOD, 'if (version === "")', "if (false)"]],
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
name: "drift is computed as a constant, not a comparison",
|
|
67
|
+
harness: HARNESS,
|
|
68
|
+
expect: "must be computed, not eyeballed",
|
|
69
|
+
disables:
|
|
70
|
+
"the comparison of observed against characterized — the divergence becomes invisible again",
|
|
71
|
+
edits: [
|
|
72
|
+
[
|
|
73
|
+
MOD,
|
|
74
|
+
"const drifted = version !== characterized;",
|
|
75
|
+
"const drifted = false;",
|
|
76
|
+
],
|
|
77
|
+
],
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
name: "only one of the two numbers survives into the message",
|
|
81
|
+
harness: HARNESS,
|
|
82
|
+
expect: "must name BOTH numbers",
|
|
83
|
+
disables:
|
|
84
|
+
"showing the divergence — the reader is back to having to remember the second number",
|
|
85
|
+
edits: [
|
|
86
|
+
[
|
|
87
|
+
MOD,
|
|
88
|
+
"`observed \\`${program}\\` ${version} — the assertions here were characterized against ` +\n `${characterized}.",
|
|
89
|
+
"`observed \\`${program}\\` ${version} — characterized elsewhere.",
|
|
90
|
+
],
|
|
91
|
+
],
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
name: "the version is not parsed out of the string, taken whole instead",
|
|
95
|
+
harness: HARNESS,
|
|
96
|
+
expect: "must be PARSED out of the line, not left raw",
|
|
97
|
+
disables:
|
|
98
|
+
"parsing the version string — `2.1.273 (Claude Code)` stops being comparable to `2.1.227`",
|
|
99
|
+
edits: [
|
|
100
|
+
[
|
|
101
|
+
MOD,
|
|
102
|
+
'const version = VERSION_IN.exec(raw)?.[1] ?? "";',
|
|
103
|
+
"const version = raw;",
|
|
104
|
+
],
|
|
105
|
+
],
|
|
106
|
+
},
|
|
107
|
+
],
|
|
108
|
+
}),
|
|
109
|
+
);
|