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,457 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* paper-edit-guard — a PreToolUse gate: a paper SOURCE must not be written from Bash, because a
|
|
3
|
+
* Bash write skips every PostToolUse check that hangs on Edit/Write (readability thresholds, the
|
|
4
|
+
* pipeline nudge, the unrun-gate surface).
|
|
5
|
+
*
|
|
6
|
+
* ── WHY THIS SHIPS AS `.mjs` AND NOT AS A `.hook.ts` SPEC ───────────────────
|
|
7
|
+
* 🔴 MEASURED, both halves, four locations (2026-09-11). The same hook, byte for byte, placed
|
|
8
|
+
* inside an installed package:
|
|
9
|
+
*
|
|
10
|
+
* <pkg>/hooks/probe.hook.mjs deny-input RC=2 + reason allow-input RC=0, silent ✅
|
|
11
|
+
* <pkg>/hooks/probe.hook.ts deny-input RC=2 "cannot be loaded"
|
|
12
|
+
* allow-input RC=2 "cannot be loaded" 🔴 DEAD
|
|
13
|
+
*
|
|
14
|
+
* A `.ts` reached through `node_modules` does not load, and a PreToolUse hook that does not load
|
|
15
|
+
* refuses EVERY Bash command — including the one that would fix it. So the shipped artifact is
|
|
16
|
+
* `.mjs`. The CLI transpiles TypeScript hooks with its own host; why that host does not serve a
|
|
17
|
+
* path inside `node_modules` was not established, and does not need to be for this decision.
|
|
18
|
+
*
|
|
19
|
+
* ── WHY NOT A THIN SPEC IN THE CONSUMER THAT IMPORTS THIS LOGIC ─────────────
|
|
20
|
+
* 🔴 ALSO MEASURED (2026-09-12), and it is the form that looks obviously right and is not. A
|
|
21
|
+
* consumer-side `.hook.ts` that pulls this logic in from the installed package:
|
|
22
|
+
*
|
|
23
|
+
* runtime deny-input RC=2, allow-input RC=0 ✅ it runs
|
|
24
|
+
* `vigiles compile` RC=1 — "hook program uses capabilities outside `vigiles/hook`" 🔴
|
|
25
|
+
*
|
|
26
|
+
* `checkHookImports` allows exactly one specifier, and that is not a bug to route around: the
|
|
27
|
+
* import list IS the capability surface. The consequence is what kills the form: compile is also
|
|
28
|
+
* what writes the tamper-evident stamp, so a hook it refuses can never be re-stamped, and the
|
|
29
|
+
* runtime FAILS CLOSED on a stamp that no longer matches its source —
|
|
30
|
+
*
|
|
31
|
+
* vigiles: hook … does not match its compiled stamp (tampered).
|
|
32
|
+
* … the way out is a FILE WRITE, not a command — this refusal blocks the recompile too.
|
|
33
|
+
*
|
|
34
|
+
* — i.e. editing that thin file wedges the repository, and the only steady state is running
|
|
35
|
+
* permanently unstamped. Shipping the whole program instead keeps the stamp question from
|
|
36
|
+
* arising (no sidecar ⇒ no check) and pins the source by lockfile integrity instead, which is
|
|
37
|
+
* stronger than a local stamp: the consumer cannot hand-edit an installed tree without the next
|
|
38
|
+
* install reverting it.
|
|
39
|
+
*
|
|
40
|
+
* The capability surface is therefore asserted HERE, on the artifact that actually executes:
|
|
41
|
+
* `hooks.harness.mjs` runs `checkHookImports` over every shipped `.hook.mjs`. That is a stricter
|
|
42
|
+
* check than compiling a twin `.ts` source would be, because a twin can drift from its build and
|
|
43
|
+
* this cannot — there is only one file.
|
|
44
|
+
*
|
|
45
|
+
* ── WHERE THE CONSUMER'S PAPERS ARE ─────────────────────────────────────────
|
|
46
|
+
* One declaration, `"paperlint": { "papersDir": "…" }`, read the way a hook is able to
|
|
47
|
+
* read anything at all: `needs: [provide("pkg", "cat \"${CLAUDE_PROJECT_DIR:-.}/package.json\"")]`.
|
|
48
|
+
* The alternatives were measured and killed in `eslint-rules/papers.mjs` — an env var cannot be
|
|
49
|
+
* read from a hook at all (no imports), and a symlinked root makes ESLint report zero files.
|
|
50
|
+
*
|
|
51
|
+
* 🔴 THE PATH IS ANCHORED, AND THAT IS NOT DECORATION. vigiles runs a provider "via execSync in
|
|
52
|
+
* the hook's cwd", so a bare `cat package.json` resolves against whatever directory the hook
|
|
53
|
+
* PROCESS happens to have — which is set by the consumer's wiring, a string this hook cannot
|
|
54
|
+
* see and does not control. With the wiring this package documents (`cd "$CLAUDE_PROJECT_DIR"
|
|
55
|
+
* && node …`) the bare read is correct; without it, any tool cwd outside the project root makes
|
|
56
|
+
* the read fail, and by the paragraph below that failure DENIES EVERY BASH COMMAND until
|
|
57
|
+
* something external resets the cwd — `pwd` included, so the wedge cannot be escaped from the
|
|
58
|
+
* shell. Reported 2026-09-16 by a session that hit exactly that after a `cd` into a subtree.
|
|
59
|
+
*
|
|
60
|
+
* ⚠️ The wiring is not hypothetical to lose: `vigiles compile` has REGENERATED it without the
|
|
61
|
+
* `cd` prefix once already in this corpus. A gate whose blast radius is "all of Bash" must not
|
|
62
|
+
* rest on a prefix a code generator can drop. `${CLAUDE_PROJECT_DIR:-.}` keeps the old behaviour
|
|
63
|
+
* when the variable is absent, so this is strictly wider than what it replaces.
|
|
64
|
+
*
|
|
65
|
+
* 🔴 AN UNREADABLE DECLARATION DENIES, IT DOES NOT DEFAULT. Measured 2026-09-12: when the
|
|
66
|
+
* provider's command fails — no `package.json`, or one with conflict markers in it — `e.ctx.pkg`
|
|
67
|
+
* arrives as the EMPTY STRING and the runtime says nothing. A `try { JSON.parse } catch { use the
|
|
68
|
+
* default }` therefore turns a broken declaration into a silently permissive gate: the prefix
|
|
69
|
+
* quietly becomes `papers`, nothing under the real root matches, and every Bash write to a paper
|
|
70
|
+
* is allowed by a gate that still looks installed and green. So parse failure is a `deny` that
|
|
71
|
+
* names the cure.
|
|
72
|
+
*
|
|
73
|
+
* ⚠️ That deny does refuse every Bash command while the declaration is broken, which is a known
|
|
74
|
+
* and accepted cost rather than an oversight: the recovery is a FILE WRITE (Edit/Write bypass
|
|
75
|
+
* PreToolUse entirely), and the message below says so. A gate that fails open here would be
|
|
76
|
+
* indistinguishable from a gate that is working.
|
|
77
|
+
*
|
|
78
|
+
* ── HONEST SCOPE — three holes, left open on purpose ────────────────────────
|
|
79
|
+
* 1. INTERPRETER INDIRECTION. The gate reads the Bash command's AST; a path that never appears
|
|
80
|
+
* in the command is invisible. A heredoc that writes a script naming the paper, then running
|
|
81
|
+
* it, passes. Closing it means deciding what an arbitrary program will write, which is
|
|
82
|
+
* undecidable, and every cheap approximation (deny `python3` with a file argument, scan
|
|
83
|
+
* heredoc bodies) buys a little coverage for a lot of false denials. A guard that denies
|
|
84
|
+
* legitimate commands gets muted, and a muted guard is worth less than none.
|
|
85
|
+
* 2. A WRAPPER THAT CHANGES DIRECTORY. Measured: `sed -i s/a/b/ <root>/x/paper.tex` → blocked;
|
|
86
|
+
* `env -C <root-parent> sed -i s/a/b/ …` → allowed. Both scans compare against a prefix
|
|
87
|
+
* starting at the repository root and neither knows the command's working directory. Closing
|
|
88
|
+
* it means a cwd model that `cd`, `env -C`, `pushd`, `git -C` and a subshell can each move —
|
|
89
|
+
* that belongs in the vocabulary upstream, not in a guard reimplementing shell semantics.
|
|
90
|
+
* 3. DELIVERY, from the product's own docs: a PreToolUse hook does not fire for a subagent's
|
|
91
|
+
* tool calls. So this is a strong default, never an unbypassable wall — and hole 3 is wider
|
|
92
|
+
* than 1 and 2 together, which is why patching them from inside the hook is not worth it.
|
|
93
|
+
*
|
|
94
|
+
* This gate is a SPEED BUMP against the idiom people reach for. The boundary is the convention
|
|
95
|
+
* in the consumer's own instructions, and that is held by prose on purpose.
|
|
96
|
+
*/
|
|
97
|
+
// 🔴 `tools`, PLURAL. The singular alias existed before vigiles 27.x and was REMOVED from
|
|
98
|
+
// `vigiles/hook` in it. The failure is not local: a missing import means the hook does not load,
|
|
99
|
+
// and a PreToolUse hook that does not load refuses EVERY Bash command — including the one that
|
|
100
|
+
// would fix it. Observed in the first consumer on 2026-09-10 after a 25.1.0 → 27.1.4 bump; the
|
|
101
|
+
// repository had to be recovered with a file write. Pin this package's vigiles to the consumer's.
|
|
102
|
+
import {
|
|
103
|
+
experimental_defineHook,
|
|
104
|
+
tools,
|
|
105
|
+
provide,
|
|
106
|
+
allow,
|
|
107
|
+
deny,
|
|
108
|
+
} from "vigiles/hook";
|
|
109
|
+
|
|
110
|
+
/** The key every carrier of this package reads its consumer-specific settings from. */
|
|
111
|
+
export const CONFIG_KEY = "paperlint";
|
|
112
|
+
/** The key's name before 2.0.0 — still read, a copy of `lib/paper-config.mjs`. */
|
|
113
|
+
export const LEGACY_CONFIG_KEY = "research-paper-pipeline";
|
|
114
|
+
/** The default. A consumer that declares nothing is assumed to keep papers in `papers/`. */
|
|
115
|
+
export const DEFAULT_PAPERS_ROOT = "papers";
|
|
116
|
+
/**
|
|
117
|
+
* The field under CONFIG_KEY that names the papers directory, and its old name. A copy of the
|
|
118
|
+
* constants in `lib/paper-config.mjs` (a hook may import nothing but `vigiles/hook`);
|
|
119
|
+
* `lib/paper-config.harness.mjs` checks that the copies match.
|
|
120
|
+
*/
|
|
121
|
+
export const PAPERS_DIR_FIELD = "papersDir";
|
|
122
|
+
export const OLD_PAPERS_DIR_FIELD = "papers";
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* The declared papers root, or a `deny` explaining why there is not one.
|
|
126
|
+
*
|
|
127
|
+
* Returns a string on success and a `Decision` on failure, so the caller cannot accidentally
|
|
128
|
+
* continue with a bad root: there is no value to continue WITH.
|
|
129
|
+
*
|
|
130
|
+
* 🔴 `declared === undefined`, NOT `declared ?? DEFAULT` — the two differ on exactly one input,
|
|
131
|
+
* `"papersDir": null`, and that difference is the point: `??` reads an explicit `null` as "nothing
|
|
132
|
+
* was declared" and silently substitutes the default, i.e. treats a typed keystroke as an
|
|
133
|
+
* absence. The same distinction is made by every other carrier in this package.
|
|
134
|
+
*/
|
|
135
|
+
/**
|
|
136
|
+
* EXPORTED ON PURPOSE — this is the only way to cross-check without a second copy of the logic.
|
|
137
|
+
* `paperlint doctor` has to say which directory THIS hook will guard, not what a retelling of it
|
|
138
|
+
* would guard. The compiled hook is forbidden to IMPORT anything but `vigiles/hook`
|
|
139
|
+
* (that's what `checkHookImports` enforces), so a shared module is impossible — but that ban
|
|
140
|
+
* doesn't restrict exporting outward, and the reverse direction, CLI → hook, is free.
|
|
141
|
+
* Returns the root string, or a rejection object: the caller tells them apart by `typeof`.
|
|
142
|
+
*/
|
|
143
|
+
export const papersRoot = (rawPkg) => {
|
|
144
|
+
let pkg;
|
|
145
|
+
try {
|
|
146
|
+
pkg = JSON.parse(rawPkg);
|
|
147
|
+
} catch {
|
|
148
|
+
return deny(
|
|
149
|
+
`${CONFIG_KEY}: this gate could not read the consumer's package.json, so it does not know ` +
|
|
150
|
+
`where papers live and is refusing rather than guessing.\n` +
|
|
151
|
+
`The declaration it needs is:\n` +
|
|
152
|
+
` "${CONFIG_KEY}": { "${PAPERS_DIR_FIELD}": "path/to/papers" }\n` +
|
|
153
|
+
`(no key at all is fine too — the default is "${DEFAULT_PAPERS_ROOT}").\n` +
|
|
154
|
+
`If package.json is mid-merge or otherwise broken, fix it with Edit or Write: file ` +
|
|
155
|
+
`tools do not pass through this gate, so that path out is always open.\n` +
|
|
156
|
+
`This refuses rather than falling back on purpose. A gate whose prefix quietly became ` +
|
|
157
|
+
`the default would allow every Bash write to the real papers root while still looking ` +
|
|
158
|
+
`installed and green.`,
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
// The settings sit under CONFIG_KEY, or under its old name. Both, with different contents, is
|
|
162
|
+
// refused: the gate cannot know which one the author means.
|
|
163
|
+
const current = pkg?.[CONFIG_KEY];
|
|
164
|
+
const legacy = pkg?.[LEGACY_CONFIG_KEY];
|
|
165
|
+
if (
|
|
166
|
+
current !== undefined &&
|
|
167
|
+
legacy !== undefined &&
|
|
168
|
+
JSON.stringify(current) !== JSON.stringify(legacy)
|
|
169
|
+
)
|
|
170
|
+
return deny(
|
|
171
|
+
`package.json has both "${CONFIG_KEY}" and "${LEGACY_CONFIG_KEY}", and they differ. ` +
|
|
172
|
+
`Keep "${CONFIG_KEY}" and delete "${LEGACY_CONFIG_KEY}" (its old name). ` +
|
|
173
|
+
`Fix it with Edit or Write: file tools do not pass through this gate.`,
|
|
174
|
+
);
|
|
175
|
+
const settings = current ?? legacy;
|
|
176
|
+
// The old field name is refused, not read as a fallback: this gate would otherwise guard the
|
|
177
|
+
// default directory while the consumer believes it guards the one they declared.
|
|
178
|
+
if (settings && Object.hasOwn(settings, OLD_PAPERS_DIR_FIELD))
|
|
179
|
+
return deny(
|
|
180
|
+
`"${OLD_PAPERS_DIR_FIELD}" was renamed to "${PAPERS_DIR_FIELD}" in package.json → "${CONFIG_KEY}"`,
|
|
181
|
+
);
|
|
182
|
+
const declared = settings?.[PAPERS_DIR_FIELD];
|
|
183
|
+
const root = declared === undefined ? DEFAULT_PAPERS_ROOT : declared;
|
|
184
|
+
if (typeof root !== "string" || root.length === 0)
|
|
185
|
+
return deny(
|
|
186
|
+
`${CONFIG_KEY}: "${PAPERS_DIR_FIELD}" must be a non-empty string, got ${JSON.stringify(root)}.\n` +
|
|
187
|
+
`Fix it in the consumer's package.json. This gate refuses while the value is unusable: ` +
|
|
188
|
+
`an empty prefix matches nothing, so the gate would pass every paper write in silence.`,
|
|
189
|
+
);
|
|
190
|
+
// One trailing slash at most, whatever the consumer typed: `"papersDir": "docs/papers/"` reaches
|
|
191
|
+
// here with its own, and `prefix + "/"` would become `docs/papers//`, which matches NOTHING.
|
|
192
|
+
// That exact defect (a trailing slash in the guarded prefix) made an earlier version of this
|
|
193
|
+
// guard a no-op while its comment claimed otherwise.
|
|
194
|
+
return root.replace(/\/+$/, "");
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Redirection targets (`> f`, `>> f`, `1> f`, `&> f`), which `touches` cannot see.
|
|
199
|
+
*
|
|
200
|
+
* MEASURED, not assumed: `leafCommandsNormalized("echo x > p/x/paper.md")` returns
|
|
201
|
+
* `[{head:"echo", argv:["echo","x"], args:["x"], …}]` — the redirection and its target are
|
|
202
|
+
* DROPPED by the parser, so they appear in no field of any leaf. The closed vocabulary
|
|
203
|
+
* (`runs` / `touches` / `pipesToShell` / `isSideEffecting`) has no way to ask what a command
|
|
204
|
+
* WRITES. Filed upstream as a `writesTo(prefixes)` primitive; when that lands, this helper
|
|
205
|
+
* deletes and the assertions stay.
|
|
206
|
+
*
|
|
207
|
+
* 🔴 THE SCAN IS QUOTE-AWARE, and the version before it was not. It stripped quoted spans
|
|
208
|
+
* wholesale, so a quoted redirection target vanished with them. MEASURED on this guard:
|
|
209
|
+
*
|
|
210
|
+
* echo x > <root>/x/paper.md → blocked
|
|
211
|
+
* echo x > "<root>/x/paper.md" → ALLOWED
|
|
212
|
+
* echo x > '<root>/x/paper.md' → ALLOWED
|
|
213
|
+
*
|
|
214
|
+
* Stripping was not merely careless: a `>` INSIDE quotes is not a redirection at all, so the
|
|
215
|
+
* guard has to know about quoting or it fires on any command that merely QUOTES a paper write —
|
|
216
|
+
* which it did, three times in a row, on the very commands testing it. Both facts are true at
|
|
217
|
+
* once and a regex cannot hold them both: the operator must be found OUTSIDE quotes while the
|
|
218
|
+
* target must be read THROUGH them. Hence a character scan that tracks quote state.
|
|
219
|
+
*/
|
|
220
|
+
const redirectTargets = (raw) => {
|
|
221
|
+
const src = raw.replace(/<<-?\s*'?(\w+)'?[\s\S]*?^\1$/gm, "<<HEREDOC");
|
|
222
|
+
const out = [];
|
|
223
|
+
let i = 0;
|
|
224
|
+
let quote = null;
|
|
225
|
+
while (i < src.length) {
|
|
226
|
+
const c = src[i];
|
|
227
|
+
if (quote) {
|
|
228
|
+
if (c === "\\" && quote === '"') i += 1;
|
|
229
|
+
else if (c === quote) quote = null;
|
|
230
|
+
i += 1;
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
if (c === "'" || c === '"') {
|
|
234
|
+
quote = c;
|
|
235
|
+
i += 1;
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
if (c === "\\") {
|
|
239
|
+
i += 2;
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
if (c !== ">") {
|
|
243
|
+
i += 1;
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
i += 1;
|
|
247
|
+
if (src[i] === ">") i += 1;
|
|
248
|
+
while (i < src.length && (src[i] === " " || src[i] === "\t")) i += 1;
|
|
249
|
+
// Read the target, unquoting as we go. `2>&1` and `2>/dev/null` fall out for free: `&` is a
|
|
250
|
+
// delimiter, so the first yields an empty token, and the second simply names a path under no
|
|
251
|
+
// guarded prefix.
|
|
252
|
+
let tok = "";
|
|
253
|
+
let tq = null;
|
|
254
|
+
while (i < src.length) {
|
|
255
|
+
const d = src[i];
|
|
256
|
+
if (tq) {
|
|
257
|
+
if (d === "\\" && tq === '"') {
|
|
258
|
+
tok += src[i + 1] ?? "";
|
|
259
|
+
i += 2;
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
if (d === tq) {
|
|
263
|
+
tq = null;
|
|
264
|
+
i += 1;
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
tok += d;
|
|
268
|
+
i += 1;
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
if (d === "'" || d === '"') {
|
|
272
|
+
tq = d;
|
|
273
|
+
i += 1;
|
|
274
|
+
continue;
|
|
275
|
+
}
|
|
276
|
+
if (/[\s;|&()<>]/.test(d)) break;
|
|
277
|
+
if (d === "\\") {
|
|
278
|
+
tok += src[i + 1] ?? "";
|
|
279
|
+
i += 2;
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
tok += d;
|
|
283
|
+
i += 1;
|
|
284
|
+
}
|
|
285
|
+
if (tok !== "") out.push(tok.replace(/^\.\//, ""));
|
|
286
|
+
}
|
|
287
|
+
return out;
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* 🔴 UNQUOTES rather than DELETES — the argument scan used to strip quoted spans, and a quoted
|
|
292
|
+
* path therefore vanished before it could be matched. MEASURED against this guard:
|
|
293
|
+
*
|
|
294
|
+
* sed -i s/a/b/ <root>/x/paper.tex → blocked
|
|
295
|
+
* sed -i s/a/b/ "<root>/x/paper.tex" → ALLOWED
|
|
296
|
+
*
|
|
297
|
+
* Quoting a path is what anyone writes for a path with a space in it, so the gate was one quote
|
|
298
|
+
* from open. Deleting quoted text was not wrong for its own job — it stopped the guard firing on
|
|
299
|
+
* a command that merely QUOTES a paper write, which really happened — but that job now belongs
|
|
300
|
+
* to `writesTo`, which answers "is this WRITTEN" instead of "is this mentioned". With the write
|
|
301
|
+
* established there, the argument scan only has to say WHICH file, so it must see the path, not
|
|
302
|
+
* lose it. Heredocs are still collapsed: their body is data, not argv.
|
|
303
|
+
*/
|
|
304
|
+
const unquote = (s) =>
|
|
305
|
+
s
|
|
306
|
+
.replace(/<<-?\s*'?(\w+)'?[\s\S]*?^\1$/gm, "<<HEREDOC")
|
|
307
|
+
.replace(/'([^']*)'/g, "$1")
|
|
308
|
+
.replace(/"((?:[^"\\]|\\.)*)"/g, "$1");
|
|
309
|
+
|
|
310
|
+
const redirectsInto = (raw, prefixes) =>
|
|
311
|
+
redirectTargets(raw).some(
|
|
312
|
+
(t) =>
|
|
313
|
+
// ⚠️ This leg is WIDER than the argv leg: it claims any redirect target under the root,
|
|
314
|
+
// whatever its extension, so the frozen-snapshot carve-out has to be repeated here. It
|
|
315
|
+
// does not reach the argv leg's carve-outs either — noted, not widened, because loosening
|
|
316
|
+
// a guard beyond the case at hand is how gates stop holding.
|
|
317
|
+
!isFrozenSnapshot(t) &&
|
|
318
|
+
prefixes.some(
|
|
319
|
+
(p) => t === p || t.startsWith(p + "/") || t.includes("/" + p + "/"),
|
|
320
|
+
),
|
|
321
|
+
);
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Programs that WRITE a path given as an argument. The list is explicit because `touches`
|
|
325
|
+
* answers "is this path MENTIONED", not "is it written".
|
|
326
|
+
*
|
|
327
|
+
* 🔴 An earlier version leaned on `isSideEffecting()` to tell a read from a write. That
|
|
328
|
+
* classifies the WHOLE command line, so `grep -c '☐' <root>/x/STATUS.md 2>/dev/null` came back
|
|
329
|
+
* side-effecting, touched a paper path, and was blocked — a plain read, refused. Coarse enough
|
|
330
|
+
* to be useless as a discriminator, and it fired on real work twice within the hour.
|
|
331
|
+
*/
|
|
332
|
+
const MUTATORS = [
|
|
333
|
+
"sed -i",
|
|
334
|
+
"cp",
|
|
335
|
+
"mv",
|
|
336
|
+
"tee",
|
|
337
|
+
"dd",
|
|
338
|
+
"truncate",
|
|
339
|
+
"install",
|
|
340
|
+
"shred",
|
|
341
|
+
];
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Does the command RUN a mutator?
|
|
345
|
+
*
|
|
346
|
+
* 🔴 THE `runs()` CONJUNCT CANNOT BE DROPPED — it is what separates a write from a read. Without
|
|
347
|
+
* it `cat <root>/x/paper.tex` touches a paper, names a paper source, and would be denied: a
|
|
348
|
+
* plain read refused, which this guard has already done once and which cost real work.
|
|
349
|
+
*
|
|
350
|
+
* ⚠️ A RAW-TEXT CRUTCH USED TO SIT BESIDE IT, AND IT IS GONE BECAUSE ITS PREMISE EXPIRED. On
|
|
351
|
+
* 2026-08-13, against vigiles 15.x, `runs()` read the RAW leaves while `writesTo`/`touches` read
|
|
352
|
+
* the NORMALIZED ones, so the vocabulary contradicted itself on a wrapped command and
|
|
353
|
+
* `env sed -i … <root>/x/paper.tex` was ALLOWED with a full, correct path. A second scan over the
|
|
354
|
+
* unquoted raw text patched that, with a comment saying it "deletes on the day the vocabulary
|
|
355
|
+
* stops disagreeing, not before".
|
|
356
|
+
*
|
|
357
|
+
* That day arrived. Re-measured on vigiles 27.1.4 (2026-09-12), and the old table is stale in
|
|
358
|
+
* the row that motivated the crutch:
|
|
359
|
+
*
|
|
360
|
+
* command runs("sed -i") writesTo touches
|
|
361
|
+
* sed -i … <root>/x/paper.tex true true true
|
|
362
|
+
* env sed -i … <root>/x/paper.tex true ← was FALSE true true
|
|
363
|
+
* env -C <parent> sed -i … <root>/… true ← was false true false
|
|
364
|
+
* cd <parent> && sed -i … <root>/… true false false
|
|
365
|
+
*
|
|
366
|
+
* `runs()` now sees through `env`, `env -C`, `command`, `nice`, `xargs`, `sudo`, `time`, a
|
|
367
|
+
* repeated space and a quoted head — every form tried. So the crutch defended nothing, which is
|
|
368
|
+
* not a judgement but a measurement: removing it left all 78 harness assertions GREEN, and no
|
|
369
|
+
* command form could be found that it alone catches. A line no mutation can kill is a line
|
|
370
|
+
* documenting a defence that does not exist, so it was deleted rather than kept for comfort.
|
|
371
|
+
*
|
|
372
|
+
* ⚠️ WHAT THIS DOES NOT FIX: rows 3 and 4 above. A wrapper that moves the working directory is
|
|
373
|
+
* still invisible, because `touches`/`namesPaperSource` compare against a repo-root prefix and
|
|
374
|
+
* neither knows the command's cwd — see hole 2 in the header. Only the `env`-without-`-C` row
|
|
375
|
+
* closed.
|
|
376
|
+
*/
|
|
377
|
+
const runsMutator = (cmd) => MUTATORS.some((m) => cmd.runs(m));
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* A PAPER SOURCE, not merely a path under the papers root.
|
|
381
|
+
*
|
|
382
|
+
* 🔴 An earlier version guarded the whole subtree, so copying a script into a paper's
|
|
383
|
+
* reproduction bundle — ordinary work, touching no prose — was denied. The gate exists to stop a
|
|
384
|
+
* paper's PROSE being edited past the PostToolUse checks; everything else under that directory
|
|
385
|
+
* is data, code and bundles that no readability gate looks at. Observed while it was blocking
|
|
386
|
+
* the artifact fix the pre-submission gate had just asked for.
|
|
387
|
+
*/
|
|
388
|
+
const isPaperSource = (p) => !isFrozenSnapshot(p) && hasSourceExtension(p);
|
|
389
|
+
|
|
390
|
+
const hasSourceExtension = (p) =>
|
|
391
|
+
p.endsWith(".tex") ||
|
|
392
|
+
/\/(paper|draft)\.md$/.test(p) ||
|
|
393
|
+
/^(paper|draft)\.md$/.test(p);
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* A FROZEN SNAPSHOT — a file under a paper's `versions/`, which is the same carve-out as the
|
|
397
|
+
* reproduction bundle above and was missed only because a snapshot keeps the `.tex` extension of
|
|
398
|
+
* the live source it was copied from.
|
|
399
|
+
*
|
|
400
|
+
* 🔴 Why the gate must not claim it. Everything the guard protects is a PostToolUse check on the
|
|
401
|
+
* paper being WRITTEN — readability thresholds, the pipeline nudge, the unrun-gate check. None of
|
|
402
|
+
* them has anything to say about a snapshot: a frozen version is immutable by construction, it is
|
|
403
|
+
* never the file an author edits, and `paper/stages` already gates it far more strictly than any
|
|
404
|
+
* of those, by BYTES, in both directions.
|
|
405
|
+
*
|
|
406
|
+
* ⚠️ Observed: restoring `versions/2026-08-29-camera-ready.tex` from the one commit that still
|
|
407
|
+
* resolved was denied, so the only recoverable source of four declared stages was unreachable by
|
|
408
|
+
* the tool that could reach it. The guard was refusing the archival write while the live
|
|
409
|
+
* `paper.tex` beside it — the file it actually exists to protect — stayed open.
|
|
410
|
+
*/
|
|
411
|
+
const isFrozenSnapshot = (p) => /(^|\/)versions\/[^/]+$/.test(p);
|
|
412
|
+
|
|
413
|
+
/** Paper-source paths named as arguments (as opposed to redirection targets). */
|
|
414
|
+
const namesPaperSource = (raw, prefixes) =>
|
|
415
|
+
unquote(raw)
|
|
416
|
+
.split(/[\s;|&()<>]+/)
|
|
417
|
+
.some(
|
|
418
|
+
(tok) =>
|
|
419
|
+
isPaperSource(tok) &&
|
|
420
|
+
prefixes.some(
|
|
421
|
+
(p) => tok.startsWith(p + "/") || tok.includes("/" + p + "/"),
|
|
422
|
+
),
|
|
423
|
+
);
|
|
424
|
+
|
|
425
|
+
export default experimental_defineHook({
|
|
426
|
+
on: "PreToolUse",
|
|
427
|
+
match: tools("Bash"),
|
|
428
|
+
needs: [provide("pkg", 'cat "${CLAUDE_PROJECT_DIR:-.}/package.json"')],
|
|
429
|
+
decide: (e) => {
|
|
430
|
+
const root = papersRoot(e.ctx.pkg);
|
|
431
|
+
// A `deny` object rather than a string means the root could not be established. Returning it
|
|
432
|
+
// unchanged is deliberate: there is no root to continue with, so there is nothing to decide.
|
|
433
|
+
if (typeof root !== "string") return root;
|
|
434
|
+
const PAPER_SOURCES = [root];
|
|
435
|
+
|
|
436
|
+
// Reads are fine — the point is that WRITES must not bypass the gates that hang on
|
|
437
|
+
// Edit/Write. A command that merely names a paper is not a bypass. `writesTo` establishes
|
|
438
|
+
// that something under the papers root is WRITTEN — quote-safe, and it sees redirection
|
|
439
|
+
// targets that `touches` cannot. The argument scan then only decides WHICH file, so the
|
|
440
|
+
// extension filter still applies and an artifact/bundle write stays allowed.
|
|
441
|
+
// ⚠️ That scan survives only because `writesTo` returns a boolean, not the targets it
|
|
442
|
+
// matched; with `writeTargets()` upstream this helper deletes.
|
|
443
|
+
const writesViaArgv =
|
|
444
|
+
runsMutator(e.command) &&
|
|
445
|
+
(e.command.writesTo(PAPER_SOURCES) || e.command.touches(PAPER_SOURCES)) &&
|
|
446
|
+
namesPaperSource(e.command.raw, PAPER_SOURCES);
|
|
447
|
+
const hits = writesViaArgv || redirectsInto(e.command.raw, PAPER_SOURCES);
|
|
448
|
+
if (!hits) return allow();
|
|
449
|
+
|
|
450
|
+
return deny(
|
|
451
|
+
"Writing a paper source from Bash skips every PostToolUse gate " +
|
|
452
|
+
"(the readability thresholds, the pipeline nudge, the unrun-gate check). " +
|
|
453
|
+
"Use Edit or Write. For a machine-generated rewrite, emit to a temp file " +
|
|
454
|
+
`outside ${root}/ and land it with Write.`,
|
|
455
|
+
);
|
|
456
|
+
},
|
|
457
|
+
});
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* paper-skills-nudge — a PostToolUse react: editing a paper SOURCE surfaces the static
|
|
3
|
+
* pre-submit checklist, so the pipeline is driven rather than winged.
|
|
4
|
+
*
|
|
5
|
+
* ── THE BUG THIS REMOVES ────────────────────────────────────────────────────
|
|
6
|
+
* Its shell predecessor was dead for twelve days: it read the payload from `$CLAUDE_TOOL_INPUT`
|
|
7
|
+
* while the harness delivers on stdin, so its path match never ran and it exited 0 in silence
|
|
8
|
+
* across dozens of paper edits. An advisory hook cannot be NOTICED failing — silence is also its
|
|
9
|
+
* success case, which is why it took a frozen throttle stamp to find it. A `react` never touches
|
|
10
|
+
* the payload: the runtime decodes the event and hands in `e.path`, so the whole family of "read
|
|
11
|
+
* the wrong input / wrote the wrong field / exited the wrong code" is unrepresentable here.
|
|
12
|
+
*
|
|
13
|
+
* ── SPLIT FROM `paper-status-gates`, DELIBERATELY ───────────────────────────
|
|
14
|
+
* This carries the STATIC checklist. The part that reads `<paper-dir>/PIPELINE-STATUS.md` and
|
|
15
|
+
* surfaces unrun gates lives in `paper-status-gates.hook.mjs`, because it needs a tool run and
|
|
16
|
+
* this one does not. Two hooks on one event is the honest line between a constant and an I/O,
|
|
17
|
+
* not an accident of porting.
|
|
18
|
+
*
|
|
19
|
+
* ── DELIVERY, MEASURED (2026-09-12, vigiles 27.1.4) ─────────────────────────
|
|
20
|
+
* `PostToolUse` is in the adapter's `injectableEvents`, so a `notice` on this event reaches the
|
|
21
|
+
* model as `hookSpecificOutput.additionalContext` on STDOUT — 1 346 bytes on a paper edit, 0 on
|
|
22
|
+
* anything else — with a copy on stderr for a human. That is NOT true of every event: `Stop` and
|
|
23
|
+
* `PreToolUse` are absent from that list, and a `notice` there reaches only the debug log. Do not
|
|
24
|
+
* move this hook to another event assuming its text still lands.
|
|
25
|
+
*
|
|
26
|
+
* ── WHY `.mjs` AND NOT A `.hook.ts` SPEC ────────────────────────────────────
|
|
27
|
+
* Measured in `paper-edit-guard.hook.mjs` — a `.ts` hook reached through `node_modules` does not
|
|
28
|
+
* load at all, and a consumer-side thin spec that imports this logic is refused by
|
|
29
|
+
* `vigiles compile` and can therefore never be re-stamped. The full reasoning, with both
|
|
30
|
+
* measurements, is in that file's header; the capability surface of this one is asserted by
|
|
31
|
+
* `hooks.harness.mjs` running `checkHookImports` over the shipped artifact.
|
|
32
|
+
*/
|
|
33
|
+
import {
|
|
34
|
+
experimental_defineReact,
|
|
35
|
+
tools,
|
|
36
|
+
provide,
|
|
37
|
+
notice,
|
|
38
|
+
nothing,
|
|
39
|
+
} from "vigiles/hook";
|
|
40
|
+
|
|
41
|
+
/** The key every carrier of this package reads its consumer-specific settings from. */
|
|
42
|
+
export const CONFIG_KEY = "paperlint";
|
|
43
|
+
/** The key's name before 2.0.0 — still read, a copy of `lib/paper-config.mjs`. */
|
|
44
|
+
export const LEGACY_CONFIG_KEY = "research-paper-pipeline";
|
|
45
|
+
/** The default. A consumer that declares nothing is assumed to keep papers in `papers/`. */
|
|
46
|
+
export const DEFAULT_PAPERS_ROOT = "papers";
|
|
47
|
+
/**
|
|
48
|
+
* The field under CONFIG_KEY that names the papers directory, and its old name. A copy of the
|
|
49
|
+
* constants in `lib/paper-config.mjs` (a hook may import nothing but `vigiles/hook`);
|
|
50
|
+
* `lib/paper-config.harness.mjs` checks that the copies match.
|
|
51
|
+
*/
|
|
52
|
+
export const PAPERS_DIR_FIELD = "papersDir";
|
|
53
|
+
export const OLD_PAPERS_DIR_FIELD = "papers";
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* The declared papers root, or `null` when it is unusable.
|
|
57
|
+
*
|
|
58
|
+
* ⚠️ NO TRAILING-SLASH NORMALISATION HERE, and that is measured rather than sloppy. The guard
|
|
59
|
+
* hand-writes its own prefix tests (`startsWith(p + "/")`), where a slash the consumer typed
|
|
60
|
+
* becomes `//` and matches nothing — a defect that shipped once. This hook instead uses
|
|
61
|
+
* `e.path.under()`, and vigiles normalises the prefix itself (`normalizePrefix` →
|
|
62
|
+
* `trimTrailingSeparators`), so `"docs/papers/"` and `"docs/papers"` are the same prefix to it.
|
|
63
|
+
* A normalise here would be a line no test could kill, which is a line that documents a
|
|
64
|
+
* defence that does not exist. Checked 2026-09-12 by mutation: removing it left the harness
|
|
65
|
+
* green, which is the finding that produced this paragraph.
|
|
66
|
+
*
|
|
67
|
+
* ⚠️ A NUDGE FALLS SILENT WHERE THE GATE REFUSES, and the asymmetry is the decision, not an
|
|
68
|
+
* inconsistency. `paper-edit-guard` denies on an unreadable declaration because a gate that
|
|
69
|
+
* quietly reverts to the default would pass every write it exists to stop. This hook has no such
|
|
70
|
+
* failure mode: with no root it prints nothing, which is what it already does on every
|
|
71
|
+
* non-paper edit. Denying here would mean an advisory hook blocking work over a courtesy
|
|
72
|
+
* message — and a `react` cannot deny at all: its type has no `deny`, so it always exits 0.
|
|
73
|
+
*
|
|
74
|
+
* 🔴 `declared === undefined`, NOT `declared ?? DEFAULT` — `"papersDir": null` is a keystroke, not
|
|
75
|
+
* an absence. Same distinction as every other carrier in this package.
|
|
76
|
+
*/
|
|
77
|
+
const papersRoot = (rawPkg) => {
|
|
78
|
+
let declared;
|
|
79
|
+
try {
|
|
80
|
+
const pkg = JSON.parse(rawPkg);
|
|
81
|
+
const settings = pkg?.[CONFIG_KEY] ?? pkg?.[LEGACY_CONFIG_KEY];
|
|
82
|
+
// Old field name: stay silent rather than fall back to the default directory.
|
|
83
|
+
if (settings && Object.hasOwn(settings, OLD_PAPERS_DIR_FIELD)) return null;
|
|
84
|
+
declared = settings?.[PAPERS_DIR_FIELD];
|
|
85
|
+
} catch {
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
const root = declared === undefined ? DEFAULT_PAPERS_ROOT : declared;
|
|
89
|
+
if (typeof root !== "string" || root.length === 0) return null;
|
|
90
|
+
return root;
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
/** A paper SOURCE — a `.tex`, or a markdown-drafted `paper.md` / `draft.md`. Not every note or README. */
|
|
94
|
+
const isPaperSource = (p) =>
|
|
95
|
+
p.endsWith(".tex") || /\/(paper|draft)\.md$/.test(p);
|
|
96
|
+
|
|
97
|
+
const CHECKLIST = `📄 Editing a paper — drive it with the \`paper-pipeline\` skill, don't wing the review.
|
|
98
|
+
→ Update <paper-dir>/PIPELINE-STATUS.md: mark the row for any stage you (re)ran, and read its
|
|
99
|
+
verdict line to see what's still ☐/◐/⚠ before submit.
|
|
100
|
+
Pre-submit checklist (skills):
|
|
101
|
+
• verify-citations — every \\cite real; nearest-neighbor delta explicit
|
|
102
|
+
• study-accepted-papers — the venue's Accept→Strong levers
|
|
103
|
+
• analyze-sibling-paper — deep-read each close competitor; SAVE colocated in <paper-dir>/siblings/
|
|
104
|
+
• grade-paper-writing — BLIND ≥3-grader panel (absolute scale) + cold-read stall pass
|
|
105
|
+
• pc-panel-review / paper-adversarial-review — multi-axis scorecard + the "reads-like-slop" halo
|
|
106
|
+
• harden-paper — threat-model · ethics · page-fit · de-anon · ACTUALLY RUN the artifact clean
|
|
107
|
+
RULES earned the hard way:
|
|
108
|
+
• After every aggressive rewrite/thinning pass → claim-preservation DIFF vs the pre-pass baseline
|
|
109
|
+
• Numbers overwhelming the reader? secondary numbers live in the artifact, not the paper
|
|
110
|
+
• bib inside \\begin{filecontents*}{refs.bib}? edit it THERE — refs.bib is overwritten on build
|
|
111
|
+
• Done + claim-clean + page-legal ships. Further churn breaks about as much as it fixes.`;
|
|
112
|
+
|
|
113
|
+
export default experimental_defineReact({
|
|
114
|
+
on: "PostToolUse",
|
|
115
|
+
match: tools("Edit", "Write", "MultiEdit"),
|
|
116
|
+
// 🔴 THE PATH IS ANCHORED TO THE PROJECT ROOT, and that is not decoration. vigiles runs the
|
|
117
|
+
// provider "via execSync in the hook's cwd", and the hook process's cwd is the consumer's
|
|
118
|
+
// own wiring — a string this hook cannot see. A bare `cat package.json` therefore reads from
|
|
119
|
+
// whatever directory the Bash tool last moved to, and a `cd` into a subdirectory with no
|
|
120
|
+
// manifest breaks the read.
|
|
121
|
+
//
|
|
122
|
+
// ⚠️ THE FAILURE HERE IS SILENT, which makes it more dangerous than its neighbor's.
|
|
123
|
+
// `paper-edit-guard` on PreToolUse DENIES loudly and visibly when the declaration is
|
|
124
|
+
// unreadable. This hook on PostToolUse just returns `nothing()`, i.e. simply stops firing:
|
|
125
|
+
// the `cat` chain fails → empty string → `JSON.parse("")` throws → `papersRoot` returns null
|
|
126
|
+
// → silence. And silence is exactly what a nudge's success state looks like, so a dead hook
|
|
127
|
+
// is indistinguishable from a working one.
|
|
128
|
+
needs: [provide("pkg", 'cat "${CLAUDE_PROJECT_DIR:-.}/package.json"')],
|
|
129
|
+
react: (e) => {
|
|
130
|
+
const root = papersRoot(e.ctx.pkg);
|
|
131
|
+
if (root === null) return nothing();
|
|
132
|
+
return e.path.under([root]) && isPaperSource(e.path.raw)
|
|
133
|
+
? notice(CHECKLIST)
|
|
134
|
+
: nothing();
|
|
135
|
+
},
|
|
136
|
+
});
|