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,287 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Harness over `action.yml` and the guard it calls.
|
|
3
|
+
* Run: `npx vigiles test action.harness.mjs`
|
|
4
|
+
*
|
|
5
|
+
* 🔴 WHAT IT REFUSES TO BE. A harness that greps `action.yml` for `--no-config-lookup` would pass
|
|
6
|
+
* on a file where the flag sits inside a comment, and would say nothing about whether the action
|
|
7
|
+
* BEHAVES. So: the YAML is read with a PARSER (`js-yaml`) and addressed as nodes, and the
|
|
8
|
+
* green-zero guard is exercised by CALLING it on fixtures — the same entry point the action calls.
|
|
9
|
+
*
|
|
10
|
+
* BOTH HALVES for every property: it fires on a planted defect AND stays quiet on a clean input.
|
|
11
|
+
* A guard only ever observed silent is indistinguishable from a dead one.
|
|
12
|
+
*/
|
|
13
|
+
import assert from "node:assert/strict";
|
|
14
|
+
import {
|
|
15
|
+
readFileSync,
|
|
16
|
+
writeFileSync,
|
|
17
|
+
mkdtempSync,
|
|
18
|
+
realpathSync,
|
|
19
|
+
} from "node:fs";
|
|
20
|
+
import { tmpdir } from "node:os";
|
|
21
|
+
import { dirname, join } from "node:path";
|
|
22
|
+
import { fileURLToPath } from "node:url";
|
|
23
|
+
import { spawnSync } from "node:child_process";
|
|
24
|
+
import * as yaml from "js-yaml";
|
|
25
|
+
import { guard } from "./scripts/eslint-report-guard.mjs";
|
|
26
|
+
|
|
27
|
+
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
28
|
+
const TMP = realpathSync(mkdtempSync(join(tmpdir(), "rpp-action-")));
|
|
29
|
+
const action = yaml.load(readFileSync(join(HERE, "action.yml"), "utf8"));
|
|
30
|
+
|
|
31
|
+
// ── I. SHAPE, read as nodes ───────────────────────────────────────────────────────────────────
|
|
32
|
+
assert.equal(action.runs.using, "composite", "the action must be composite");
|
|
33
|
+
const names = action.runs.steps.map((s) => s.name);
|
|
34
|
+
assert.equal(
|
|
35
|
+
action.runs.steps.length,
|
|
36
|
+
4,
|
|
37
|
+
`expected 4 steps, got ${action.runs.steps.length}: ${names}`,
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
const eslintStep = action.runs.steps.find((s) => s.name === "paperlint lint");
|
|
41
|
+
assert.ok(eslintStep, `no step named "paperlint lint" among: ${names}`);
|
|
42
|
+
|
|
43
|
+
// 🔴 THE ACTION MUST CALL THE PACKAGE'S OWN CLI, NOT ESLINT. This is not tidiness: while the step
|
|
44
|
+
// invoked `npx eslint` directly it was a SECOND implementation of the same job, and it had already
|
|
45
|
+
// drifted — the directory-structure check (a paper directory with no PIPELINE-STATUS.md gets zero
|
|
46
|
+
// rules and reports clean) lives in `bin/rpp.mjs`, so in CI it did not run at all. Any future edit
|
|
47
|
+
// that reaches past the CLI reintroduces exactly that gap, silently and greenly.
|
|
48
|
+
assert.match(
|
|
49
|
+
eslintStep.run,
|
|
50
|
+
/npx paperlint lint/,
|
|
51
|
+
"the lint step must call this package's own CLI — invoking eslint directly bypasses rpp.json and the structure check",
|
|
52
|
+
);
|
|
53
|
+
assert.doesNotMatch(
|
|
54
|
+
eslintStep.run,
|
|
55
|
+
/npx eslint/,
|
|
56
|
+
"the lint step must not invoke eslint directly — that is the bypass this step was fixed to remove",
|
|
57
|
+
);
|
|
58
|
+
// `--no-config-lookup` is deliberately absent and is NOT missing: the CLI builds its config with
|
|
59
|
+
// `overrideConfigFile: true`, so a consumer's nested config cannot reach the run. The guarantee
|
|
60
|
+
// moved from a flag into the code — assert the code, not the flag.
|
|
61
|
+
//
|
|
62
|
+
// 🔴 AND ASSERT IT IN THE FILE THAT HOLDS IT. This read said `bin/rpp.mjs` until the CLI moved to
|
|
63
|
+
// TypeScript; `bin/rpp.mjs` is now a loader shim and contains no such call, so the assertion went
|
|
64
|
+
// red on a move that changed no behaviour. That redness is the point — the same assertion written
|
|
65
|
+
// as a grep over "the CLI" would have kept passing against whichever file still matched, which is
|
|
66
|
+
// how a check quietly stops watching its subject.
|
|
67
|
+
assert.match(
|
|
68
|
+
readFileSync(new URL("./src/cli.ts", import.meta.url), "utf8"),
|
|
69
|
+
/overrideConfigFile:\s*true/,
|
|
70
|
+
"the CLI must pin its own config, or a consumer's nested config silently changes the rule set",
|
|
71
|
+
);
|
|
72
|
+
// The redirect into a report file is only sound while `--json` keeps stdout clean.
|
|
73
|
+
assert.match(
|
|
74
|
+
eslintStep.run,
|
|
75
|
+
/--json > "\$RUNNER_TEMP\/rpp\.json"/,
|
|
76
|
+
"the machine-readable report must be redirected whole — it is what the guard reads",
|
|
77
|
+
);
|
|
78
|
+
// The guard must be CALLED, and by file — an inline blob would be untestable.
|
|
79
|
+
assert.match(
|
|
80
|
+
eslintStep.run,
|
|
81
|
+
/eslint-report-guard\.mjs/,
|
|
82
|
+
"the lint step must hand its report to scripts/eslint-report-guard.mjs",
|
|
83
|
+
);
|
|
84
|
+
// ── I-b. THE STEP'S SHELL SEMANTICS, EXECUTED — not grepped ───────────────────────────────────
|
|
85
|
+
//
|
|
86
|
+
// 🔴 THE ASSERTION THAT STOOD HERE READ THE TEXT (`/RC=\$\?/`) AND WAS GREEN OVER A DEAD ERROR
|
|
87
|
+
// PATH. GitHub runs composite `shell: bash` as `bash --noprofile --norc -eo pipefail`. Under `-e`
|
|
88
|
+
// a bare `npx eslint` that exits non-zero ABORTS THE SCRIPT, so the guard on the next line never
|
|
89
|
+
// ran — and the file contained the characters `RC=$?` either way, so the text assertion could not
|
|
90
|
+
// tell the two apart. Every failure this action exists to explain (bad config, unreadable report,
|
|
91
|
+
// non-zero ESLint) lives on exactly that dead path.
|
|
92
|
+
//
|
|
93
|
+
// So: execute the REAL `run:` block under the REAL flags, with a stub standing in for `npx`, and
|
|
94
|
+
// require that the guard was reached and that ESLint's code came through it.
|
|
95
|
+
const stubbedStepRun = (stubRc) => {
|
|
96
|
+
const bin = realpathSync(mkdtempSync(join(tmpdir(), "rpp-bin-")));
|
|
97
|
+
writeFileSync(
|
|
98
|
+
join(bin, "npx"),
|
|
99
|
+
// Stub prints the report TO STDOUT, not to a file by the `-o` flag: the step no longer passes
|
|
100
|
+
// `-o`, it REDIRECTS output. So the stub also checks the redirect itself — if it disappears
|
|
101
|
+
// from the step, the file stays empty and the guard says "nothing was measured".
|
|
102
|
+
`#!/usr/bin/env bash\n` +
|
|
103
|
+
`printf '%s' "$REPORT_JSON"\n` +
|
|
104
|
+
`exit "$STUB_RC"\n`,
|
|
105
|
+
{ mode: 0o755 },
|
|
106
|
+
);
|
|
107
|
+
return spawnSync(
|
|
108
|
+
"bash",
|
|
109
|
+
["--noprofile", "--norc", "-eo", "pipefail", "-c", eslintStep.run],
|
|
110
|
+
{
|
|
111
|
+
cwd: TMP,
|
|
112
|
+
encoding: "utf8",
|
|
113
|
+
env: {
|
|
114
|
+
...process.env,
|
|
115
|
+
PATH: `${bin}:${process.env.PATH}`,
|
|
116
|
+
RUNNER_TEMP: TMP,
|
|
117
|
+
GITHUB_ACTION_PATH: HERE,
|
|
118
|
+
RPP_CONFIG: "eslint.config.mjs",
|
|
119
|
+
RPP_PATHS: ".",
|
|
120
|
+
RPP_MAXWARN: "-1",
|
|
121
|
+
STUB_RC: String(stubRc),
|
|
122
|
+
REPORT_JSON: JSON.stringify([{ filePath: "/x/a.md", messages: [] }]),
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
);
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
// FIRES-THROUGH: ESLint failed. This is the case the old text assertion could not see.
|
|
129
|
+
{
|
|
130
|
+
const r = stubbedStepRun(2);
|
|
131
|
+
assert.match(
|
|
132
|
+
r.stdout,
|
|
133
|
+
/linted 1 file\(s\)/,
|
|
134
|
+
"the guard must still run when ESLint exits non-zero — under `bash -e` a bare command aborts the step",
|
|
135
|
+
);
|
|
136
|
+
assert.equal(
|
|
137
|
+
r.status,
|
|
138
|
+
2,
|
|
139
|
+
"ESLint's failure code must come through the guard, not be masked to 1",
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
// QUIET HALF: ESLint succeeded — the guard runs and passes 0 through.
|
|
143
|
+
{
|
|
144
|
+
const r = stubbedStepRun(0);
|
|
145
|
+
assert.match(
|
|
146
|
+
r.stdout,
|
|
147
|
+
/linted 1 file\(s\)/,
|
|
148
|
+
"the guard must run on success too",
|
|
149
|
+
);
|
|
150
|
+
assert.equal(r.status, 0, "a clean run must stay green");
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// texcount is checked for EXISTENCE, not by the installer's exit code: an installer can put nothing
|
|
154
|
+
// in place and report success (measured in this corpus, 2026-09-01).
|
|
155
|
+
const pathStep = action.runs.steps.find((s) => /on PATH/i.test(s.name ?? ""));
|
|
156
|
+
assert.ok(pathStep, `no step verifying texcount is on PATH among: ${names}`);
|
|
157
|
+
assert.match(
|
|
158
|
+
pathStep.run,
|
|
159
|
+
/command -v texcount/,
|
|
160
|
+
"the PATH step must probe the binary itself",
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
for (const key of [
|
|
164
|
+
"config",
|
|
165
|
+
"paths",
|
|
166
|
+
"max-warnings",
|
|
167
|
+
"texcount",
|
|
168
|
+
"working-directory",
|
|
169
|
+
])
|
|
170
|
+
assert.ok(action.inputs[key], `input \`${key}\` is missing`);
|
|
171
|
+
|
|
172
|
+
// ── `paths` is REQUIRED, and the requirement is ENFORCED, not merely declared ─────────────────
|
|
173
|
+
// 🔴 GitHub does not enforce `required: true` for COMPOSITE actions. A caller who omits the
|
|
174
|
+
// input reaches the first step with an empty string and no error at all. So two separate things
|
|
175
|
+
// are asserted here, and neither implies the other: that the contract is DECLARED, and that a
|
|
176
|
+
// step exists which can actually fail on it.
|
|
177
|
+
assert.equal(
|
|
178
|
+
action.inputs.paths.required,
|
|
179
|
+
true,
|
|
180
|
+
"`paths` must be declared required",
|
|
181
|
+
);
|
|
182
|
+
assert.ok(
|
|
183
|
+
!("default" in action.inputs.paths),
|
|
184
|
+
"`paths` must have NO default — the old default `.` linted the whole checkout, so a caller " +
|
|
185
|
+
"who never chose a scope still got a green job over one",
|
|
186
|
+
);
|
|
187
|
+
const pathsGuard = action.runs.steps.find((s) =>
|
|
188
|
+
/paths was actually given/i.test(s.name ?? ""),
|
|
189
|
+
);
|
|
190
|
+
assert.ok(
|
|
191
|
+
pathsGuard,
|
|
192
|
+
`no step enforcing the \`paths\` contract among: ${names}`,
|
|
193
|
+
);
|
|
194
|
+
assert.match(
|
|
195
|
+
pathsGuard.run,
|
|
196
|
+
/exit 1/,
|
|
197
|
+
"the `paths` guard must FAIL, not warn — a declaration nobody checks is documentation",
|
|
198
|
+
);
|
|
199
|
+
// And it must come FIRST: checking scope after texlive setup would burn apt minutes
|
|
200
|
+
// for a call already wrong.
|
|
201
|
+
assert.equal(
|
|
202
|
+
action.runs.steps[0].name,
|
|
203
|
+
pathsGuard.name,
|
|
204
|
+
"the `paths` guard must run FIRST — checking scope after an apt install burns minutes on a " +
|
|
205
|
+
"call that was already wrong",
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
// ── II. BEHAVIOUR of the guard — both halves, on fixtures ─────────────────────────────────────
|
|
209
|
+
const fixture = (name, body) => {
|
|
210
|
+
const p = join(TMP, name);
|
|
211
|
+
writeFileSync(p, body);
|
|
212
|
+
return p;
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
// FIRES: zero files linted. The whole reason the guard exists.
|
|
216
|
+
{
|
|
217
|
+
const { code, lines } = guard(fixture("empty.json", "[]"), 0, {
|
|
218
|
+
paths: ".",
|
|
219
|
+
config: "c.mjs",
|
|
220
|
+
});
|
|
221
|
+
assert.equal(
|
|
222
|
+
code,
|
|
223
|
+
1,
|
|
224
|
+
"a report of zero linted files must FAIL, not pass as clean",
|
|
225
|
+
);
|
|
226
|
+
assert.match(lines.join("\n"), /linted ZERO files/);
|
|
227
|
+
}
|
|
228
|
+
// FIRES: the report is absent or unparsable — nothing was measured either way.
|
|
229
|
+
assert.equal(
|
|
230
|
+
guard(join(TMP, "absent.json"), 0).code,
|
|
231
|
+
1,
|
|
232
|
+
"an unreadable report must FAIL",
|
|
233
|
+
);
|
|
234
|
+
assert.equal(
|
|
235
|
+
guard(fixture("garbage.json", "{oops"), 0).code,
|
|
236
|
+
1,
|
|
237
|
+
"an unparsable report must FAIL",
|
|
238
|
+
);
|
|
239
|
+
assert.equal(
|
|
240
|
+
guard(fixture("object.json", '{"not":"an array"}'), 0).code,
|
|
241
|
+
1,
|
|
242
|
+
"a report that is not a result array must FAIL",
|
|
243
|
+
);
|
|
244
|
+
|
|
245
|
+
// QUIET: files were linted and ESLint was happy → ESLint's code passes through untouched.
|
|
246
|
+
{
|
|
247
|
+
const one = fixture(
|
|
248
|
+
"one.json",
|
|
249
|
+
JSON.stringify([
|
|
250
|
+
{
|
|
251
|
+
filePath: "/x/a.md",
|
|
252
|
+
messages: [
|
|
253
|
+
{ severity: 1, line: 3, column: 1, ruleId: "paper/x", message: "m" },
|
|
254
|
+
],
|
|
255
|
+
},
|
|
256
|
+
]),
|
|
257
|
+
);
|
|
258
|
+
assert.equal(
|
|
259
|
+
guard(one, 0).code,
|
|
260
|
+
0,
|
|
261
|
+
"a real run with findings must pass ESLint's own 0 through",
|
|
262
|
+
);
|
|
263
|
+
assert.equal(
|
|
264
|
+
guard(one, 2).code,
|
|
265
|
+
2,
|
|
266
|
+
"a real run must pass ESLint's failure code through, not mask it",
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
// QUIET: a file linted with NO findings is a legitimate clean run — the guard must not fire on it.
|
|
270
|
+
{
|
|
271
|
+
const clean = fixture(
|
|
272
|
+
"clean.json",
|
|
273
|
+
JSON.stringify([{ filePath: "/x/a.md", messages: [] }]),
|
|
274
|
+
);
|
|
275
|
+
const { code, lines } = guard(clean, 0);
|
|
276
|
+
assert.equal(
|
|
277
|
+
code,
|
|
278
|
+
0,
|
|
279
|
+
"one file linted with zero findings is CLEAN, not empty — the guard must be quiet",
|
|
280
|
+
);
|
|
281
|
+
assert.doesNotMatch(lines.join("\n"), /linted ZERO files/);
|
|
282
|
+
assert.match(lines.join("\n"), /linted 1 file\(s\) · 0 finding\(s\)/);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
console.log(
|
|
286
|
+
"✓ action.yml shape (parsed, not grepped) + guard behaviour, both halves — 20 assertions",
|
|
287
|
+
);
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mutation battery over `action.yml` and `scripts/eslint-report-guard.mjs`.
|
|
3
|
+
* Run: `node action.mutations.mjs` (not `vigiles test` — this is not a harness).
|
|
4
|
+
*
|
|
5
|
+
* 🔴 EVERY MUTATION CARRIES A "THE PATCH LANDED" ASSERTION: the target must occur EXACTLY ONCE,
|
|
6
|
+
* the mutant must be present in the file re-read from disk, the original text must be GONE, and
|
|
7
|
+
* after the rollback the contents must return byte for byte. In this corpus a green mutation has
|
|
8
|
+
* three times meant not "the defence holds" but "the patch never applied".
|
|
9
|
+
*
|
|
10
|
+
* A GREEN HARNESS UNDER A MUTATION is a finding about the TEST, not a conclusion about the defence.
|
|
11
|
+
* Rank the causes: the wrong test · the patch did not land · the set is too narrow · THE RUN WAS
|
|
12
|
+
* ALREADY RED BEFORE THE MUTATION. The last is why the baseline runs FIRST and its failure stops
|
|
13
|
+
* the battery.
|
|
14
|
+
*
|
|
15
|
+
* ⚠️ Mutation 1 is the load-bearing one and it is deliberately asymmetric: it sends the step back
|
|
16
|
+
* to calling `npx eslint` directly while LEAVING the words `paperlint lint` in the comments above. A
|
|
17
|
+
* harness that grepped the file would stay green; this one parses the YAML and addresses the step
|
|
18
|
+
* as a node, so it dies. That is the whole argument for the parser, made executable.
|
|
19
|
+
*
|
|
20
|
+
* It replaced a mutation over `--no-config-lookup`, which stopped having a target on 2026-09-18:
|
|
21
|
+
* the flag went away with the eslint call, and the guarantee it carried moved INTO the CLI
|
|
22
|
+
* (`overrideConfigFile: true`). The bypass it protected against is the same one — a consumer's
|
|
23
|
+
* nested config deciding the rule set — so the case moved rather than disappeared.
|
|
24
|
+
*/
|
|
25
|
+
import { readFileSync, writeFileSync } from "node:fs";
|
|
26
|
+
import { execFileSync } from "node:child_process";
|
|
27
|
+
|
|
28
|
+
const HARNESS = "action.harness.mjs";
|
|
29
|
+
|
|
30
|
+
if (process.env.MUTATIONS_REPORT_COVERAGE) {
|
|
31
|
+
console.log(`MUTATION-COVERS\t${HARNESS}`);
|
|
32
|
+
process.exit(0);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const ACTION = "action.yml";
|
|
36
|
+
const GUARD = "scripts/eslint-report-guard.mjs";
|
|
37
|
+
const PRISTINE = new Map(
|
|
38
|
+
[ACTION, GUARD].map((f) => [f, readFileSync(f, "utf8")]),
|
|
39
|
+
);
|
|
40
|
+
const restoreAll = () => {
|
|
41
|
+
for (const [f, body] of PRISTINE) writeFileSync(f, body);
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const M = [
|
|
45
|
+
[
|
|
46
|
+
ACTION,
|
|
47
|
+
"the step goes back around the CLI to eslint (the words `paperlint lint` left in the comments)",
|
|
48
|
+
"reinstate the bypass: rpp.json unread, the directory-structure check absent in CI, and a " +
|
|
49
|
+
"consumer's nested config free to decide the rule set — while a grep still finds `paperlint lint`",
|
|
50
|
+
"npx paperlint lint $RPP_PATHS",
|
|
51
|
+
'npx eslint --config "$RPP_CONFIG" $RPP_PATHS',
|
|
52
|
+
],
|
|
53
|
+
[
|
|
54
|
+
ACTION,
|
|
55
|
+
"the guard is no longer called",
|
|
56
|
+
"keep the lint run and drop the green-zero check — the job passes loudest when it measured nothing",
|
|
57
|
+
'node "$GITHUB_ACTION_PATH/scripts/eslint-report-guard.mjs" \\',
|
|
58
|
+
"true # MUT \\",
|
|
59
|
+
],
|
|
60
|
+
[
|
|
61
|
+
ACTION,
|
|
62
|
+
"texcount probed by the installer's exit code instead of by the binary",
|
|
63
|
+
"trust an installer that can put nothing in place and report success",
|
|
64
|
+
"command -v texcount >/dev/null || {",
|
|
65
|
+
"true || {",
|
|
66
|
+
],
|
|
67
|
+
[
|
|
68
|
+
GUARD,
|
|
69
|
+
"a report of ZERO linted files treated as clean",
|
|
70
|
+
"reinstate the exact indistinguishability the guard exists to break",
|
|
71
|
+
" if (res.length === 0) {",
|
|
72
|
+
" if (false) { /* MUT */",
|
|
73
|
+
],
|
|
74
|
+
[
|
|
75
|
+
GUARD,
|
|
76
|
+
"ESLint's return code masked",
|
|
77
|
+
"swallow a real ESLint failure and report success",
|
|
78
|
+
" return { code: eslintRc, lines };",
|
|
79
|
+
" return { code: 0, lines }; /* MUT */",
|
|
80
|
+
],
|
|
81
|
+
[
|
|
82
|
+
GUARD,
|
|
83
|
+
"an unparsable report treated as clean",
|
|
84
|
+
"let a missing or corrupt report pass — nothing was measured either way",
|
|
85
|
+
" lines.push(\n `::error::ESLint wrote no parsable report to ${reportPath} (rc=${eslintRc}). Nothing was measured.`,\n );\n return { code: 1, lines };",
|
|
86
|
+
" return { code: 0, lines }; /* MUT */",
|
|
87
|
+
],
|
|
88
|
+
];
|
|
89
|
+
|
|
90
|
+
const run = () => {
|
|
91
|
+
try {
|
|
92
|
+
return {
|
|
93
|
+
ok: true,
|
|
94
|
+
out: execFileSync("npx", ["vigiles", "test", HARNESS], {
|
|
95
|
+
encoding: "utf8",
|
|
96
|
+
stdio: "pipe",
|
|
97
|
+
}),
|
|
98
|
+
};
|
|
99
|
+
} catch (e) {
|
|
100
|
+
return { ok: false, out: `${e.stdout ?? ""}${e.stderr ?? ""}` };
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const base = run();
|
|
105
|
+
if (!base.ok) {
|
|
106
|
+
console.log(
|
|
107
|
+
"❌ THE HARNESS IS RED BEFORE ANY MUTATION — the battery cannot tell a killed mutation from that:\n" +
|
|
108
|
+
base.out.slice(-1200),
|
|
109
|
+
);
|
|
110
|
+
process.exit(1);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
let ok = 0;
|
|
114
|
+
let bad = 0;
|
|
115
|
+
const rows = [];
|
|
116
|
+
|
|
117
|
+
for (const [file, label, what, from, to] of M) {
|
|
118
|
+
const pristine = PRISTINE.get(file);
|
|
119
|
+
const hits = pristine.split(from).length - 1;
|
|
120
|
+
if (hits !== 1) {
|
|
121
|
+
console.log(
|
|
122
|
+
`❌ ${label}: TARGET ${hits === 0 ? "NOT FOUND" : `NOT UNIQUE (${hits})`} in ${file}`,
|
|
123
|
+
);
|
|
124
|
+
bad++;
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
writeFileSync(file, pristine.replace(from, to));
|
|
128
|
+
const on = readFileSync(file, "utf8");
|
|
129
|
+
if (!on.includes(to) || on.includes(from)) {
|
|
130
|
+
console.log(
|
|
131
|
+
`❌ ${label}: THE MUTATION DID NOT LAND (checked by re-reading ${file})`,
|
|
132
|
+
);
|
|
133
|
+
bad++;
|
|
134
|
+
restoreAll();
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
const res = run();
|
|
138
|
+
restoreAll();
|
|
139
|
+
if (readFileSync(file, "utf8") !== pristine) {
|
|
140
|
+
console.log(`❌ ${label}: THE ROLLBACK FAILED on ${file}`);
|
|
141
|
+
bad++;
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
if (!res.ok) {
|
|
145
|
+
ok++;
|
|
146
|
+
const why =
|
|
147
|
+
(res.out.match(/AssertionError[^\n]*?: ([^\n]*)/) ?? [])[1] ??
|
|
148
|
+
"(the harness died)";
|
|
149
|
+
rows.push([label, what, why.trim().slice(0, 100)]);
|
|
150
|
+
} else {
|
|
151
|
+
console.log(
|
|
152
|
+
`🔴 ${label}: THE HARNESS IS GREEN UNDER THE MUTATION — a finding about the TEST, not a conclusion about the defence`,
|
|
153
|
+
);
|
|
154
|
+
bad++;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
console.log("\n| property | mutation | what the harness died on |");
|
|
159
|
+
console.log("|---|---|---|");
|
|
160
|
+
for (const r of rows) console.log(`| ${r[0]} | ${r[1]} | ${r[2]} |`);
|
|
161
|
+
console.log(`\n${ok} mutation(s) killed, ${bad} problem(s)`);
|
|
162
|
+
process.exit(bad === 0 ? 0 : 1);
|
package/action.yml
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# Composite action: set up what the LaTeX rules need, then run `paperlint lint` over a consumer's corpus.
|
|
2
|
+
#
|
|
3
|
+
# 🔴 THIS ACTION CALLS THE PACKAGE'S OWN CLI. It used to invoke `npx eslint` directly, and that was
|
|
4
|
+
# not a style question — it was a second implementation of the same job, and it had already drifted:
|
|
5
|
+
# the CLI's directory-structure check (a paper directory missing PIPELINE-STATUS.md gets ZERO rules
|
|
6
|
+
# and reports clean) lived only in `bin/rpp.mjs`, so the gate existed locally and was ABSENT in CI,
|
|
7
|
+
# which is the one place it has to hold. The same bypass also meant `rpp.json` was never read and
|
|
8
|
+
# the caller still had to keep an ESLint flat config — the exact thing the CLI exists to remove.
|
|
9
|
+
#
|
|
10
|
+
# WHY AN ACTION AND NOT A DOCUMENTED `run:` BLOCK. Two steps have to happen in this order and
|
|
11
|
+
# neither is guessable from the outside: `texcount` must exist on PATH before linting starts, and
|
|
12
|
+
# the scope has to be checked before any install spends minutes on a call that was already wrong.
|
|
13
|
+
# A consumer who copies only the last step gets a green job over rules that silently degraded —
|
|
14
|
+
# see the `texcount` note below.
|
|
15
|
+
#
|
|
16
|
+
# 🔴 THE GREEN-ZERO GUARD IS PART OF THIS ACTION, NOT AN OPTIONAL EXTRA. `eslint` prints findings
|
|
17
|
+
# and exits 0 when there are none — and "no findings" is byte-identical to "not one rule was handed
|
|
18
|
+
# a single file". A rule whose glob matched nothing is never invoked, and a rule that is never
|
|
19
|
+
# invoked physically cannot report that. So this action asserts that ESLint linted at least one
|
|
20
|
+
# file and fails loudly when it did not. This does NOT replace a per-rule glob audit (the package's
|
|
21
|
+
# own `scripts/rules-see-files.mjs` does that, and it reads the package's config, not yours) — it closes the
|
|
22
|
+
# one failure mode that needs no config knowledge: nothing was linted at all.
|
|
23
|
+
name: paperlint — LaTeX prose gate
|
|
24
|
+
description: Install the toolchain the texcount/* rules need, run ESLint, and refuse to pass on an empty run.
|
|
25
|
+
|
|
26
|
+
inputs:
|
|
27
|
+
config:
|
|
28
|
+
description: >-
|
|
29
|
+
Path to `rpp.json`. Optional: `paperlint lint` finds it by walking up from the working directory,
|
|
30
|
+
the way eslint and tsc find theirs, so most callers never set this. Pass it only when the
|
|
31
|
+
file is somewhere the walk cannot reach. ⚠️ Until 2026-09-18 this input took a path to an
|
|
32
|
+
ESLint FLAT CONFIG and defaulted to `eslint.config.mjs`; a caller still passing one will
|
|
33
|
+
fail loudly on the first run rather than lint against it.
|
|
34
|
+
required: false
|
|
35
|
+
default: ""
|
|
36
|
+
paths:
|
|
37
|
+
description: >-
|
|
38
|
+
What to lint, as ESLint would take it on the command line. REQUIRED, and deliberately
|
|
39
|
+
without a default: the previous default was "." — the whole checkout — so a caller who
|
|
40
|
+
never thought about scope still got a green job, over whatever happened to be in the
|
|
41
|
+
repository. Scope is the one thing this action cannot guess.
|
|
42
|
+
required: true
|
|
43
|
+
max-warnings:
|
|
44
|
+
description: >-
|
|
45
|
+
Passed through to ESLint. The default -1 means warnings never fail the job: in this rule set
|
|
46
|
+
most findings are advisory by design, and a gate that fails on advice gets muted.
|
|
47
|
+
required: false
|
|
48
|
+
default: "-1"
|
|
49
|
+
texcount:
|
|
50
|
+
description: >-
|
|
51
|
+
Install the `texcount` binary before linting. 🔴 THIS PACKAGE SHIPS NO RULE THAT USES IT —
|
|
52
|
+
measured 2026-09-17, zero rules named `texcount/*` here. It is on by default for CONSUMERS
|
|
53
|
+
whose own config carries such rules (the author's does: `texcount/walls`,
|
|
54
|
+
`texcount/appendix-ratio`), because without the binary those rules DO NOT FAIL — they
|
|
55
|
+
quietly fall back to a weaker word count and the job stays green. If your config has no
|
|
56
|
+
such rule, set this to "false" and save the apt fetch.
|
|
57
|
+
required: false
|
|
58
|
+
default: "true"
|
|
59
|
+
working-directory:
|
|
60
|
+
description: Directory to run in.
|
|
61
|
+
required: false
|
|
62
|
+
default: "."
|
|
63
|
+
|
|
64
|
+
runs:
|
|
65
|
+
using: composite
|
|
66
|
+
steps:
|
|
67
|
+
# 🔴 `required: true` ABOVE IS A DECLARATION, NOT AN ENFORCEMENT. GitHub does not enforce
|
|
68
|
+
# required inputs for COMPOSITE actions — a caller who omits `paths` reaches this step with
|
|
69
|
+
# an empty string and no error of any kind. So the contract is checked here, where it can
|
|
70
|
+
# actually fail, rather than trusted where it is merely written down. Same reasoning as the
|
|
71
|
+
# green-zero guard below: the declaration and the check are different things.
|
|
72
|
+
- name: paths was actually given
|
|
73
|
+
shell: bash
|
|
74
|
+
env:
|
|
75
|
+
RPP_PATHS: ${{ inputs.paths }}
|
|
76
|
+
run: |
|
|
77
|
+
if [ -z "${RPP_PATHS// /}" ]; then
|
|
78
|
+
echo "::error::input \`paths\` is required and was empty. Name what to lint, e.g." \
|
|
79
|
+
"\`paths: papers\`. There is no default on purpose: linting the whole checkout" \
|
|
80
|
+
"by accident produces a green job over a scope nobody chose."
|
|
81
|
+
exit 1
|
|
82
|
+
fi
|
|
83
|
+
|
|
84
|
+
# texlive-extra-utils is what carries /usr/bin/texcount. Cached, because the apt fetch is the
|
|
85
|
+
# only network hop in this action and a slow mirror once cost a caller 14 minutes.
|
|
86
|
+
- name: texcount (for a CONSUMER's texcount/* rules — this package ships none)
|
|
87
|
+
if: inputs.texcount == 'true'
|
|
88
|
+
uses: awalsh128/cache-apt-pkgs-action@v1
|
|
89
|
+
with:
|
|
90
|
+
packages: texlive-extra-utils
|
|
91
|
+
version: "1.0"
|
|
92
|
+
|
|
93
|
+
# 🔴 EXIT=0 AND EMPTY OUTPUT IS NOT "CLEAN" — an installer can put nothing in place and report
|
|
94
|
+
# success. So the binary is checked for existence, not the installer's return code.
|
|
95
|
+
- name: texcount is actually on PATH
|
|
96
|
+
if: inputs.texcount == 'true'
|
|
97
|
+
shell: bash
|
|
98
|
+
run: |
|
|
99
|
+
command -v texcount >/dev/null || {
|
|
100
|
+
echo "::error::texcount is not on PATH after the install step. The texcount/* rules would" \
|
|
101
|
+
"silently fall back to a weaker word count, and this job would still be green."
|
|
102
|
+
exit 1
|
|
103
|
+
}
|
|
104
|
+
texcount -version | head -1
|
|
105
|
+
|
|
106
|
+
# ONE invocation, JSON to disk, exit code preserved and then handed to the guard. Linting twice
|
|
107
|
+
# (once for humans, once for the machine) would read the corpus twice and could disagree with
|
|
108
|
+
# itself. The guard lives in `scripts/eslint-report-guard.mjs` rather than inline here, because
|
|
109
|
+
# an inline `node -e` blob cannot be reached by a test.
|
|
110
|
+
#
|
|
111
|
+
# 🔴 THE REDIRECT IS ONLY SAFE BECAUSE `--json` KEEPS stdout CLEAN. `paperlint lint` prints the name
|
|
112
|
+
# of the config it discovered; in `--json` mode that line goes to stderr, precisely so a pipe
|
|
113
|
+
# or a redirect receives one parsable document. That was a real defect for an hour on 18-09 —
|
|
114
|
+
# the line sat on stdout in front of the array — and it was found by wiring this step up, not
|
|
115
|
+
# by a test, because the harness had been WORKING AROUND it (slicing the first line off).
|
|
116
|
+
#
|
|
117
|
+
# `--no-config-lookup` is gone with the eslint call and is not missing: the CLI builds its
|
|
118
|
+
# config with `overrideConfigFile: true`, so a consumer's nested config cannot reach the run
|
|
119
|
+
# at all. The guarantee moved from a flag into the code.
|
|
120
|
+
- name: paperlint lint
|
|
121
|
+
shell: bash
|
|
122
|
+
working-directory: ${{ inputs.working-directory }}
|
|
123
|
+
env:
|
|
124
|
+
RPP_CONFIG: ${{ inputs.config }}
|
|
125
|
+
RPP_PATHS: ${{ inputs.paths }}
|
|
126
|
+
RPP_MAXWARN: ${{ inputs.max-warnings }}
|
|
127
|
+
run: |
|
|
128
|
+
# 🔴 `|| RC=$?` IS LOAD-BEARING, NOT STYLE. GitHub runs composite `shell: bash` as
|
|
129
|
+
# `bash --noprofile --norc -eo pipefail`, so a bare `npx paperlint lint` that exits non-zero
|
|
130
|
+
# ABORTS THE SCRIPT and the guard below never runs — measured, not reasoned: the line
|
|
131
|
+
# after a failing command did not print. That is the whole error path of this action
|
|
132
|
+
# (a bad config, an unreadable report, findings), so under `-e` the guard was reachable
|
|
133
|
+
# ONLY on success, which is the one case it was not written for.
|
|
134
|
+
RC=0
|
|
135
|
+
npx paperlint lint $RPP_PATHS ${RPP_CONFIG:+--config "$RPP_CONFIG"} \
|
|
136
|
+
--max-warnings="$RPP_MAXWARN" --json > "$RUNNER_TEMP/rpp.json" || RC=$?
|
|
137
|
+
node "$GITHUB_ACTION_PATH/scripts/eslint-report-guard.mjs" \
|
|
138
|
+
"$RUNNER_TEMP/rpp.json" "$RC" "$RPP_PATHS" "${RPP_CONFIG:-rpp.json}"
|
package/bin/rpp.mjs
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* SHIM. The real CLI lives in `src/*.ts` and builds to `dist/`; this file stays put and
|
|
4
|
+
* remains JavaScript deliberately.
|
|
5
|
+
*
|
|
6
|
+
* 🔴 WHY NOT MOVE `bin` TO `dist/cli.js`. This path is a PUBLIC CONTRACT, and it already has
|
|
7
|
+
* two consumers outside the package:
|
|
8
|
+
* 1. `plugin/hooks/hooks.json` calls
|
|
9
|
+
* node "${CLAUDE_PROJECT_DIR}/node_modules/paperlint/bin/rpp.mjs" hook <name>
|
|
10
|
+
* — the hook wiring was fixed exactly because it addressed a file the consumer did not have;
|
|
11
|
+
* changing it the next day would be the same class of error;
|
|
12
|
+
* 2. docs/configuration.md documents `import { buildConfig } from "paperlint/bin/rpp.mjs"`.
|
|
13
|
+
* A move would cost both, and yield zero: a file name says nothing about what language it is in.
|
|
14
|
+
*
|
|
15
|
+
* 🔴 AND WHY THE FAILURE HERE IS LOUD. A consumer who installed the package from the registry
|
|
16
|
+
* has `dist/` in the tarball ready — this IS MEASURED (`npm pack --dry-run`: 16 files `dist/*`), and
|
|
17
|
+
* it is kept by the `files` allowlist in the manifest. The list is not for show: without the `files` field,
|
|
18
|
+
* npm used `.gitignore` as the exclusion list, and `dist/` in it would eject the build from the package —
|
|
19
|
+
* 376 files became 360, and the consumer got exactly this refusal. The check stays for two cases where
|
|
20
|
+
* `dist/` can genuinely be missing: a clone before `npm run build` and installing the package as a git
|
|
21
|
+
* dependency with `--ignore-scripts` (so `prepare` does not run). Without it, both would yield `ERR_MODULE_NOT_FOUND`
|
|
22
|
+
* from deep in the loader: a message about a file the consumer did not write, and no hint what to do.
|
|
23
|
+
*/
|
|
24
|
+
import { existsSync } from "node:fs";
|
|
25
|
+
import { fileURLToPath } from "node:url";
|
|
26
|
+
|
|
27
|
+
const dist = new URL("../dist/cli.js", import.meta.url);
|
|
28
|
+
if (!existsSync(fileURLToPath(dist))) {
|
|
29
|
+
process.stderr.write(
|
|
30
|
+
`paperlint: not built — ${fileURLToPath(dist)} missing.\n` +
|
|
31
|
+
`The package is written in TypeScript; in the registry tarball \`dist/\` already lies built, so\n` +
|
|
32
|
+
`you reach here from a repository clone or a git dependency installed with\n` +
|
|
33
|
+
`\`--ignore-scripts\` (so the \`prepare\` step is skipped).\n` +
|
|
34
|
+
`In a clone: \`npm run build\`. For a git dependency: reinstall without \`--ignore-scripts\`.\n`,
|
|
35
|
+
);
|
|
36
|
+
process.exit(2);
|
|
37
|
+
}
|
|
38
|
+
export * from "../dist/cli.js";
|
|
39
|
+
const { run, isMain } = await import("../dist/cli.js");
|
|
40
|
+
// The question is about THIS file, not `dist/cli.js`: `bin` is the target of a symlink from
|
|
41
|
+
// `node_modules/.bin`, and `isMain` can compare it to the real path. For `cli.js` itself
|
|
42
|
+
// the answer is now always "no" — it is imported, not invoked.
|
|
43
|
+
if (isMain(import.meta.url)) process.exit(await run(process.argv.slice(2)));
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The ref `paperlint init` pins the GitHub Action to, decided from the version of the package that is
|
|
3
|
+
* running. semantic-release tags every release `v<version>` on the commit it publishes, so the
|
|
4
|
+
* action at that tag is the same code as the installed package.
|
|
5
|
+
*
|
|
6
|
+
* Only a plain `X.Y.Z` has such a tag. A git checkout or `npm link` carries the unreleased
|
|
7
|
+
* placeholder `0.0.0-semantically-released`, a version that could not be read is `undefined`, and
|
|
8
|
+
* anything else is not known to be tagged: those return `null` ("no pin known"), and the caller
|
|
9
|
+
* keeps the placeholder. A wrong tag written confidently is worse than an obvious placeholder.
|
|
10
|
+
*/
|
|
11
|
+
export declare function actionRef(version: string | undefined): string | null;
|
|
12
|
+
//# sourceMappingURL=action-ref.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"action-ref.d.ts","sourceRoot":"","sources":["../src/action-ref.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,wBAAgB,SAAS,CAAC,OAAO,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,IAAI,CAIpE"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The ref `paperlint init` pins the GitHub Action to, decided from the version of the package that is
|
|
3
|
+
* running. semantic-release tags every release `v<version>` on the commit it publishes, so the
|
|
4
|
+
* action at that tag is the same code as the installed package.
|
|
5
|
+
*
|
|
6
|
+
* Only a plain `X.Y.Z` has such a tag. A git checkout or `npm link` carries the unreleased
|
|
7
|
+
* placeholder `0.0.0-semantically-released`, a version that could not be read is `undefined`, and
|
|
8
|
+
* anything else is not known to be tagged: those return `null` ("no pin known"), and the caller
|
|
9
|
+
* keeps the placeholder. A wrong tag written confidently is worse than an obvious placeholder.
|
|
10
|
+
*/
|
|
11
|
+
export function actionRef(version) {
|
|
12
|
+
return version !== undefined && /^\d+\.\d+\.\d+$/.test(version)
|
|
13
|
+
? `v${version}`
|
|
14
|
+
: null;
|
|
15
|
+
}
|
|
16
|
+
//# sourceMappingURL=action-ref.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"action-ref.js","sourceRoot":"","sources":["../src/action-ref.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,MAAM,UAAU,SAAS,CAAC,OAA2B;IACnD,OAAO,OAAO,KAAK,SAAS,IAAI,iBAAiB,CAAC,IAAI,CAAC,OAAO,CAAC;QAC7D,CAAC,CAAC,IAAI,OAAO,EAAE;QACf,CAAC,CAAC,IAAI,CAAC;AACX,CAAC"}
|