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,713 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* hooks.harness.mjs β the three shipped hooks, each asserted in BOTH halves.
|
|
3
|
+
*
|
|
4
|
+
* π΄ WHY "BOTH HALVES" IS THE ORGANISING RULE HERE AND NOT A STYLE. Two of these three are
|
|
5
|
+
* ADVISORY: silence is their success state. So "it printed nothing" and "it is dead" look
|
|
6
|
+
* identical from outside, and a harness that only checks the quiet case is green over a corpse.
|
|
7
|
+
* Three hooks in the first consumer were dead for twelve days behind exactly that. Every block
|
|
8
|
+
* below therefore pairs a firing case with a silent one.
|
|
9
|
+
*
|
|
10
|
+
* ββ WHAT IS MEASURED: BYTES AND EXIT CODES, NOT THE RETURNED OBJECT βββββββββ
|
|
11
|
+
* π΄ The first consumer's harness asserts over the REACTION β the object the pure function
|
|
12
|
+
* returned β and that is why a 184-assertion suite could not see that nine of its nudges were
|
|
13
|
+
* being delivered nowhere. "The hook fired" and "the hook's text landed" are different claims
|
|
14
|
+
* and the second does not follow from the first. Here every assertion reads `exitCode`,
|
|
15
|
+
* `stdout` and `stderr` of a real `run-program`, through `runHook`.
|
|
16
|
+
*
|
|
17
|
+
* ββ THE CAPABILITY SURFACE IS ASSERTED ON THE ARTIFACT THAT EXECUTES ββββββββ
|
|
18
|
+
* These hooks ship as `.mjs`, so `vigiles compile` β which normally enforces "a compiled hook
|
|
19
|
+
* may import ONLY `vigiles/hook`" β never sees them. Part I runs that same check,
|
|
20
|
+
* `checkHookImports`, over the shipped files directly. That is STRICTER than compiling a twin
|
|
21
|
+
* `.ts` source would be: a twin can drift from its build, and there is no twin here to drift.
|
|
22
|
+
*
|
|
23
|
+
* Run: `npx vigiles test hooks/hooks.harness.mjs`
|
|
24
|
+
* Killed by: `hooks/hooks.mutations.mjs`
|
|
25
|
+
*/
|
|
26
|
+
import assert from "node:assert/strict";
|
|
27
|
+
import { runHook } from "vigiles";
|
|
28
|
+
import { checkHookImports } from "vigiles/hook";
|
|
29
|
+
import {
|
|
30
|
+
mkdtempSync,
|
|
31
|
+
mkdirSync,
|
|
32
|
+
readdirSync,
|
|
33
|
+
readFileSync,
|
|
34
|
+
rmSync,
|
|
35
|
+
symlinkSync,
|
|
36
|
+
writeFileSync,
|
|
37
|
+
realpathSync,
|
|
38
|
+
} from "node:fs";
|
|
39
|
+
import { tmpdir } from "node:os";
|
|
40
|
+
import { dirname, join, resolve } from "node:path";
|
|
41
|
+
import { fileURLToPath } from "node:url";
|
|
42
|
+
import {
|
|
43
|
+
OLD_PAPERS_DIR_FIELD,
|
|
44
|
+
PAPERS_DIR_FIELD,
|
|
45
|
+
} from "../lib/paper-config.mjs";
|
|
46
|
+
|
|
47
|
+
const HOOKS = dirname(fileURLToPath(import.meta.url));
|
|
48
|
+
const ROOT = resolve(HOOKS, "..");
|
|
49
|
+
const CLI = join(ROOT, "node_modules", "vigiles", "dist", "cli.js");
|
|
50
|
+
/** `>` built rather than typed: a literal one in this file is a redirection to the guard's own scan. */
|
|
51
|
+
const GT = String.fromCharCode(62);
|
|
52
|
+
|
|
53
|
+
const SHIPPED = readdirSync(HOOKS)
|
|
54
|
+
.filter((f) => f.endsWith(".hook.mjs"))
|
|
55
|
+
.sort();
|
|
56
|
+
|
|
57
|
+
let n = 0;
|
|
58
|
+
const check = (label, cond) => {
|
|
59
|
+
assert.ok(cond, label);
|
|
60
|
+
n++;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* A throwaway consumer repository: a `package.json` with the given block, a papers tree, and
|
|
65
|
+
* `node_modules` wired so both `vigiles/hook` and `require.resolve("<this package>/β¦")` work
|
|
66
|
+
* from inside it.
|
|
67
|
+
*
|
|
68
|
+
* π΄ IT IS A REAL DIRECTORY, NOT A MOCK, because what is under test is precisely the part a
|
|
69
|
+
* mock would stub out: whether the hook can READ a declaration out of the consumer's
|
|
70
|
+
* `package.json` via a provider command, and whether a path resolves once this package is
|
|
71
|
+
* installed rather than adjacent. The `node_modules/<pkg>` entry is a symlink to this very
|
|
72
|
+
* checkout, which is what `npm install` of a local package does anyway.
|
|
73
|
+
*/
|
|
74
|
+
const consumer = (block, { papers = "docs/papers", paper = "alpha" } = {}) => {
|
|
75
|
+
const dir = realpathSync(mkdtempSync(join(tmpdir(), "rpp-hooks-")));
|
|
76
|
+
const nm = join(dir, "node_modules");
|
|
77
|
+
mkdirSync(nm, { recursive: true });
|
|
78
|
+
symlinkSync(join(ROOT, "node_modules", "vigiles"), join(nm, "vigiles"));
|
|
79
|
+
symlinkSync(ROOT, join(nm, "paperlint"));
|
|
80
|
+
writeFileSync(
|
|
81
|
+
join(dir, "package.json"),
|
|
82
|
+
block === null
|
|
83
|
+
? '{"name":"c","type":"module"}\n'
|
|
84
|
+
: JSON.stringify({ name: "c", type: "module", ...block }) + "\n",
|
|
85
|
+
);
|
|
86
|
+
if (papers !== null) {
|
|
87
|
+
mkdirSync(join(dir, papers, paper), { recursive: true });
|
|
88
|
+
writeFileSync(
|
|
89
|
+
join(dir, papers, paper, "PIPELINE-STATUS.md"),
|
|
90
|
+
"**Readiness verdict:** β
fixture verdict line\n\n| gate | state |\n| study-accepted | β |\n",
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
return dir;
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
/** `run-program <hook>` exactly as a consumer's settings block invokes it. */
|
|
97
|
+
const program = (name) =>
|
|
98
|
+
`node ${JSON.stringify(CLI)} hook-runtime run-program ${JSON.stringify(join(HOOKS, name + ".hook.mjs"))}`;
|
|
99
|
+
|
|
100
|
+
const onBash = (command) => ({
|
|
101
|
+
hook_event_name: "PreToolUse",
|
|
102
|
+
tool_name: "Bash",
|
|
103
|
+
tool_input: { command },
|
|
104
|
+
});
|
|
105
|
+
const onEdit = (file_path) => ({
|
|
106
|
+
hook_event_name: "PostToolUse",
|
|
107
|
+
tool_name: "Edit",
|
|
108
|
+
tool_input: { file_path },
|
|
109
|
+
tool_response: {},
|
|
110
|
+
});
|
|
111
|
+
const STOP = { hook_event_name: "Stop", tool_name: "", tool_input: {} };
|
|
112
|
+
|
|
113
|
+
/** Run a hook AS a consumer: cwd and `CLAUDE_PROJECT_DIR` both point at the fixture repo. */
|
|
114
|
+
const at = (dir, name, input) =>
|
|
115
|
+
runHook(program(name), input, { cwd: dir, env: { CLAUDE_PROJECT_DIR: dir } });
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* The same hook, run with the tool's cwd DRIFTED away from the project root.
|
|
119
|
+
*
|
|
120
|
+
* π΄ Why this helper has to exist at all: `at()` sets `cwd` and `CLAUDE_PROJECT_DIR` to the SAME
|
|
121
|
+
* directory, so a provider reading `package.json` relatively and one reading it anchored are
|
|
122
|
+
* indistinguishable under it. The defect was therefore invisible by construction β not missed
|
|
123
|
+
* by inattention, but unexpressible by the only runner the harness had.
|
|
124
|
+
*
|
|
125
|
+
* β οΈ The drifted cwd is deliberately a directory with NO `package.json`, because that is the
|
|
126
|
+
* live case: a session `cd`s into a subtree, and vigiles runs every provider Β«via execSync in
|
|
127
|
+
* the hook's cwdΒ».
|
|
128
|
+
*/
|
|
129
|
+
const adrift = (dir, name, input) =>
|
|
130
|
+
runHook(program(name), input, {
|
|
131
|
+
cwd: realpathSync(mkdtempSync(join(tmpdir(), "drift-"))),
|
|
132
|
+
env: { CLAUDE_PROJECT_DIR: dir },
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
/** An injected notice, as the model would receive it β not the returned reaction. */
|
|
136
|
+
const injected = (r) => {
|
|
137
|
+
try {
|
|
138
|
+
return JSON.parse(r.stdout).hookSpecificOutput.additionalContext ?? "";
|
|
139
|
+
} catch {
|
|
140
|
+
return "";
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
const tmps = [];
|
|
145
|
+
const fixture = (...args) => {
|
|
146
|
+
const d = consumer(...args);
|
|
147
|
+
tmps.push(d);
|
|
148
|
+
return d;
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
try {
|
|
152
|
+
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
153
|
+
// I. CAPABILITY CLOSURE β on the shipped artifact, not on a source twin
|
|
154
|
+
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
155
|
+
// β οΈ `checkHookImports` IS A REGEX OVER THE SOURCE TEXT, so it counts an import-shaped
|
|
156
|
+
// sentence inside a COMMENT as an offender. Measured 2026-09-12: this block failed on the
|
|
157
|
+
// guard's own docblock, which quoted the rejected form verbatim while explaining why it was
|
|
158
|
+
// rejected. That is the product's own instrument landing on the "a string is the SHADOW of a
|
|
159
|
+
// thing" rule β and the fix is NOT to loosen the check, because it is the same check any
|
|
160
|
+
// future `vigiles compile`/`lint` pass would apply to the shipped artifact. It is to describe
|
|
161
|
+
// a forbidden import in prose WITHOUT writing one. Anyone extending these headers: do not
|
|
162
|
+
// quote an import statement in a comment here; name it in words.
|
|
163
|
+
check("there are shipped hooks to check at all", SHIPPED.length === 3);
|
|
164
|
+
for (const f of SHIPPED) {
|
|
165
|
+
const offenders = checkHookImports(readFileSync(join(HOOKS, f), "utf8"));
|
|
166
|
+
check(
|
|
167
|
+
`${f} imports ONLY vigiles/hook (offenders: ${offenders.join(", ") || "none"})`,
|
|
168
|
+
offenders.length === 0,
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
173
|
+
// II. EVERY SHIPPED HOOK LOADS β FIRST among the behavioural blocks, deliberately
|
|
174
|
+
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
175
|
+
// π΄ A hook that cannot resolve its imports prints `vigiles: cannot load hook program` and,
|
|
176
|
+
// for a PreToolUse gate, REFUSES EVERY COMMAND. In the first consumer that state was reached
|
|
177
|
+
// five separate times β a lockfile behind its manifest, a missing `node_modules`, a merge
|
|
178
|
+
// conflict on the import path, a removed export after a major bump. The behavioural blocks
|
|
179
|
+
// below do go red for it, but they name a hundred other things first: "blocks a redirect
|
|
180
|
+
// write" is a long way from "your runtime lacks the export this hook imports".
|
|
181
|
+
//
|
|
182
|
+
// π IT RUNS EARLY, AND THAT PLACEMENT IS THE CHECK β node's assert aborts on the first
|
|
183
|
+
// failure, so a diagnostic written at the END only ever prints when nothing else is broken,
|
|
184
|
+
// which is when it diagnoses nothing.
|
|
185
|
+
{
|
|
186
|
+
const dir = fixture({
|
|
187
|
+
paperlint: { [PAPERS_DIR_FIELD]: "docs/papers" },
|
|
188
|
+
});
|
|
189
|
+
for (const f of SHIPPED) {
|
|
190
|
+
const name = f.replace(/\.hook\.mjs$/, "");
|
|
191
|
+
const r = at(dir, name, STOP);
|
|
192
|
+
check(
|
|
193
|
+
`${name} LOADS (the runtime can resolve its imports)`,
|
|
194
|
+
!/cannot load hook program/.test(r.stdout + r.stderr),
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
200
|
+
// III. paper-edit-guard β the blocking half and the allowing half
|
|
201
|
+
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
202
|
+
{
|
|
203
|
+
const dir = fixture({
|
|
204
|
+
paperlint: { [PAPERS_DIR_FIELD]: "docs/papers" },
|
|
205
|
+
});
|
|
206
|
+
const P = "docs/papers/alpha/paper.md";
|
|
207
|
+
const T = "docs/papers/alpha/paper.tex";
|
|
208
|
+
const deny = (label, cmd) => {
|
|
209
|
+
const r = at(dir, "paper-edit-guard", onBash(cmd));
|
|
210
|
+
check(
|
|
211
|
+
`guard BLOCKS ${label} (rc=${r.exitCode})`,
|
|
212
|
+
r.exitCode === 2 && r.blocked,
|
|
213
|
+
);
|
|
214
|
+
check(
|
|
215
|
+
`guard's refusal of ${label} says what to do instead`,
|
|
216
|
+
/Use Edit or Write/.test(r.stderr),
|
|
217
|
+
);
|
|
218
|
+
};
|
|
219
|
+
const allow = (label, cmd) => {
|
|
220
|
+
const r = at(dir, "paper-edit-guard", onBash(cmd));
|
|
221
|
+
check(
|
|
222
|
+
`guard ALLOWS ${label} (rc=${r.exitCode}, ${r.stderr.length}b err)`,
|
|
223
|
+
r.exitCode === 0 && !r.blocked,
|
|
224
|
+
);
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
// The two cases `touches` alone MISSES: a redirection target is not an argv token.
|
|
228
|
+
deny("a redirect write to paper.md", `echo x ${GT} ${P}`);
|
|
229
|
+
deny("an append write to a .tex", `cat /tmp/a ${GT}${GT} ${T}`);
|
|
230
|
+
deny("sed -i on a paper", `sed -i s/a/b/ ${P}`);
|
|
231
|
+
deny("cp onto a paper", `cp /tmp/a ${P}`);
|
|
232
|
+
// The quote bypasses: both were OPEN until 2026-08-13 and each was one quote wide.
|
|
233
|
+
deny("a QUOTED redirect target", `echo x ${GT} "${P}"`);
|
|
234
|
+
deny("a QUOTED argv path", `sed -i s/a/b/ "${T}"`);
|
|
235
|
+
// The wrapper bypass: `runs()` reads RAW leaves, `writesTo`/`touches` read NORMALIZED ones.
|
|
236
|
+
deny("a mutator behind `env`", `env sed -i s/a/b/ ${T}`);
|
|
237
|
+
|
|
238
|
+
allow("reading a paper", `cat ${P}`);
|
|
239
|
+
allow("an unrelated command", "echo hi");
|
|
240
|
+
allow("an unrelated write", `echo x ${GT} /tmp/unrelated.txt`);
|
|
241
|
+
// The false positives that blocked real work: a bundle copy is data, and a grep is a read.
|
|
242
|
+
allow(
|
|
243
|
+
"a copy into a paper's repro bundle",
|
|
244
|
+
`cp h.mjs docs/papers/alpha/repro/anon/h.mjs`,
|
|
245
|
+
);
|
|
246
|
+
// A FROZEN SNAPSHOT is archival, not the live source: `versions/` is immutable by
|
|
247
|
+
// construction and `paper/stages` gates it by bytes, which is stricter than anything the
|
|
248
|
+
// PostToolUse checks this guard protects would say about it.
|
|
249
|
+
allow(
|
|
250
|
+
"freezing a snapshot into versions/",
|
|
251
|
+
`cp /tmp/frozen.tex docs/papers/alpha/versions/2026-08-29-camera-ready.tex`,
|
|
252
|
+
);
|
|
253
|
+
allow(
|
|
254
|
+
"a redirect into versions/",
|
|
255
|
+
`echo x ${GT} docs/papers/alpha/versions/a.tex`,
|
|
256
|
+
);
|
|
257
|
+
// β οΈ NOT exempt, and deliberately asserted so the limit is recorded rather than discovered:
|
|
258
|
+
// with the LIVE source as the copy's argument, `namesPaperSource` hits on that token, and
|
|
259
|
+
// argv carries no direction β `cp a b` and `cp b a` are the same shape. Freezing therefore
|
|
260
|
+
// goes through a temp file, which is what the refusal already tells the caller to do.
|
|
261
|
+
deny(
|
|
262
|
+
"a copy whose SOURCE argument is the live paper",
|
|
263
|
+
`cp ${T} docs/papers/alpha/versions/a.tex`,
|
|
264
|
+
);
|
|
265
|
+
// π΄ The other half: the carve-out must not reach the LIVE source, which keeps the same
|
|
266
|
+
// extension. Without this assert the exemption could be widened to `.tex` and stay green.
|
|
267
|
+
deny("sed -i on the live source beside versions/", `sed -i s/a/b/ ${T}`);
|
|
268
|
+
deny(
|
|
269
|
+
"a write to a .tex that merely MENTIONS versions",
|
|
270
|
+
`cp /tmp/versions/a.tex ${T}`,
|
|
271
|
+
);
|
|
272
|
+
allow(
|
|
273
|
+
"a grep of a status file",
|
|
274
|
+
`grep -c x docs/papers/alpha/PIPELINE-STATUS.md 2${GT}/dev/null`,
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
279
|
+
// III-b. CWD DRIFT β every hook reads its manifest from the PROJECT ROOT
|
|
280
|
+
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
281
|
+
// π΄ Reported live 2026-09-16 by a consumer session: after a `cd` into a subdirectory with no
|
|
282
|
+
// `package.json`, `paper-edit-guard` denied EVERY Bash command β `pwd` included β and the wedge
|
|
283
|
+
// could not be escaped from the shell, because a PreToolUse hook fires before the command that
|
|
284
|
+
// would fix it. The cause is one character of scope: the provider read `package.json`
|
|
285
|
+
// relatively, and vigiles runs providers Β«via execSync in the hook's cwdΒ».
|
|
286
|
+
//
|
|
287
|
+
// π΄ AND THE SIBLINGS FAIL THE OTHER WAY, WHICH IS WHY THEY GET THEIR OWN ASSERTIONS. They are
|
|
288
|
+
// PostToolUse and answer `nothing()` when the root will not parse, so a drifted cwd does not
|
|
289
|
+
// block anything β it turns them OFF. Silence is a nudge's success state, so a dead nudge and a
|
|
290
|
+
// working one produce identical output. The lockout announces itself; this does not.
|
|
291
|
+
{
|
|
292
|
+
const dir = fixture({
|
|
293
|
+
paperlint: { [PAPERS_DIR_FIELD]: "docs/papers" },
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
// paper-edit-guard: still GUARDS from a foreign cwd, rather than denying everything.
|
|
297
|
+
const write = at(
|
|
298
|
+
dir,
|
|
299
|
+
"paper-edit-guard",
|
|
300
|
+
onBash(`sed -i s/a/b/ docs/papers/alpha/paper.tex`),
|
|
301
|
+
);
|
|
302
|
+
check("drift Β· guard still blocks a paper write", write.exitCode === 2);
|
|
303
|
+
const idle = adrift(dir, "paper-edit-guard", onBash("echo hi"));
|
|
304
|
+
check(
|
|
305
|
+
`drift Β· guard does NOT deny an unrelated command (rc=${idle.exitCode})`,
|
|
306
|
+
idle.exitCode === 0,
|
|
307
|
+
);
|
|
308
|
+
const guarded = adrift(
|
|
309
|
+
dir,
|
|
310
|
+
"paper-edit-guard",
|
|
311
|
+
onBash(`sed -i s/a/b/ docs/papers/alpha/paper.tex`),
|
|
312
|
+
);
|
|
313
|
+
check(
|
|
314
|
+
"drift Β· guard STILL blocks the paper write it exists for",
|
|
315
|
+
guarded.exitCode === 2,
|
|
316
|
+
);
|
|
317
|
+
|
|
318
|
+
// The two PostToolUse hooks: still SPEAK from a foreign cwd.
|
|
319
|
+
const nudge = adrift(
|
|
320
|
+
dir,
|
|
321
|
+
"paper-skills-nudge",
|
|
322
|
+
onEdit(`${dir}/docs/papers/alpha/paper.tex`),
|
|
323
|
+
);
|
|
324
|
+
check("drift Β· the skills nudge still fires", injected(nudge).length > 0);
|
|
325
|
+
const gates = adrift(
|
|
326
|
+
dir,
|
|
327
|
+
"paper-status-gates",
|
|
328
|
+
onEdit(`${dir}/docs/papers/alpha/PIPELINE-STATUS.md`),
|
|
329
|
+
);
|
|
330
|
+
check(
|
|
331
|
+
`drift Β· the status-gates hook still runs (rc=${gates.exitCode})`,
|
|
332
|
+
gates.exitCode === 0,
|
|
333
|
+
);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
337
|
+
// IV. THE CARRIER β a root that cannot be read DENIES, it does not default
|
|
338
|
+
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
339
|
+
// π΄ THIS IS THE BLOCK THE EXTRACTION EXISTS FOR. Measured 2026-09-12: when the provider's
|
|
340
|
+
// command fails, `e.ctx.pkg` arrives as the EMPTY STRING and the runtime says nothing. So a
|
|
341
|
+
// `catch { use the default }` silently re-roots the gate at `papers/`, nothing under the real
|
|
342
|
+
// root matches, and every Bash write to a paper is allowed by a gate that still looks
|
|
343
|
+
// installed. An empty prefix must mean REFUSE, never "pass everything".
|
|
344
|
+
{
|
|
345
|
+
const harmless = onBash("echo hi");
|
|
346
|
+
// π΄ THE TWO PATHS ARE CHOSEN SO THE DECLARATION AND THE DEFAULT DISAGREE. A first draft of
|
|
347
|
+
// this block declared `docs/papers` and tested a write under it β which proves nothing,
|
|
348
|
+
// because the guard's prefix test is DEPTH-AGNOSTIC on purpose (`t.includes("/" + p + "/")`,
|
|
349
|
+
// so a relative path deeper in the tree still matches). `docs/papers/β¦` is therefore a hit
|
|
350
|
+
// under the default `papers` as well, and the "declared root is honoured" assertion passed
|
|
351
|
+
// for a gate that had never read the declaration. Caught by its own negative control,
|
|
352
|
+
// 2026-09-12. A declared root only demonstrates anything when the default would MISS it.
|
|
353
|
+
const DECLARED = "writing/drafts";
|
|
354
|
+
const declaredWrite = onBash(`echo x ${GT} ${DECLARED}/alpha/paper.md`);
|
|
355
|
+
const defaultWrite = onBash(`echo x ${GT} papers/alpha/paper.md`);
|
|
356
|
+
|
|
357
|
+
// (a) a declared root is used AS DECLARED β and the default is NOT.
|
|
358
|
+
{
|
|
359
|
+
const dir = fixture(
|
|
360
|
+
{ paperlint: { [PAPERS_DIR_FIELD]: DECLARED } },
|
|
361
|
+
{ papers: DECLARED },
|
|
362
|
+
);
|
|
363
|
+
check(
|
|
364
|
+
"carrier: a DECLARED root is honoured",
|
|
365
|
+
at(dir, "paper-edit-guard", declaredWrite).exitCode === 2,
|
|
366
|
+
);
|
|
367
|
+
check(
|
|
368
|
+
"carrier: β¦and the DEFAULT root is not guarded once something else is declared",
|
|
369
|
+
at(dir, "paper-edit-guard", defaultWrite).exitCode === 0,
|
|
370
|
+
);
|
|
371
|
+
check(
|
|
372
|
+
"carrier: and it still allows an unrelated command",
|
|
373
|
+
at(dir, "paper-edit-guard", harmless).exitCode === 0,
|
|
374
|
+
);
|
|
375
|
+
}
|
|
376
|
+
// (aΒ½) the key's OLD name (before 2.0.0) is still read β an upgrade must not unguard papers.
|
|
377
|
+
{
|
|
378
|
+
const dir = fixture(
|
|
379
|
+
{ "research-paper-pipeline": { [PAPERS_DIR_FIELD]: DECLARED } },
|
|
380
|
+
{ papers: DECLARED },
|
|
381
|
+
);
|
|
382
|
+
check(
|
|
383
|
+
"carrier: π΄ a root declared under the OLD key is still guarded",
|
|
384
|
+
at(dir, "paper-edit-guard", declaredWrite).exitCode === 2 &&
|
|
385
|
+
at(dir, "paper-edit-guard", defaultWrite).exitCode === 0,
|
|
386
|
+
);
|
|
387
|
+
}
|
|
388
|
+
{
|
|
389
|
+
const dir = fixture(
|
|
390
|
+
{
|
|
391
|
+
paperlint: { [PAPERS_DIR_FIELD]: DECLARED },
|
|
392
|
+
"research-paper-pipeline": { [PAPERS_DIR_FIELD]: "papers" },
|
|
393
|
+
},
|
|
394
|
+
{ papers: DECLARED },
|
|
395
|
+
);
|
|
396
|
+
const r = at(dir, "paper-edit-guard", harmless);
|
|
397
|
+
check(
|
|
398
|
+
"carrier: both keys with different contents β the gate refuses and names both",
|
|
399
|
+
r.exitCode === 2 &&
|
|
400
|
+
/has both "paperlint" and "research-paper-pipeline"/.test(
|
|
401
|
+
r.stderr + r.stdout,
|
|
402
|
+
),
|
|
403
|
+
);
|
|
404
|
+
}
|
|
405
|
+
// (b) no key at all β the documented default, and the converse of (a).
|
|
406
|
+
{
|
|
407
|
+
const dir = fixture(null, { papers: "papers" });
|
|
408
|
+
check(
|
|
409
|
+
"carrier: no declaration β the default root guards `papers/`",
|
|
410
|
+
at(dir, "paper-edit-guard", defaultWrite).exitCode === 2,
|
|
411
|
+
);
|
|
412
|
+
check(
|
|
413
|
+
"carrier: β¦and does NOT guard a root nobody declared",
|
|
414
|
+
at(dir, "paper-edit-guard", declaredWrite).exitCode === 0,
|
|
415
|
+
);
|
|
416
|
+
}
|
|
417
|
+
// (c) an explicit `null` is a KEYSTROKE, not an absence. `??` would read it as "undeclared"
|
|
418
|
+
// and silently substitute the default.
|
|
419
|
+
{
|
|
420
|
+
const dir = fixture({
|
|
421
|
+
paperlint: { [PAPERS_DIR_FIELD]: null },
|
|
422
|
+
});
|
|
423
|
+
const r = at(dir, "paper-edit-guard", harmless);
|
|
424
|
+
check(
|
|
425
|
+
`carrier: "${PAPERS_DIR_FIELD}": null REFUSES (rc=${r.exitCode})`,
|
|
426
|
+
r.exitCode === 2,
|
|
427
|
+
);
|
|
428
|
+
check(
|
|
429
|
+
"carrier: β¦and the refusal names the offending value",
|
|
430
|
+
/got null/.test(r.stderr),
|
|
431
|
+
);
|
|
432
|
+
}
|
|
433
|
+
// (d) the empty string β the value that makes every prefix test vacuously true.
|
|
434
|
+
{
|
|
435
|
+
const dir = fixture({
|
|
436
|
+
paperlint: { [PAPERS_DIR_FIELD]: "" },
|
|
437
|
+
});
|
|
438
|
+
const r = at(dir, "paper-edit-guard", harmless);
|
|
439
|
+
check(
|
|
440
|
+
`carrier: "${PAPERS_DIR_FIELD}": "" REFUSES (rc=${r.exitCode})`,
|
|
441
|
+
r.exitCode === 2,
|
|
442
|
+
);
|
|
443
|
+
check(
|
|
444
|
+
"carrier: β¦and explains that an empty prefix matches nothing",
|
|
445
|
+
/matches nothing/.test(r.stderr),
|
|
446
|
+
);
|
|
447
|
+
}
|
|
448
|
+
// (d2) the field's OLD name is refused and named β never read as a fallback, never ignored
|
|
449
|
+
// in favour of the default directory.
|
|
450
|
+
{
|
|
451
|
+
const dir = fixture({
|
|
452
|
+
paperlint: { [OLD_PAPERS_DIR_FIELD]: "writing/drafts" },
|
|
453
|
+
});
|
|
454
|
+
const r = at(dir, "paper-edit-guard", harmless);
|
|
455
|
+
check(
|
|
456
|
+
`carrier: the old field name "${OLD_PAPERS_DIR_FIELD}" REFUSES (rc=${r.exitCode})`,
|
|
457
|
+
r.exitCode === 2,
|
|
458
|
+
);
|
|
459
|
+
check(
|
|
460
|
+
"carrier: β¦and the refusal says what it was renamed to",
|
|
461
|
+
r.stderr.includes(
|
|
462
|
+
`"${OLD_PAPERS_DIR_FIELD}" was renamed to "${PAPERS_DIR_FIELD}" in package.json β "paperlint"`,
|
|
463
|
+
),
|
|
464
|
+
);
|
|
465
|
+
}
|
|
466
|
+
// (e) an unreadable package.json β the case that arrives by itself, mid-merge.
|
|
467
|
+
{
|
|
468
|
+
const dir = fixture(
|
|
469
|
+
{ paperlint: { [PAPERS_DIR_FIELD]: DECLARED } },
|
|
470
|
+
{ papers: DECLARED },
|
|
471
|
+
);
|
|
472
|
+
writeFileSync(join(dir, "package.json"), '{ "name": "c" <<<<<<< HEAD\n');
|
|
473
|
+
const r = at(dir, "paper-edit-guard", harmless);
|
|
474
|
+
check(
|
|
475
|
+
`carrier: an unparseable package.json REFUSES (rc=${r.exitCode})`,
|
|
476
|
+
r.exitCode === 2,
|
|
477
|
+
);
|
|
478
|
+
check(
|
|
479
|
+
"carrier: β¦and the refusal carries the way OUT (a file write, not a command)",
|
|
480
|
+
/Edit or Write/.test(r.stderr) &&
|
|
481
|
+
/do not pass through this gate/.test(r.stderr),
|
|
482
|
+
);
|
|
483
|
+
}
|
|
484
|
+
// (f) a trailing slash the consumer typed β `prefix + "/"` would become `//`, matching
|
|
485
|
+
// nothing. This defect made an earlier version of the guard a silent no-op.
|
|
486
|
+
{
|
|
487
|
+
const dir = fixture(
|
|
488
|
+
{ paperlint: { [PAPERS_DIR_FIELD]: DECLARED + "/" } },
|
|
489
|
+
{ papers: DECLARED },
|
|
490
|
+
);
|
|
491
|
+
check(
|
|
492
|
+
"carrier: a TRAILING SLASH in the declaration still guards",
|
|
493
|
+
at(dir, "paper-edit-guard", declaredWrite).exitCode === 2,
|
|
494
|
+
);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
499
|
+
// V. paper-skills-nudge β it must LAND, not merely fire
|
|
500
|
+
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
501
|
+
{
|
|
502
|
+
const dir = fixture({
|
|
503
|
+
paperlint: { [PAPERS_DIR_FIELD]: "docs/papers" },
|
|
504
|
+
});
|
|
505
|
+
const lands = (label, p) => {
|
|
506
|
+
const r = at(dir, "paper-skills-nudge", onEdit(p));
|
|
507
|
+
const ctx = injected(r);
|
|
508
|
+
check(
|
|
509
|
+
`nudge LANDS on ${label} (${r.stdout.length}b stdout)`,
|
|
510
|
+
ctx.length > 0,
|
|
511
|
+
);
|
|
512
|
+
check(
|
|
513
|
+
`nudge's text on ${label} is the checklist`,
|
|
514
|
+
/Pre-submit checklist/.test(ctx),
|
|
515
|
+
);
|
|
516
|
+
check(`nudge on ${label} exits 0`, r.exitCode === 0);
|
|
517
|
+
};
|
|
518
|
+
const silent = (label, p) => {
|
|
519
|
+
const r = at(dir, "paper-skills-nudge", onEdit(p));
|
|
520
|
+
check(
|
|
521
|
+
`nudge SILENT on ${label} (${r.stdout.length}b stdout, ${r.stderr.length}b stderr)`,
|
|
522
|
+
r.stdout.length === 0 && r.stderr.length === 0 && r.exitCode === 0,
|
|
523
|
+
);
|
|
524
|
+
};
|
|
525
|
+
lands("a relative paper.md", "docs/papers/alpha/paper.md");
|
|
526
|
+
lands("a .tex", "docs/papers/alpha/paper.tex");
|
|
527
|
+
// π΄ THE ABSOLUTE SPELLING. The live harness sends absolute paths; a helper that only builds
|
|
528
|
+
// relative ones let an anchored predecessor pass every test while being dead in production.
|
|
529
|
+
lands("an ABSOLUTE paper.md", join(dir, "docs/papers/alpha/paper.md"));
|
|
530
|
+
silent("a README under the papers root", "docs/papers/README.md");
|
|
531
|
+
silent("a file outside the papers root", "README.md");
|
|
532
|
+
// The advisory's own asymmetry: it goes quiet where the gate refuses.
|
|
533
|
+
{
|
|
534
|
+
const broken = fixture(
|
|
535
|
+
{ paperlint: { [PAPERS_DIR_FIELD]: null } },
|
|
536
|
+
{ papers: "papers" },
|
|
537
|
+
);
|
|
538
|
+
const r = at(
|
|
539
|
+
broken,
|
|
540
|
+
"paper-skills-nudge",
|
|
541
|
+
onEdit("docs/papers/alpha/paper.md"),
|
|
542
|
+
);
|
|
543
|
+
check(
|
|
544
|
+
"nudge is SILENT (not blocking) on an unusable declaration",
|
|
545
|
+
r.exitCode === 0 && r.stdout.length === 0,
|
|
546
|
+
);
|
|
547
|
+
// π΄ AND SILENT ON THE DEFAULT ROOT TOO β this is the half that catches `??`. With
|
|
548
|
+
// `declared ?? DEFAULT` an explicit `null` is read as Β«nothing was declaredΒ», the prefix
|
|
549
|
+
// becomes `papers/`, and the nudge starts firing about a tree the consumer never named.
|
|
550
|
+
// Without this case that mutation survives, because the assertion above passes either way.
|
|
551
|
+
const d = at(
|
|
552
|
+
broken,
|
|
553
|
+
"paper-skills-nudge",
|
|
554
|
+
onEdit("papers/alpha/paper.md"),
|
|
555
|
+
);
|
|
556
|
+
check(
|
|
557
|
+
"nudge: an explicit null is NOT read as an absence (no fallback to the default root)",
|
|
558
|
+
d.stdout.length === 0,
|
|
559
|
+
);
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
564
|
+
// VI. paper-status-gates β the tool runs, and only for a validated directory
|
|
565
|
+
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
566
|
+
{
|
|
567
|
+
const dir = fixture({
|
|
568
|
+
paperlint: { [PAPERS_DIR_FIELD]: "docs/papers" },
|
|
569
|
+
});
|
|
570
|
+
const fires = (label, p) => {
|
|
571
|
+
const r = at(dir, "paper-status-gates", onEdit(p));
|
|
572
|
+
check(
|
|
573
|
+
`gates FIRES on ${label} (${r.stderr.length}b stderr)`,
|
|
574
|
+
/fixture verdict line/.test(r.stderr),
|
|
575
|
+
);
|
|
576
|
+
check(`gates on ${label} exits 0`, r.exitCode === 0);
|
|
577
|
+
};
|
|
578
|
+
const silent = (label, p) => {
|
|
579
|
+
const r = at(dir, "paper-status-gates", onEdit(p));
|
|
580
|
+
check(
|
|
581
|
+
`gates SILENT on ${label}`,
|
|
582
|
+
r.stdout.length === 0 && r.stderr.length === 0 && r.exitCode === 0,
|
|
583
|
+
);
|
|
584
|
+
};
|
|
585
|
+
fires("a relative paper.md", "docs/papers/alpha/paper.md");
|
|
586
|
+
fires("an ABSOLUTE paper.md", join(dir, "docs/papers/alpha/paper.md"));
|
|
587
|
+
silent("a README under the papers root", "docs/papers/README.md");
|
|
588
|
+
silent("a file outside the papers root", "README.md");
|
|
589
|
+
// π΄ THE BOUNDARY IN `(?:^|/)`: a directory merely ENDING in the root's last segment must
|
|
590
|
+
// not smuggle a match. Without the boundary this is a hit.
|
|
591
|
+
silent(
|
|
592
|
+
"a look-alike root (`notdocs/papers/β¦`)",
|
|
593
|
+
"notdocs/papers/alpha/paper.md",
|
|
594
|
+
);
|
|
595
|
+
// A `.tex` whose directory name carries a shell metacharacter cannot reach a command.
|
|
596
|
+
silent(
|
|
597
|
+
"a directory name with a shell metacharacter",
|
|
598
|
+
"docs/papers/al;pha/paper.tex",
|
|
599
|
+
);
|
|
600
|
+
// π΄ The `??` discriminator again, for this hook's own copy of the carrier.
|
|
601
|
+
{
|
|
602
|
+
const broken = fixture(
|
|
603
|
+
{ paperlint: { [PAPERS_DIR_FIELD]: null } },
|
|
604
|
+
{ papers: "papers" },
|
|
605
|
+
);
|
|
606
|
+
check(
|
|
607
|
+
"gates: an explicit null is NOT read as an absence (no fallback to the default root)",
|
|
608
|
+
at(broken, "paper-status-gates", onEdit("papers/alpha/paper.md")).stderr
|
|
609
|
+
.length === 0,
|
|
610
|
+
);
|
|
611
|
+
}
|
|
612
|
+
// The declared root is regex-ESCAPED: `.` in a root must not act as a wildcard.
|
|
613
|
+
{
|
|
614
|
+
const dotted = fixture(
|
|
615
|
+
{ paperlint: { [PAPERS_DIR_FIELD]: "docs.v2/papers" } },
|
|
616
|
+
{ papers: "docs.v2/papers" },
|
|
617
|
+
);
|
|
618
|
+
check(
|
|
619
|
+
"gates: a root with a dot matches ITSELF",
|
|
620
|
+
/fixture verdict line/.test(
|
|
621
|
+
at(
|
|
622
|
+
dotted,
|
|
623
|
+
"paper-status-gates",
|
|
624
|
+
onEdit("docs.v2/papers/alpha/paper.md"),
|
|
625
|
+
).stderr,
|
|
626
|
+
),
|
|
627
|
+
);
|
|
628
|
+
check(
|
|
629
|
+
"gates: β¦and the dot is NOT a wildcard",
|
|
630
|
+
at(
|
|
631
|
+
dotted,
|
|
632
|
+
"paper-status-gates",
|
|
633
|
+
onEdit("docsXv2/papers/alpha/paper.md"),
|
|
634
|
+
).stderr.length === 0,
|
|
635
|
+
);
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
640
|
+
// VII. THE FORCED DUPLICATION β moved out, and why
|
|
641
|
+
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
642
|
+
// The three hooks spell out CONFIG_KEY and DEFAULT_PAPERS_ROOT because a compiled hook may
|
|
643
|
+
// import only `vigiles/hook`. That duplication is checked in `lib/paper-config.harness.mjs`,
|
|
644
|
+
// next to the module the values now come from.
|
|
645
|
+
//
|
|
646
|
+
// π΄ IT MOVED BECAUSE ITS SCOPE WAS WRONG, NOT ITS CODE. Living here, it could only ever
|
|
647
|
+
// compare hooks with hooks. Measured 2026-09-18: two more files declared the same constants β
|
|
648
|
+
// `eslint-rules/papers.mjs` and `skills/paper-pipeline/scripts/consumer.mjs`, neither of them
|
|
649
|
+
// a hook, neither of them under any import restriction, and neither of them inside the
|
|
650
|
+
// comparison. They could have drifted in silence. A check named after one part of a set
|
|
651
|
+
// cannot notice the rest of the set.
|
|
652
|
+
|
|
653
|
+
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
654
|
+
// VIII. paper-status-gates.sh β a TOOL, and it must refuse to be a hook
|
|
655
|
+
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
656
|
+
{
|
|
657
|
+
const dir = fixture({
|
|
658
|
+
paperlint: { [PAPERS_DIR_FIELD]: "docs/papers" },
|
|
659
|
+
});
|
|
660
|
+
const sh = (args) =>
|
|
661
|
+
runHook(
|
|
662
|
+
`bash ${JSON.stringify(join(HOOKS, "paper-status-gates.sh"))} ${args}`,
|
|
663
|
+
STOP,
|
|
664
|
+
{
|
|
665
|
+
cwd: dir,
|
|
666
|
+
env: { CLAUDE_PROJECT_DIR: dir },
|
|
667
|
+
},
|
|
668
|
+
);
|
|
669
|
+
const usage = sh("");
|
|
670
|
+
check(
|
|
671
|
+
"the .sh called with no --surface says it is a tool",
|
|
672
|
+
/a tool, not a hook/.test(usage.stderr),
|
|
673
|
+
);
|
|
674
|
+
check("β¦and still exits 0 (it must never block)", usage.exitCode === 0);
|
|
675
|
+
const good = sh("--surface alpha");
|
|
676
|
+
check(
|
|
677
|
+
"the .sh surfaces the verdict for a real paper",
|
|
678
|
+
/fixture verdict line/.test(good.stderr),
|
|
679
|
+
);
|
|
680
|
+
check(
|
|
681
|
+
"β¦and reads the papers root from the DECLARATION",
|
|
682
|
+
good.exitCode === 0,
|
|
683
|
+
);
|
|
684
|
+
const missing = sh("--surface nosuchpaper");
|
|
685
|
+
check(
|
|
686
|
+
"the .sh is silent for a directory with no status file",
|
|
687
|
+
missing.stderr.length === 0,
|
|
688
|
+
);
|
|
689
|
+
// π΄ THE NASTY NAME NEEDS A REAL DIRECTORY BEHIND IT, or the assertion cannot fail. First
|
|
690
|
+
// draft just passed `a;b` and checked for silence β but a name that names nothing is silent
|
|
691
|
+
// anyway, for want of a status file, so the mutation that DELETES the name check survived
|
|
692
|
+
// green. The fixture now contains a paper literally called `a;b` with a status file in it:
|
|
693
|
+
// with the check, silence; without it, the verdict comes out. Measured 2026-09-12.
|
|
694
|
+
mkdirSync(join(dir, "docs/papers/a;b"), { recursive: true });
|
|
695
|
+
writeFileSync(
|
|
696
|
+
join(dir, "docs/papers/a;b/PIPELINE-STATUS.md"),
|
|
697
|
+
"**Readiness verdict:** β
fixture verdict line\n",
|
|
698
|
+
);
|
|
699
|
+
check(
|
|
700
|
+
"the .sh's positive control: a plain name with a status file DOES surface",
|
|
701
|
+
/fixture verdict line/.test(sh("--surface alpha").stderr),
|
|
702
|
+
);
|
|
703
|
+
const nasty = sh("--surface 'a;b'");
|
|
704
|
+
check(
|
|
705
|
+
"the .sh re-rejects a name with a metacharacter EVEN THOUGH that paper exists",
|
|
706
|
+
nasty.stderr.length === 0 && nasty.exitCode === 0,
|
|
707
|
+
);
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
console.log(`hooks.harness: ${n} assertions passed`);
|
|
711
|
+
} finally {
|
|
712
|
+
for (const d of tmps) rmSync(d, { recursive: true, force: true });
|
|
713
|
+
}
|