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
package/dist/cli.js
ADDED
|
@@ -0,0 +1,951 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* `paperlint lint [paths…]` — run every rule over the corpus of papers.
|
|
4
|
+
*
|
|
5
|
+
* 🔴 WHY THIS UTILITY EXISTS. Before it, "installation" meant: install the package AND WRITE BY
|
|
6
|
+
* HAND sixty lines of ESLint flat config, listing ten rules, three languages and four `files`
|
|
7
|
+
* blocks. That is, the tool dumped its own implementation onto the user: to count the bytes of a
|
|
8
|
+
* pdf you first had to learn what `language: "tex/latex"` is. The rules still run under ESLint —
|
|
9
|
+
* but that is INTERNAL machinery, and you no longer need to know it in order to run them.
|
|
10
|
+
*
|
|
11
|
+
* Two entry points into the tool, and both are whole now:
|
|
12
|
+
* npx paperlint lint ← here
|
|
13
|
+
* uses: zernie/research-paper-pipeline@<sha> ← action.yml
|
|
14
|
+
*
|
|
15
|
+
* ⚠️ THE BOUNDARY THIS UTILITY HAS NO RIGHT TO ERASE: the consumer's data stays with the consumer.
|
|
16
|
+
* The typography debt, the marker of the author-list check run, the field dictionary — all of that
|
|
17
|
+
* is about ONE corpus, and wiring it into the package would repeat the defect that put the path
|
|
18
|
+
* `.claude/skills/verify-citations/...` into a rule's message. So they live in the consumer's
|
|
19
|
+
* `rpp.json`.
|
|
20
|
+
*
|
|
21
|
+
* 🔴 WHY THE COMMAND IS CALLED `lint` AND NOT `check`. It does exactly what everyone else calls by
|
|
22
|
+
* that word: reads files, changes nothing, prints findings, exits non-zero. `check` is taken in the
|
|
23
|
+
* ecosystem by another meaning — `cargo check`, `tsc --noEmit`, `npm run check` — it means "build,
|
|
24
|
+
* but do not emit the artifact", that is, half of a BUILD. A package whose paper build comes first
|
|
25
|
+
* has no right to occupy that word with a linter. `check` stays as an alias and prints what
|
|
26
|
+
* replaced it: silently breaking someone else's workflow is worse than asking them to fix a line.
|
|
27
|
+
*/
|
|
28
|
+
import { ESLint } from "eslint";
|
|
29
|
+
import { readFileSync, existsSync } from "node:fs";
|
|
30
|
+
import { createRequire } from "node:module";
|
|
31
|
+
import { spawnSync } from "node:child_process";
|
|
32
|
+
import { fileURLToPath } from "node:url";
|
|
33
|
+
import { join, dirname, resolve, relative, basename, sep } from "node:path";
|
|
34
|
+
import markdown from "@eslint/markdown";
|
|
35
|
+
// Types come from consumer.d.mts beside it, the same arrangement as lib/paper-config.d.mts.
|
|
36
|
+
import { isMain } from "../skills/paper-pipeline/scripts/consumer.mjs";
|
|
37
|
+
export { isMain };
|
|
38
|
+
import { checkStructure, formatStructure, asEslintResults, } from "./structure.js";
|
|
39
|
+
import { buildPapers, papersIn, anyFailed, remedyFor, readFacts, MAIN, } from "./build.js";
|
|
40
|
+
import { prepareEngine } from "./build-engine.js";
|
|
41
|
+
import { runToolchain } from "./toolchain.js";
|
|
42
|
+
import { banalInstaller, parseBanalSettings } from "./adapters/banal/index.js";
|
|
43
|
+
import { curlDownload } from "./adapters/curl/index.js";
|
|
44
|
+
import { hostDirs, nodeAdapters } from "./adapters/node/index.js";
|
|
45
|
+
import { mergeRequirements, requirementsFor, NO_REQUIREMENTS, } from "./tex-requirements.js";
|
|
46
|
+
import { doctor } from "./doctor.js";
|
|
47
|
+
import { init, processInteractivity, askOnTerminal } from "./init.js";
|
|
48
|
+
import { DEFAULT_FORMAT, FORMATS, isFormat, newPaper, reportNewPaper, } from "./new-paper.js";
|
|
49
|
+
// The one source for the consumer's config key lives in the .mjs half of the package (the ESLint
|
|
50
|
+
// rules and the skill scripts import it too); its types are in lib/paper-config.d.mts.
|
|
51
|
+
import { CONFIG_KEY, LEGACY_CONFIG_KEY, LEGACY_KEY_MESSAGE, PAPERS_DIR_FIELD, SETTINGS_KEYS, declaredSettings, renamedFieldMessage, } from "../lib/paper-config.mjs";
|
|
52
|
+
import { parseRuleBlocks, shippedRuleIds, unknownKeys, } from "./rules-config.js";
|
|
53
|
+
export { init };
|
|
54
|
+
export { nextSteps } from "./init.js";
|
|
55
|
+
// @ts-expect-error — an ESLint rule in .mjs, it has no types
|
|
56
|
+
import paperStages from "../eslint-rules/paper-stages.mjs";
|
|
57
|
+
// @ts-expect-error — an ESLint rule in .mjs, it has no types
|
|
58
|
+
import researchQuestion from "../eslint-rules/paper-research-question.mjs";
|
|
59
|
+
// @ts-expect-error — an ESLint rule in .mjs, it has no types
|
|
60
|
+
import typography from "../eslint-rules/paper-typography.mjs";
|
|
61
|
+
// @ts-expect-error — an ESLint rule in .mjs, it has no types
|
|
62
|
+
import texBuild from "../eslint-rules/tex-build.mjs";
|
|
63
|
+
// @ts-expect-error — an ESLint rule in .mjs, it has no types
|
|
64
|
+
import docFields from "../eslint-rules/doc-fields.mjs";
|
|
65
|
+
// @ts-expect-error — an ESLint rule in .mjs, it has no types
|
|
66
|
+
import findingsCause from "../eslint-rules/review-findings-cause.mjs";
|
|
67
|
+
// @ts-expect-error — an ESLint rule in .mjs, it has no types
|
|
68
|
+
import pdfRules from "../eslint-rules/pdf-last-page-balance.mjs";
|
|
69
|
+
const USAGE = `paperlint — machine-checkable gates for a paper kept in git
|
|
70
|
+
|
|
71
|
+
npx paperlint init [dir] set the project up: detect the papers directory, declare it
|
|
72
|
+
in package.json, link the skills, wire the hooks into
|
|
73
|
+
.claude/settings.json, offer the CI step, report what is missing
|
|
74
|
+
npx paperlint new <name> [--format tex|md]
|
|
75
|
+
create <papers>/<name>/ from the template; never overwrites,
|
|
76
|
+
on an existing folder adds only the missing files, then lints it
|
|
77
|
+
npx paperlint lint [paths…] run every rule over your papers
|
|
78
|
+
npx paperlint build <paper> | --all
|
|
79
|
+
compile paper.tex to paper.pdf: pdflatex and bibtex, rerun until
|
|
80
|
+
the references settle. Prints the plan first; a build.sh in the
|
|
81
|
+
paper directory is ignored (--dry-run: print the plan only).
|
|
82
|
+
Compiles with paperlint's TeX Live, else one on PATH that has every
|
|
83
|
+
package the venue declares; on a terminal it offers to install
|
|
84
|
+
one, without a terminal it stops and names \`npx paperlint toolchain\`
|
|
85
|
+
npx paperlint toolchain [--check] install TeX Live with every package the venue profiles declare
|
|
86
|
+
into ~/.cache/rpp/texlive (RPP_TEXLIVE_DIR overrides); a second
|
|
87
|
+
run does nothing. --check: report what is missing, change nothing
|
|
88
|
+
npx paperlint doctor say what is actually wired — and what only LOOKS wired
|
|
89
|
+
npx paperlint hook <name> run an editor hook (.claude/settings.json calls this)
|
|
90
|
+
npx paperlint --help
|
|
91
|
+
|
|
92
|
+
init:
|
|
93
|
+
--yes, -y ask nothing, take every default. Without a terminal on stdin AND stdout,
|
|
94
|
+
or with CI set, nothing is asked either
|
|
95
|
+
--no-hooks do not wire the hooks (the default without a human is to wire them)
|
|
96
|
+
--paper <name> create this paper too (without a human, the only way init creates one)
|
|
97
|
+
--format tex|md the new paper's source format; default tex
|
|
98
|
+
|
|
99
|
+
lint:
|
|
100
|
+
npx paperlint lint [paths…] [--config <file.json>] [--json]
|
|
101
|
+
|
|
102
|
+
<paths…> where your papers live, e.g. papers. Optional ONLY because the declaration
|
|
103
|
+
names it — one of the two must name the scope. There is no default
|
|
104
|
+
of ".": linting whatever happens to be in the checkout is how a green
|
|
105
|
+
report over a scope nobody chose gets produced.
|
|
106
|
+
--config <file> read the settings from this file instead of the discovered one
|
|
107
|
+
--json machine-readable findings on stdout, nothing else on it
|
|
108
|
+
--max-warnings <n> fail when warnings exceed n. Default -1: warnings never fail, because
|
|
109
|
+
most findings here are advisory and a gate that fails on advice gets muted
|
|
110
|
+
|
|
111
|
+
settings — the \`paperlint\` key of your package.json, found by walking up from the
|
|
112
|
+
current directory, the way every other tool in the stack finds its config. \`rpp.json\` is still
|
|
113
|
+
read as a deprecated fallback and the run says so. \`papersDir\` is required; the rest is optional:
|
|
114
|
+
|
|
115
|
+
"paperlint": {
|
|
116
|
+
"papersDir": "papers",
|
|
117
|
+
"authorListCommand": "node scripts/bib-authors.mjs",
|
|
118
|
+
"typographyDebt": { "papers/my-paper": { "sectionSign": 12 } },
|
|
119
|
+
"docFields": { "read": { "values": ["full", "abstract", "none"] } },
|
|
120
|
+
"reviewSince": "2026-08-23",
|
|
121
|
+
"minFindings": 3,
|
|
122
|
+
"causeMarker": "Cause:",
|
|
123
|
+
"rules": [ { "files": ["papers/my-paper/**"],
|
|
124
|
+
"rules": { "pdf/last-page-balance": "error" } } ]
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
"rules" takes ESLint flat-config blocks (files, ignores, rules), appended after rpp's own, with
|
|
128
|
+
files relative to the file holding the settings. Optional rules (off unless turned on there):
|
|
129
|
+
pdf/last-page-balance. An unknown key, anywhere in the settings, is an error.
|
|
130
|
+
`;
|
|
131
|
+
/** The config the user would otherwise write by hand. The data comes from `opts`, the mechanism is here. */
|
|
132
|
+
export function buildConfig(opts = {}, texLanguage) {
|
|
133
|
+
const paperRules = { ...researchQuestion.rules, ...typography.rules };
|
|
134
|
+
const typographyOpt = ["warn", { debt: opts.typographyDebt ?? {} }];
|
|
135
|
+
const md = {
|
|
136
|
+
language: "markdown/gfm",
|
|
137
|
+
languageOptions: { frontmatter: "yaml" },
|
|
138
|
+
};
|
|
139
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- #49: replace with a real type
|
|
140
|
+
const cfg = [
|
|
141
|
+
// 🔴 THE PROJECT'S PAPER TEMPLATE IS NOT A PAPER. `paperlint new` reads `<papers>/.template/`, and
|
|
142
|
+
// its files carry every marker a paper does. Flat config does NOT ignore dot-directories by
|
|
143
|
+
// default (only `node_modules/` and `.git/`), so without this block `paperlint lint` would lint the
|
|
144
|
+
// template as a paper — and a richer template with placeholder stages would fail the run.
|
|
145
|
+
{ ignores: ["**/.template/"] },
|
|
146
|
+
// The `pdf` plugin is registered for EVERY file, and its rule is on for none. A consumer's
|
|
147
|
+
// block (`rules`, appended below) turns it on for a glob that also matches markdown files;
|
|
148
|
+
// with the plugin defined only beside `paper.tex`, ESLint would refuse those files with
|
|
149
|
+
// "could not find plugin". The rule itself acts on `paper.tex` only.
|
|
150
|
+
{ plugins: { pdf: pdfRules } },
|
|
151
|
+
{
|
|
152
|
+
files: ["**/PIPELINE-STATUS.md"],
|
|
153
|
+
plugins: { markdown, paper: paperStages },
|
|
154
|
+
...md,
|
|
155
|
+
rules: {
|
|
156
|
+
"paper/stages": "error",
|
|
157
|
+
"paper/source": "error",
|
|
158
|
+
"paper/author-list": [
|
|
159
|
+
"warn",
|
|
160
|
+
opts.authorListCommand ? { command: opts.authorListCommand } : {},
|
|
161
|
+
],
|
|
162
|
+
},
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
files: ["**/paper.md", "**/draft.md"],
|
|
166
|
+
plugins: { markdown, paper: { rules: paperRules } },
|
|
167
|
+
...md,
|
|
168
|
+
rules: {
|
|
169
|
+
"paper/research-question": "warn",
|
|
170
|
+
"paper/typography": typographyOpt,
|
|
171
|
+
},
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
files: ["**/reviews/*.md"],
|
|
175
|
+
plugins: {
|
|
176
|
+
markdown,
|
|
177
|
+
review: { rules: { ...findingsCause.rules } },
|
|
178
|
+
doc: docFields,
|
|
179
|
+
},
|
|
180
|
+
...md,
|
|
181
|
+
rules: {
|
|
182
|
+
/*
|
|
183
|
+
* 🔴 THE DEFAULT IS ENGLISH SINCE 2026-09-17. It used to be the Russian word for "Cause:" —
|
|
184
|
+
* a Russian word in a package whose interface is English. An `error`-level rule demanded
|
|
185
|
+
* that a person put Cyrillic into their own file, and there was nothing to change the
|
|
186
|
+
* marker with: `causeMarker` was not threaded through the CLI at all. The only way out was
|
|
187
|
+
* to abandon the command and assemble the ESLint config by hand — that is, the defect
|
|
188
|
+
* pushed you onto exactly the path the utility frees you from.
|
|
189
|
+
* The Russian marker stays EXPRESSIBLE, but now as a value, not as the default.
|
|
190
|
+
*/
|
|
191
|
+
"review/findings-cause": [
|
|
192
|
+
"error",
|
|
193
|
+
{
|
|
194
|
+
minFindings: opts.minFindings ?? 3,
|
|
195
|
+
...(opts.causeMarker ? { causeMarker: opts.causeMarker } : {}),
|
|
196
|
+
...(opts.reviewSince ? { sinceCreated: opts.reviewSince } : {}),
|
|
197
|
+
},
|
|
198
|
+
],
|
|
199
|
+
...(opts.docFields
|
|
200
|
+
? { "doc/fields": ["warn", { fields: opts.docFields }] }
|
|
201
|
+
: {}),
|
|
202
|
+
},
|
|
203
|
+
},
|
|
204
|
+
];
|
|
205
|
+
// `.tex` only if the language loaded: it pulls in the LaTeX parser, and dying because of it on a
|
|
206
|
+
// corpus without a single `.tex` would be refusing to work where work is possible.
|
|
207
|
+
if (texLanguage)
|
|
208
|
+
cfg.push({
|
|
209
|
+
files: ["**/paper.tex"],
|
|
210
|
+
plugins: {
|
|
211
|
+
tex: { languages: { latex: texLanguage }, rules: texBuild },
|
|
212
|
+
paper: { rules: paperRules },
|
|
213
|
+
},
|
|
214
|
+
language: "tex/latex",
|
|
215
|
+
rules: {
|
|
216
|
+
"paper/research-question": "warn",
|
|
217
|
+
"paper/typography": typographyOpt,
|
|
218
|
+
"tex/future-promise": "warn",
|
|
219
|
+
"tex/acm-frontmatter-override": "error",
|
|
220
|
+
},
|
|
221
|
+
});
|
|
222
|
+
// The consumer's own blocks, LAST, so a later block wins — ESLint's rule. Parsed by
|
|
223
|
+
// `readConfig`; each carries the settings file's directory as its `basePath`.
|
|
224
|
+
cfg.push(...(opts.rules ?? []));
|
|
225
|
+
return cfg;
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* The rule ids a consumer may name in `rules`: every rule rpp's own config defines, read off that
|
|
229
|
+
* config rather than listed again. `@eslint/markdown` is a dependency's plugin, not rpp's.
|
|
230
|
+
*/
|
|
231
|
+
export const SHIPPED_RULES = shippedRuleIds(buildConfig({}, { sentinel: "tex language" }), [markdown]);
|
|
232
|
+
/** Whether a rule entry (`"error"`, `2`, `["warn", {…}]`) turns the rule on. */
|
|
233
|
+
const isOn = (entry) => {
|
|
234
|
+
const sev = Array.isArray(entry) ? entry[0] : entry;
|
|
235
|
+
return sev !== undefined && sev !== "off" && sev !== 0;
|
|
236
|
+
};
|
|
237
|
+
/**
|
|
238
|
+
* Rules paperlint ships and turns on for no file itself — the ones a consumer opts into with `rules`.
|
|
239
|
+
* Derived: every shipped rule that no block of rpp's own config names.
|
|
240
|
+
*/
|
|
241
|
+
export const OPTIONAL_RULES = new Set([...SHIPPED_RULES].filter((id) => !buildConfig({}, { sentinel: "tex language" }).some((b) => isOn(b.rules?.[id]))));
|
|
242
|
+
/**
|
|
243
|
+
* 🔴 AN OPTIONAL RULE THAT IS ON AND REACHES NO PAPER IS A GREEN ZERO. A `files` glob that matches
|
|
244
|
+
* nothing — a typo, a path relative to the wrong directory — leaves the rule never invoked, and a
|
|
245
|
+
* rule that never runs reports exactly like a rule that passed. So for every optional rule the
|
|
246
|
+
* consumer turned on, some linted `paper.tex` must actually have it enabled; the ones none has are
|
|
247
|
+
* returned, for the caller to refuse.
|
|
248
|
+
*/
|
|
249
|
+
export async function silentOptionalRules(eslint, lintedFiles, opts) {
|
|
250
|
+
const turnedOn = new Set((opts.rules ?? []).flatMap((b) => Object.entries(b.rules)
|
|
251
|
+
.filter(([id, e]) => OPTIONAL_RULES.has(id) && isOn(e))
|
|
252
|
+
.map(([id]) => id)));
|
|
253
|
+
const reached = new Set();
|
|
254
|
+
for (const f of lintedFiles.filter((p) => basename(p) === MAIN)) {
|
|
255
|
+
const cfg = (await eslint.calculateConfigForFile(f));
|
|
256
|
+
for (const id of turnedOn)
|
|
257
|
+
if (isOn(cfg.rules?.[id]))
|
|
258
|
+
reached.add(id);
|
|
259
|
+
}
|
|
260
|
+
return [...turnedOn].filter((id) => !reached.has(id));
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* The settings after the boundary: an unknown key is refused by name, and `rules` becomes parsed
|
|
264
|
+
* config blocks. Nothing after this sees the raw object.
|
|
265
|
+
*/
|
|
266
|
+
export function parseSettings(opts, where, baseDir) {
|
|
267
|
+
const raw = opts;
|
|
268
|
+
const unknown = unknownKeys(raw);
|
|
269
|
+
if (unknown.length > 0)
|
|
270
|
+
return {
|
|
271
|
+
ok: false,
|
|
272
|
+
error: `${where}: unknown key${unknown.length > 1 ? "s" : ""} ${unknown.map((k) => `"${k}"`).join(", ")} — ` +
|
|
273
|
+
`a typo would otherwise read as "not set". Known keys: ${Object.keys(SETTINGS_KEYS).join(", ")}`,
|
|
274
|
+
};
|
|
275
|
+
const rules = parseRuleBlocks(raw["rules"], where, SHIPPED_RULES, baseDir);
|
|
276
|
+
if (!rules.ok)
|
|
277
|
+
return rules;
|
|
278
|
+
return { ok: true, value: { ...opts, rules: rules.value } };
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* The directory ESLint runs from. ESLint ignores every file outside it (#48), and
|
|
282
|
+
* `paper/typography` reads its debt keys relative to it — keys the config writes from its own
|
|
283
|
+
* directory. So: the deepest directory holding the config's directory and every path. With the
|
|
284
|
+
* papers inside the config's directory, that is the config's directory itself.
|
|
285
|
+
*/
|
|
286
|
+
const lintRoot = (home, paths) => commonDir([home, ...paths]);
|
|
287
|
+
/** The longest shared leading run of path segments. */
|
|
288
|
+
const commonDir = (paths) => {
|
|
289
|
+
const [first = [], ...rest] = paths.map((p) => p.split(sep));
|
|
290
|
+
const end = first.findIndex((part, i) => rest.some((other) => other[i] !== part));
|
|
291
|
+
return first.slice(0, end === -1 ? undefined : end).join(sep) || sep;
|
|
292
|
+
};
|
|
293
|
+
export function parseArgs(argv) {
|
|
294
|
+
// `--help` is parsed BEFORE argv[0] becomes the command: otherwise `paperlint --help` answers
|
|
295
|
+
// "unknown command `--help`" — caught by the very first run of the utility.
|
|
296
|
+
const out = {
|
|
297
|
+
cmd: null,
|
|
298
|
+
paths: [],
|
|
299
|
+
config: null,
|
|
300
|
+
json: false,
|
|
301
|
+
all: false,
|
|
302
|
+
dryRun: false,
|
|
303
|
+
check: false,
|
|
304
|
+
yes: false,
|
|
305
|
+
noHooks: false,
|
|
306
|
+
paper: null,
|
|
307
|
+
format: null,
|
|
308
|
+
hooksMode: null,
|
|
309
|
+
// -1 = warnings NEVER fail the run. In this set most findings are advisory by design, and a
|
|
310
|
+
// gate that fails on advice gets muted entirely.
|
|
311
|
+
maxWarnings: -1,
|
|
312
|
+
};
|
|
313
|
+
const rest = [...argv];
|
|
314
|
+
if (rest[0] && !rest[0].startsWith("-"))
|
|
315
|
+
out.cmd = rest.shift() ?? null;
|
|
316
|
+
// 🔴 A FLAG WHOSE VALUE WAS TAKEN AWAY IS A REFUSAL, NOT A DEFAULT. The compiler found this
|
|
317
|
+
// during the move to TypeScript: `rest[++i]` past the last argument gives `undefined`, and
|
|
318
|
+
// `paperlint lint --config` (the value forgotten, or eaten by a substitution in CI) silently turned
|
|
319
|
+
// into "no config given" — that is, it went to auto-discovery and linted against SOMEONE ELSE'S
|
|
320
|
+
// file, saying nothing. The failure is one-sided and toward silence, so it is cured by
|
|
321
|
+
// behaviour, not by a type cast.
|
|
322
|
+
const valueFor = (flag, i) => {
|
|
323
|
+
const v = rest[i];
|
|
324
|
+
if (v === undefined)
|
|
325
|
+
out.missingValue = flag;
|
|
326
|
+
return v;
|
|
327
|
+
};
|
|
328
|
+
for (let i = 0; i < rest.length; i++) {
|
|
329
|
+
const a = rest[i];
|
|
330
|
+
if (a === undefined)
|
|
331
|
+
continue;
|
|
332
|
+
if (a === "--json")
|
|
333
|
+
out.json = true;
|
|
334
|
+
else if (a === "--all")
|
|
335
|
+
out.all = true;
|
|
336
|
+
else if (a === "--dry-run")
|
|
337
|
+
out.dryRun = true;
|
|
338
|
+
else if (a === "--check")
|
|
339
|
+
out.check = true;
|
|
340
|
+
else if (a === "--yes" || a === "-y")
|
|
341
|
+
out.yes = true;
|
|
342
|
+
else if (a === "--no-hooks")
|
|
343
|
+
out.noHooks = true;
|
|
344
|
+
else if (a.startsWith("--hooks="))
|
|
345
|
+
out.hooksMode = a.slice("--hooks=".length);
|
|
346
|
+
else if (a === "--paper")
|
|
347
|
+
out.paper = valueFor(a, ++i) ?? null;
|
|
348
|
+
else if (a === "--format")
|
|
349
|
+
out.format = valueFor(a, ++i) ?? null;
|
|
350
|
+
// `--options` was the first spelling and is kept working. It named the wrong thing — every
|
|
351
|
+
// other tool in the stack calls this file its config — but a flag in someone's CI is not
|
|
352
|
+
// ours to break.
|
|
353
|
+
else if (a === "--config" || a === "--options")
|
|
354
|
+
out.config = valueFor(a, ++i) ?? null;
|
|
355
|
+
else if (a === "--max-warnings") {
|
|
356
|
+
const v = valueFor(a, ++i);
|
|
357
|
+
if (v !== undefined)
|
|
358
|
+
out.maxWarnings = Number(v);
|
|
359
|
+
}
|
|
360
|
+
else if (a === "--help" || a === "-h")
|
|
361
|
+
out.help = true;
|
|
362
|
+
else
|
|
363
|
+
out.paths.push(a);
|
|
364
|
+
}
|
|
365
|
+
return out;
|
|
366
|
+
}
|
|
367
|
+
export const CONFIG_NAME = "rpp.json";
|
|
368
|
+
export const PKG_NAME = "package.json";
|
|
369
|
+
/**
|
|
370
|
+
* This package's own version, from the `package.json` beside `src/` and `dist/` alike. `init` pins
|
|
371
|
+
* the CI action to its release tag; an unreadable manifest yields `undefined`, and init then keeps
|
|
372
|
+
* the placeholder instead of guessing.
|
|
373
|
+
*/
|
|
374
|
+
function ownVersion() {
|
|
375
|
+
try {
|
|
376
|
+
const v = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"))?.version;
|
|
377
|
+
return typeof v === "string" ? v : undefined;
|
|
378
|
+
}
|
|
379
|
+
catch {
|
|
380
|
+
return undefined;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
/**
|
|
384
|
+
* 🔴 THE CLI HAD TO LEARN TO READ `package.json`, AND THAT IS NOT A SIDE ERRAND. `paperlint init` now
|
|
385
|
+
* writes ONE declaration, into the `package.json` key that the three hooks and `eslint-rules`
|
|
386
|
+
* already read. Without this walker the install it produces would not work at all: `paperlint lint`
|
|
387
|
+
* would find no `rpp.json`, report "nothing to lint", and the consumer would be back to
|
|
388
|
+
* declaring the same directory twice — the defect the single declaration removes (issue #33,
|
|
389
|
+
* `docs/install.md`).
|
|
390
|
+
*
|
|
391
|
+
* `rpp.json` stays readable as a DEPRECATED fallback, and the read says so out loud. Silently
|
|
392
|
+
* dropping a file this command used to write would break working setups on upgrade.
|
|
393
|
+
*
|
|
394
|
+
* The walk goes up to the filesystem root, the way eslint, prettier and tsc find theirs, so a run
|
|
395
|
+
* from inside one paper sees the same settings as a run from the repository root. At each level
|
|
396
|
+
* `package.json` wins over `rpp.json`: it is the carrier every other reader uses, so preferring
|
|
397
|
+
* it is what keeps "one declaration" true rather than merely intended.
|
|
398
|
+
*/
|
|
399
|
+
export function findDeclaration(startDir) {
|
|
400
|
+
let dir = resolve(startDir);
|
|
401
|
+
for (;;) {
|
|
402
|
+
const pkg = join(dir, PKG_NAME);
|
|
403
|
+
if (existsSync(pkg) && declaresSettings(pkg))
|
|
404
|
+
return { path: pkg, kind: "package.json" };
|
|
405
|
+
const rpp = join(dir, CONFIG_NAME);
|
|
406
|
+
if (existsSync(rpp))
|
|
407
|
+
return { path: rpp, kind: "rpp.json" };
|
|
408
|
+
const up = dirname(dir);
|
|
409
|
+
if (up === dir)
|
|
410
|
+
return null;
|
|
411
|
+
dir = up;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
/**
|
|
415
|
+
* A `package.json` WITHOUT the key is not a declaration and must not stop the walk — every
|
|
416
|
+
* project on the way up has one, so stopping there would make the search find nothing, always.
|
|
417
|
+
* An unparsable one is treated the same way here; `paperlint doctor` is the command that reports it.
|
|
418
|
+
*/
|
|
419
|
+
const declaresSettings = (pkgPath) => {
|
|
420
|
+
try {
|
|
421
|
+
const d = declaredSettings(JSON.parse(readFileSync(pkgPath, "utf8")));
|
|
422
|
+
return d.settings !== undefined || d.conflict !== null;
|
|
423
|
+
}
|
|
424
|
+
catch {
|
|
425
|
+
return false;
|
|
426
|
+
}
|
|
427
|
+
};
|
|
428
|
+
/** Kept as the one-line question "which file holds the settings" — callers that only need a path. */
|
|
429
|
+
export function findConfig(startDir) {
|
|
430
|
+
return findDeclaration(startDir)?.path ?? null;
|
|
431
|
+
}
|
|
432
|
+
/**
|
|
433
|
+
* Reading the config, ONE reader for all commands. Pulled out of `run()` the moment a second
|
|
434
|
+
* command needed the same config (`build`): two copies of this block would have drifted apart on
|
|
435
|
+
* the very first edit — exactly the class that already cost us the empty-set guard in two places.
|
|
436
|
+
*
|
|
437
|
+
* @returns `{ opts, configPath }` on success, or `{ code }` — and then the caller exits with it.
|
|
438
|
+
*/
|
|
439
|
+
export function readConfig(a, { log = console.log, err = console.error, cwd = process.cwd(), } = {}) {
|
|
440
|
+
// 🔴 THE CONFIG FINDS ITSELF. An explicit `--config` beats the discovered one — it was named out
|
|
441
|
+
// loud, and a substitution is never silent. For an explicit path the FILE NAME decides the
|
|
442
|
+
// carrier: the path here is a value, not a text to make guesses about, and `package.json` holds
|
|
443
|
+
// the settings under a key.
|
|
444
|
+
const decl = a.config
|
|
445
|
+
? {
|
|
446
|
+
path: a.config,
|
|
447
|
+
kind: basename(a.config) === PKG_NAME ? "package.json" : "rpp.json",
|
|
448
|
+
}
|
|
449
|
+
: findDeclaration(cwd);
|
|
450
|
+
const configPath = decl?.path ?? null;
|
|
451
|
+
if (a.config && !existsSync(a.config)) {
|
|
452
|
+
err(`config file not found: ${a.config}`);
|
|
453
|
+
return { code: 2 };
|
|
454
|
+
}
|
|
455
|
+
let opts = {};
|
|
456
|
+
let legacyKey = false;
|
|
457
|
+
if (decl && configPath) {
|
|
458
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- #49: replace with a real type
|
|
459
|
+
let parsed;
|
|
460
|
+
try {
|
|
461
|
+
parsed = JSON.parse(readFileSync(configPath, "utf8"));
|
|
462
|
+
}
|
|
463
|
+
catch (e) {
|
|
464
|
+
err(`${configPath} is not valid JSON: ${e.message}`);
|
|
465
|
+
return { code: 2 };
|
|
466
|
+
}
|
|
467
|
+
if (decl.kind === "package.json") {
|
|
468
|
+
const d = declaredSettings(parsed);
|
|
469
|
+
if (d.conflict !== null) {
|
|
470
|
+
err(d.conflict);
|
|
471
|
+
return { code: 2 };
|
|
472
|
+
}
|
|
473
|
+
opts = (d.settings ?? {});
|
|
474
|
+
legacyKey = d.legacy;
|
|
475
|
+
}
|
|
476
|
+
else
|
|
477
|
+
opts = parsed;
|
|
478
|
+
// The discovered config is NAMED out loud. Otherwise a run from someone else's directory picks
|
|
479
|
+
// up someone else's file and does not say so — and a typography-debt mismatch looks like a finding.
|
|
480
|
+
//
|
|
481
|
+
// 🔴 IN `--json` MODE — TO stderr. Machine output must be ONE parsable document: a line before
|
|
482
|
+
// the array breaks any `| jq`, and it breaks it for the consumer, not for us. Caught not by a
|
|
483
|
+
// test but by an attempt to wire our own action to this output; in the harness I first WORKED
|
|
484
|
+
// AROUND this line (stripped the first line before JSON.parse) — that is, the workaround hid
|
|
485
|
+
// the defect exactly where it should have been shouting.
|
|
486
|
+
(a.json ? err : log)(`config: ${relative(cwd, configPath) || CONFIG_NAME}`);
|
|
487
|
+
// 🔴 THE DEPRECATED CARRIER IS NAMED OUT LOUD, IT DOES NOT STOP BEING READ. The hooks read ONLY
|
|
488
|
+
// package.json, so a consumer whose settings stayed in rpp.json lints one directory and guards
|
|
489
|
+
// another — and both states look equally green.
|
|
490
|
+
if (decl.kind === "rpp.json")
|
|
491
|
+
(a.json ? err : log)(` ⚠ ${CONFIG_NAME} is deprecated — move these keys under "${CONFIG_KEY}" in ${PKG_NAME}; ` +
|
|
492
|
+
`the hooks read only that file. \`npx paperlint init\` does it for you.`);
|
|
493
|
+
if (legacyKey)
|
|
494
|
+
(a.json ? err : log)(` ⚠ ${LEGACY_KEY_MESSAGE}`);
|
|
495
|
+
}
|
|
496
|
+
// The old field name is refused before anything else is read from the settings: falling back
|
|
497
|
+
// to it would keep it working forever, and this package has no released users to migrate.
|
|
498
|
+
const where = decl?.kind === "package.json"
|
|
499
|
+
? `${PKG_NAME} → "${legacyKey ? LEGACY_CONFIG_KEY : CONFIG_KEY}"`
|
|
500
|
+
: (decl?.path ?? CONFIG_NAME);
|
|
501
|
+
const renamed = decl ? renamedFieldMessage(opts, where) : null;
|
|
502
|
+
if (renamed) {
|
|
503
|
+
err(renamed);
|
|
504
|
+
return { code: 2 };
|
|
505
|
+
}
|
|
506
|
+
if (decl && configPath) {
|
|
507
|
+
const parsed = parseSettings(opts, where, dirname(resolve(cwd, configPath)));
|
|
508
|
+
if (!parsed.ok) {
|
|
509
|
+
err(parsed.error);
|
|
510
|
+
return { code: 2 };
|
|
511
|
+
}
|
|
512
|
+
opts = parsed.value;
|
|
513
|
+
}
|
|
514
|
+
// 🔴 THE PAPERS DIRECTORY IS A REQUIRED FIELD. The papers directory is the one thing without which the tool
|
|
515
|
+
// does not know what it works on, and the one thing that cannot be guessed: a default of "." runs
|
|
516
|
+
// the rules over the whole checkout and exits green over a scope nobody chose.
|
|
517
|
+
if (decl && !hasPapers(opts)) {
|
|
518
|
+
err(decl.kind === "package.json"
|
|
519
|
+
? `${decl.path} must declare \`${PAPERS_DIR_FIELD}\` — the directory your papers live in, e.g.\n` +
|
|
520
|
+
` { "${CONFIG_KEY}": { "${PAPERS_DIR_FIELD}": "papers" } }\n` +
|
|
521
|
+
`It is the one thing this tool cannot guess. \`npx paperlint init\` writes it for you.`
|
|
522
|
+
: `${decl.path} must declare \`${PAPERS_DIR_FIELD}\` — the directory your papers live in, e.g.\n` +
|
|
523
|
+
` { "${PAPERS_DIR_FIELD}": "papers" }\n` +
|
|
524
|
+
`It is the one thing this tool cannot guess.`);
|
|
525
|
+
return { code: 2 };
|
|
526
|
+
}
|
|
527
|
+
return { opts, configPath };
|
|
528
|
+
}
|
|
529
|
+
/** The papers directory field of the settings, read by its one declared name. */
|
|
530
|
+
export function papersDirOf(opts) {
|
|
531
|
+
return opts[PAPERS_DIR_FIELD];
|
|
532
|
+
}
|
|
533
|
+
/** The papers directory may be one directory or several; both spellings normalise to a list. */
|
|
534
|
+
export function toPaths(papers) {
|
|
535
|
+
if (typeof papers === "string")
|
|
536
|
+
return papers.trim() ? [papers.trim()] : [];
|
|
537
|
+
if (Array.isArray(papers))
|
|
538
|
+
return papers.filter((x) => typeof x === "string" && x.trim());
|
|
539
|
+
return [];
|
|
540
|
+
}
|
|
541
|
+
const hasPapers = (opts) => toPaths(papersDirOf(opts)).length > 0;
|
|
542
|
+
/**
|
|
543
|
+
* `paperlint hook <name>` — run an editor hook. It exists for ONE thing: so that the wiring does not
|
|
544
|
+
* address the runtime from the project root.
|
|
545
|
+
*
|
|
546
|
+
* 🔴 WHAT IT WAS AND WHY IT BROKE. `hooks.json` called
|
|
547
|
+
* node "${CLAUDE_PROJECT_DIR}/node_modules/vigiles/dist/cli.js" hook-runtime run-program …
|
|
548
|
+
* While `vigiles` was a PEER dependency this path was GUARANTEED: a peer is installed by the
|
|
549
|
+
* consumer itself, into its own root. After the move to ordinary dependencies the guarantee was
|
|
550
|
+
* gone, and a measurement showed it — one tarball, two managers:
|
|
551
|
+
* npm: node_modules/vigiles/dist/cli.js PRESENT
|
|
552
|
+
* pnpm: node_modules/vigiles/dist/cli.js ABSENT (only paperlint in the root)
|
|
553
|
+
* The cost of the failure is asymmetric: `|| exit 2` stood on PreToolUse(Bash), that is, ANY
|
|
554
|
+
* command was denied, including the one you fix it with.
|
|
555
|
+
*
|
|
556
|
+
* WHAT IT IS NOW. The wiring calls ITS OWN bin — `paperlint` is a direct dependency,
|
|
557
|
+
* so it lies in the root under any manager — and the runtime is resolved FROM THE POSITION OF THIS
|
|
558
|
+
* FILE via `createRequire`. Wherever the manager laid the tree out, the resolver finds the same
|
|
559
|
+
* thing an `import` from inside the package would.
|
|
560
|
+
*
|
|
561
|
+
* 🔴 AND `|| exit 2` IS REMOVED FROM THE SHELL. The decision to stop is a decision, and it is taken
|
|
562
|
+
* here, in code. In the shell it meant "any mishap = block everything": the runtime was not found —
|
|
563
|
+
* work stopped. Now a runtime that is not found complains LOUDLY and returns 0, while the hook's
|
|
564
|
+
* real verdict (2 included) passes through. Silent degradation is worse than explicit degradation,
|
|
565
|
+
* but blocking everything is worse than both.
|
|
566
|
+
*/
|
|
567
|
+
export function runHook(name, { err = console.error, run = spawnSync,
|
|
568
|
+
// The resolver is injected so that "the runtime was not found" is checked by an assert and not
|
|
569
|
+
// by deleting node_modules: a failure must be reproducible, not staged.
|
|
570
|
+
// 🔴 WE RESOLVE THE PACKAGE, NOT A FILE INSIDE IT. `require.resolve("vigiles/dist/cli.js")`
|
|
571
|
+
// DOES NOT WORK: the package's `exports` map hands out only "." and nine named subpaths, and
|
|
572
|
+
// `./dist/cli.js` — even `./package.json` — is not among them:
|
|
573
|
+
// Package subpath './dist/cli.js' is not defined by "exports"
|
|
574
|
+
// This is neither our oversight nor their bug: a closed export map is normal practice. So we
|
|
575
|
+
// resolve the root entry ("." → dist/test.js), take its directory and put `cli.js` next to it —
|
|
576
|
+
// the very file the package itself declares as its `bin`.
|
|
577
|
+
resolve = (spec) => createRequire(import.meta.url).resolve(spec), } = {}) {
|
|
578
|
+
if (!name) {
|
|
579
|
+
err(`\`hook\` needs a name, e.g. \`paperlint hook paper-edit-guard\``);
|
|
580
|
+
return 2;
|
|
581
|
+
}
|
|
582
|
+
const program = fileURLToPath(new URL(`../hooks/${name}.hook.mjs`, import.meta.url));
|
|
583
|
+
if (!existsSync(program)) {
|
|
584
|
+
err(`unknown hook \`${name}\` — no such program at ${program}`);
|
|
585
|
+
return 2;
|
|
586
|
+
}
|
|
587
|
+
let runtime;
|
|
588
|
+
try {
|
|
589
|
+
runtime = join(dirname(resolve("vigiles")), "cli.js");
|
|
590
|
+
if (!existsSync(runtime))
|
|
591
|
+
throw new Error(`resolved vigiles, but no cli.js beside it: ${runtime}`);
|
|
592
|
+
}
|
|
593
|
+
catch {
|
|
594
|
+
err(`rpp: the hook runtime (vigiles) is not resolvable from ${fileURLToPath(new URL(".", import.meta.url))}.\n` +
|
|
595
|
+
`The \`${name}\` hook is NOT running. Everything else — \`paperlint lint\`, CI — is unaffected.\n` +
|
|
596
|
+
`Reinstall this package so its dependencies are present.`);
|
|
597
|
+
return 0;
|
|
598
|
+
}
|
|
599
|
+
const r = run(process.execPath, [runtime, "hook-runtime", "run-program", program], {
|
|
600
|
+
stdio: "inherit",
|
|
601
|
+
});
|
|
602
|
+
return r.status ?? 0;
|
|
603
|
+
}
|
|
604
|
+
/**
|
|
605
|
+
* Create one paper and lint it — shared by `paperlint new` and `paperlint init --paper`, so the two cannot
|
|
606
|
+
* become two implementations. The lint runs on THAT folder, so the first thing printed after the
|
|
607
|
+
* file list is its verdict rather than the old "missing PIPELINE-STATUS.md".
|
|
608
|
+
*/
|
|
609
|
+
export async function createPaperAt(papersRoot, name, format, { log, err, cwd, }) {
|
|
610
|
+
const result = newPaper(papersRoot, name, format);
|
|
611
|
+
const here = (p) => relative(cwd, p) || p;
|
|
612
|
+
for (const line of reportNewPaper(result, here))
|
|
613
|
+
log(line);
|
|
614
|
+
if (!result.ok)
|
|
615
|
+
return 2;
|
|
616
|
+
log(``);
|
|
617
|
+
return run(["lint", result.dir], { log, err, cwd });
|
|
618
|
+
}
|
|
619
|
+
/**
|
|
620
|
+
* `paperlint new <name>` — the papers directory comes from the same declaration every other command
|
|
621
|
+
* reads; there is no second way to name it. See `new-paper.ts` for what it writes and why.
|
|
622
|
+
*/
|
|
623
|
+
async function runNew(a, { log, err, cwd, ask = askOnTerminal, }) {
|
|
624
|
+
const [name, ...extra] = a.paths;
|
|
625
|
+
if (!name || extra.length > 0) {
|
|
626
|
+
err(`\`new\` takes exactly one paper name: \`paperlint new my-paper [--format tex|md]\``);
|
|
627
|
+
return 2;
|
|
628
|
+
}
|
|
629
|
+
let format = DEFAULT_FORMAT;
|
|
630
|
+
if (a.format !== null) {
|
|
631
|
+
if (!isFormat(a.format)) {
|
|
632
|
+
err(`--format must be one of ${FORMATS.join(", ")} — got \`${a.format}\``);
|
|
633
|
+
return 2;
|
|
634
|
+
}
|
|
635
|
+
format = a.format;
|
|
636
|
+
}
|
|
637
|
+
else if (processInteractivity(a.yes).interactive) {
|
|
638
|
+
const f = await ask(`format: tex / md [${DEFAULT_FORMAT}] `).catch(() => "");
|
|
639
|
+
if (isFormat(f.trim()))
|
|
640
|
+
format = f.trim();
|
|
641
|
+
}
|
|
642
|
+
const cfg = readConfig({ ...a, json: false }, { log: () => { }, err, cwd });
|
|
643
|
+
if (cfg.code !== undefined)
|
|
644
|
+
return cfg.code;
|
|
645
|
+
const roots = toPaths(papersDirOf(cfg.opts)).map((rel) => resolve(dirname(cfg.configPath ?? cwd), rel));
|
|
646
|
+
const papersRoot = roots[0];
|
|
647
|
+
if (!cfg.configPath || papersRoot === undefined) {
|
|
648
|
+
err(`no papers directory is declared, so there is nowhere to put \`${name}\`.\n` +
|
|
649
|
+
`Run \`npx paperlint init\` first — it declares the directory in package.json.`);
|
|
650
|
+
return 2;
|
|
651
|
+
}
|
|
652
|
+
if (roots.length > 1)
|
|
653
|
+
log(`several papers directories are declared — using the first: ${relative(cwd, papersRoot) || papersRoot}`);
|
|
654
|
+
return createPaperAt(papersRoot, name, format, { log, err, cwd });
|
|
655
|
+
}
|
|
656
|
+
/**
|
|
657
|
+
* 🔴 THE TARGET IS NAMED, "EVERYTHING" IS AN OPTION. That is how it is for everyone whose build is
|
|
658
|
+
* expensive and has side effects: `make <target>`, `docker build <context>`, `latexmk paper.tex`;
|
|
659
|
+
* with cargo, "the whole workspace" is turned on by a separate flag. A default of "build
|
|
660
|
+
* everything" on a corpus of five papers is twenty pdflatex runs instead of one, and almost never
|
|
661
|
+
* what was wanted.
|
|
662
|
+
*/
|
|
663
|
+
async function runBuild(a, { log, err, cwd, }) {
|
|
664
|
+
const cfg = readConfig(a, { log, err, cwd });
|
|
665
|
+
if (cfg.code !== undefined)
|
|
666
|
+
return cfg.code;
|
|
667
|
+
const { opts, configPath } = cfg;
|
|
668
|
+
// The key that used to name the scripts to run. It is read by nothing now; saying so beats a
|
|
669
|
+
// setting that silently stopped doing anything.
|
|
670
|
+
if (opts.buildScripts !== undefined)
|
|
671
|
+
log(`note: "buildScripts" in ${relative(cwd, configPath ?? "") || "the settings"} is ignored — paperlint builds the paper itself`);
|
|
672
|
+
const roots = toPaths(papersDirOf(opts)).map((rel) => resolve(configPath ? dirname(configPath) : cwd, rel));
|
|
673
|
+
let targets;
|
|
674
|
+
if (a.all) {
|
|
675
|
+
targets = roots.flatMap((r) => papersIn(r));
|
|
676
|
+
if (targets.length === 0) {
|
|
677
|
+
err(`--all: no papers found under ${roots.join(", ") || "(nothing declared)"}`);
|
|
678
|
+
return 1;
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
else if (a.paths.length > 0) {
|
|
682
|
+
targets = a.paths.map((p) => resolve(cwd, p));
|
|
683
|
+
}
|
|
684
|
+
else {
|
|
685
|
+
err(`\`build\` needs a target: \`paperlint build papers/my-paper\` or \`paperlint build --all\`.\n` +
|
|
686
|
+
`There is deliberately no "build everything" default: a build is expensive and has side\n` +
|
|
687
|
+
`effects, so the target is named — as with make, docker and latexmk.`);
|
|
688
|
+
return 2;
|
|
689
|
+
}
|
|
690
|
+
// The engine is resolved INSIDE buildPapers, after it has removed the stale PDFs: a run that
|
|
691
|
+
// stops for want of a TeX Live must not leave an old paper.pdf looking current either.
|
|
692
|
+
const out = await buildPapers(targets, {
|
|
693
|
+
cwd,
|
|
694
|
+
dryRun: a.dryRun,
|
|
695
|
+
log,
|
|
696
|
+
engine: () => engineEnv(targets, a, { log, err }),
|
|
697
|
+
});
|
|
698
|
+
if (out.kind === "no-engine")
|
|
699
|
+
return 1;
|
|
700
|
+
const remedy = remedyFor(out.results);
|
|
701
|
+
if (remedy)
|
|
702
|
+
err(remedy);
|
|
703
|
+
return anyFailed(out.results) ? 1 : 0;
|
|
704
|
+
}
|
|
705
|
+
/** What one paper needs from TeX Live; a venue.json that does not parse is the build's to report. */
|
|
706
|
+
function paperRequirements(dir) {
|
|
707
|
+
let venue = null;
|
|
708
|
+
try {
|
|
709
|
+
venue = readFacts(dir).venue;
|
|
710
|
+
}
|
|
711
|
+
catch {
|
|
712
|
+
venue = null;
|
|
713
|
+
}
|
|
714
|
+
return requirementsFor(venue).tex;
|
|
715
|
+
}
|
|
716
|
+
/**
|
|
717
|
+
* The environment the builds run in — PATH led by a TeX Live that has every package the targeted
|
|
718
|
+
* papers' venues declare — or null when there is none and none was installed (the reason is
|
|
719
|
+
* already printed). Papers without `paper.tex` need no engine: they are refused by the build.
|
|
720
|
+
*/
|
|
721
|
+
async function engineEnv(targets, a, { log, err }) {
|
|
722
|
+
const latex = targets.filter((t) => existsSync(join(t, MAIN)));
|
|
723
|
+
if (latex.length === 0)
|
|
724
|
+
return process.env;
|
|
725
|
+
const tex = latex
|
|
726
|
+
.map(paperRequirements)
|
|
727
|
+
.reduce(mergeRequirements, NO_REQUIREMENTS);
|
|
728
|
+
const out = await prepareEngine({
|
|
729
|
+
tex,
|
|
730
|
+
dryRun: a.dryRun,
|
|
731
|
+
interactive: processInteractivity(false).interactive,
|
|
732
|
+
ask: askOnTerminal,
|
|
733
|
+
log,
|
|
734
|
+
err,
|
|
735
|
+
});
|
|
736
|
+
return out.ok ? out.env : null;
|
|
737
|
+
}
|
|
738
|
+
/** Commands that take the parsed arguments and the output streams, and nothing else. */
|
|
739
|
+
const SIMPLE = {
|
|
740
|
+
hook: (a, { err }) => runHook(a.paths[0], { err }),
|
|
741
|
+
new: (a, io) => runNew(a, io),
|
|
742
|
+
build: (a, io) => runBuild(a, io),
|
|
743
|
+
toolchain: (a, { log, err }) => runToolchain({ check: a.check, log, err, banal: hostBanalInstaller() }),
|
|
744
|
+
};
|
|
745
|
+
/** banal's installer, wired from this process's environment: the composition root's work. */
|
|
746
|
+
function hostBanalInstaller() {
|
|
747
|
+
const s = parseBanalSettings(process.env, hostDirs());
|
|
748
|
+
const ports = nodeAdapters({ tmpDir: s.tmpDir });
|
|
749
|
+
const download = curlDownload({
|
|
750
|
+
run: ports.run,
|
|
751
|
+
env: s.processEnv,
|
|
752
|
+
tmpDir: s.tmpDir,
|
|
753
|
+
});
|
|
754
|
+
return banalInstaller({ ...ports, download }, s);
|
|
755
|
+
}
|
|
756
|
+
export async function run(argv, { log = console.log, err = console.error, cwd = process.cwd(), } = {}) {
|
|
757
|
+
const a = parseArgs(argv);
|
|
758
|
+
// The refusal must come FIRST: behind a flag without a value there is usually a typo, or a
|
|
759
|
+
// substitution in CI that collapsed into nothing, and any continuation works on something other
|
|
760
|
+
// than what was asked for.
|
|
761
|
+
if (a.missingValue) {
|
|
762
|
+
err(`${a.missingValue} needs a value — it was given none.\n` +
|
|
763
|
+
`Without it the run would silently fall back to whatever config it discovers, which is ` +
|
|
764
|
+
`not what the command line said.`);
|
|
765
|
+
return 2;
|
|
766
|
+
}
|
|
767
|
+
if (a.help || !a.cmd) {
|
|
768
|
+
log(USAGE);
|
|
769
|
+
return a.help ? 0 : 2;
|
|
770
|
+
}
|
|
771
|
+
// `init` asks the CLI's OWN reader what it would lint, so the two sides `doctor` compares are
|
|
772
|
+
// not two implementations of the same question. A second resolver here is the defect the
|
|
773
|
+
// comparison exists to catch.
|
|
774
|
+
if (a.cmd === "init") {
|
|
775
|
+
// Deferred, not implemented: named and refused, rather than read as the directory argument.
|
|
776
|
+
if (a.hooksMode !== null) {
|
|
777
|
+
err(`--hooks=${a.hooksMode} is not implemented. init writes the hooks into .claude/settings.json ` +
|
|
778
|
+
`(shared, committed) or, with --no-hooks, nowhere.`);
|
|
779
|
+
return 2;
|
|
780
|
+
}
|
|
781
|
+
if (a.format !== null && !isFormat(a.format)) {
|
|
782
|
+
err(`--format must be one of ${FORMATS.join(", ")} — got \`${a.format}\``);
|
|
783
|
+
return 2;
|
|
784
|
+
}
|
|
785
|
+
return await init(a.paths[0] ?? ".", {
|
|
786
|
+
log,
|
|
787
|
+
err,
|
|
788
|
+
cwd,
|
|
789
|
+
version: ownVersion(),
|
|
790
|
+
yes: a.yes,
|
|
791
|
+
hooks: !a.noHooks,
|
|
792
|
+
paper: a.paper,
|
|
793
|
+
format: isFormat(a.format) ? a.format : null,
|
|
794
|
+
createPaper: (papersRoot, name, format) => createPaperAt(papersRoot, name, format, { log, err, cwd }),
|
|
795
|
+
resolveCliPapers: (root) => {
|
|
796
|
+
const read = readConfig({ ...a, config: null }, { log: () => { }, err: () => { }, cwd: root });
|
|
797
|
+
return read.code === undefined
|
|
798
|
+
? (toPaths(papersDirOf(read.opts))[0] ?? null)
|
|
799
|
+
: null;
|
|
800
|
+
},
|
|
801
|
+
});
|
|
802
|
+
}
|
|
803
|
+
// `doctor` reads the config but must NOT die on a broken one — reporting that the config is
|
|
804
|
+
// broken is precisely its job. So a failed read becomes "the CLI would lint nothing", which is
|
|
805
|
+
// what it prints, rather than an early exit that tells the reader nothing about the hooks.
|
|
806
|
+
if (a.cmd === "doctor") {
|
|
807
|
+
const read = readConfig(a, { log: () => { }, err: () => { }, cwd });
|
|
808
|
+
const papers = read.code === undefined
|
|
809
|
+
? (toPaths(papersDirOf(read.opts))[0] ?? null)
|
|
810
|
+
: null;
|
|
811
|
+
return doctor({
|
|
812
|
+
log,
|
|
813
|
+
cwd,
|
|
814
|
+
projectDir: process.env["CLAUDE_PROJECT_DIR"] ?? cwd,
|
|
815
|
+
cliPapers: papers,
|
|
816
|
+
});
|
|
817
|
+
}
|
|
818
|
+
const simple = SIMPLE[a.cmd];
|
|
819
|
+
if (simple)
|
|
820
|
+
return await simple(a, { log, err, cwd });
|
|
821
|
+
if (a.cmd === "check")
|
|
822
|
+
err(`\`check\` is now \`lint\` — running it anyway. Update the call to \`paperlint lint\`.`);
|
|
823
|
+
if (a.cmd !== "lint" && a.cmd !== "check") {
|
|
824
|
+
err(`unknown command \`${a.cmd}\`\n\n${USAGE}`);
|
|
825
|
+
return 2;
|
|
826
|
+
}
|
|
827
|
+
const cfg = readConfig(a, { log, err, cwd });
|
|
828
|
+
if (cfg.code !== undefined)
|
|
829
|
+
return cfg.code;
|
|
830
|
+
const { opts, configPath } = cfg;
|
|
831
|
+
// A command-line argument OVERRIDES the config: one paper out of the corpus gets linted without
|
|
832
|
+
// editing a file.
|
|
833
|
+
//
|
|
834
|
+
// 🔴 A path FROM THE CONFIG is resolved relative to the CONFIG'S DIRECTORY, not the current one.
|
|
835
|
+
// Otherwise walking up is pointless: from `papers/aisec-2026` the file would be found, but
|
|
836
|
+
// `"papersDir": "papers"` would point at `papers/aisec-2026/papers`, which does not exist — and the
|
|
837
|
+
// run would fail with "nothing found" where everything is in place. A command-line argument stays
|
|
838
|
+
// relative to the current directory: it was typed here and now.
|
|
839
|
+
//
|
|
840
|
+
// Both kinds end up ABSOLUTE: ESLint below runs from `lintRoot`, not from here, and would resolve a
|
|
841
|
+
// relative argument against the wrong directory.
|
|
842
|
+
const paths = a.paths.length > 0
|
|
843
|
+
? a.paths.map((p) => resolve(cwd, p))
|
|
844
|
+
: toPaths(papersDirOf(opts)).map((rel) => resolve(dirname(configPath ?? cwd), rel));
|
|
845
|
+
if (paths.length === 0) {
|
|
846
|
+
err(`nothing to lint: no path was given and no ${CONFIG_NAME} was found.\n` +
|
|
847
|
+
`Run \`npx paperlint init\` here, or pass the directory: \`paperlint lint papers\`.`);
|
|
848
|
+
return 2;
|
|
849
|
+
}
|
|
850
|
+
// 🔴 STRUCTURE IS CHECKED BEFORE ESLint AND SEPARATELY FROM IT. A rule is invoked for the file
|
|
851
|
+
// handed to it; a missing file is never handed over, so no rule at all can report the absence —
|
|
852
|
+
// a directory without `PIPELINE-STATUS.md` simply gets not a single rule and reports clean. The
|
|
853
|
+
// analysis of why a structure plugin for ESLint does not cure this is in `structure.mjs`.
|
|
854
|
+
const structure = checkStructure(paths, opts.structure, { cwd });
|
|
855
|
+
let texLanguage = null;
|
|
856
|
+
try {
|
|
857
|
+
// @ts-expect-error — the module is .mjs and has no types; a missing LaTeX parser is a normal
|
|
858
|
+
// case here, it is caught by the catch below.
|
|
859
|
+
({ texLanguage } = await import("../eslint-rules/latex-language.mjs"));
|
|
860
|
+
}
|
|
861
|
+
catch {
|
|
862
|
+
/* without a LaTeX parser we work over markdown */
|
|
863
|
+
}
|
|
864
|
+
const eslint = new ESLint({
|
|
865
|
+
cwd: lintRoot(configPath ? dirname(resolve(cwd, configPath)) : cwd, paths),
|
|
866
|
+
overrideConfigFile: true,
|
|
867
|
+
overrideConfig: buildConfig(opts, texLanguage),
|
|
868
|
+
});
|
|
869
|
+
// 🔴 ESLint THROWS on an empty set (`NoFilesFoundError`) — the guard below simply never got
|
|
870
|
+
// reached, which is what the very first run over an empty directory showed: instead of a clear
|
|
871
|
+
// message a stack from the depths of eslint-helpers.js flew out. A failure stays a failure, but
|
|
872
|
+
// an explicable one.
|
|
873
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- #49: replace with a real type
|
|
874
|
+
let results;
|
|
875
|
+
try {
|
|
876
|
+
results = await eslint.lintFiles(paths);
|
|
877
|
+
}
|
|
878
|
+
catch (e) {
|
|
879
|
+
const fail = e;
|
|
880
|
+
if (fail?.messageTemplate === "file-not-found" ||
|
|
881
|
+
/No files matching/i.test(fail?.message ?? ""))
|
|
882
|
+
results = [];
|
|
883
|
+
else
|
|
884
|
+
throw e;
|
|
885
|
+
}
|
|
886
|
+
// 🔴 THE GUARD AGAINST A GREEN ZERO, the same one as in action.yml and for the same reason:
|
|
887
|
+
// ESLint exits zero when there are no findings, and "no findings" is byte-for-byte
|
|
888
|
+
// indistinguishable from "not a single rule got a single file". A rule whose glob did not match
|
|
889
|
+
// is not invoked — and, not being invoked, it physically cannot report that.
|
|
890
|
+
if (results.length === 0) {
|
|
891
|
+
err(`nothing was linted under ${paths.map((x) => relative(cwd, x) || x).join(", ")} — no PIPELINE-STATUS.md, paper.md/tex or reviews/ found there. A clean report over zero files is not a clean report.`);
|
|
892
|
+
return 1;
|
|
893
|
+
}
|
|
894
|
+
return await reportLint(eslint, results, structure, {
|
|
895
|
+
a,
|
|
896
|
+
log,
|
|
897
|
+
err,
|
|
898
|
+
where: relative(cwd, dirname(resolve(cwd, configPath ?? "."))) || ".",
|
|
899
|
+
opts,
|
|
900
|
+
});
|
|
901
|
+
}
|
|
902
|
+
/**
|
|
903
|
+
* The end of `paperlint lint`: refuse an optional rule that reached no paper, print the findings, and
|
|
904
|
+
* decide the exit code. Pulled out of `run` so each question has its own function.
|
|
905
|
+
*/
|
|
906
|
+
async function reportLint(eslint,
|
|
907
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- #49: replace with a real type
|
|
908
|
+
results, structure, { a, log, err, where, opts, }) {
|
|
909
|
+
const silent = await silentOptionalRules(eslint, results.map((r) => r.filePath), opts);
|
|
910
|
+
if (silent.length > 0) {
|
|
911
|
+
err(`${silent.join(", ")} is turned on in "rules", but no linted paper.tex gets it — check the block's ` +
|
|
912
|
+
`"files" (relative to ${where}). A rule that never runs reports exactly like a rule that passed.`);
|
|
913
|
+
return 1;
|
|
914
|
+
}
|
|
915
|
+
if (a.json)
|
|
916
|
+
log(JSON.stringify([...asEslintResults(structure), ...results], null, 1));
|
|
917
|
+
else {
|
|
918
|
+
// Missing things are printed FIRST: they explain why the report below may be suspiciously
|
|
919
|
+
// short. The reverse order would read as "all clean — oh, and also this".
|
|
920
|
+
if (structure.length > 0)
|
|
921
|
+
log(formatStructure(structure));
|
|
922
|
+
const out = await (await eslint.loadFormatter("stylish")).format(results);
|
|
923
|
+
log(out.trim() ||
|
|
924
|
+
(structure.length > 0
|
|
925
|
+
? ""
|
|
926
|
+
: `✓ ${results.length} file(s) checked, no findings`));
|
|
927
|
+
}
|
|
928
|
+
if (structure.length > 0 || results.some((r) => r.errorCount > 0))
|
|
929
|
+
return 1;
|
|
930
|
+
// Warnings fail the run only when the threshold is named EXPLICITLY. A negative threshold means
|
|
931
|
+
// "do not count them at all", and that is the default.
|
|
932
|
+
if (a.maxWarnings >= 0) {
|
|
933
|
+
const warnings = results.reduce((n, r) => n + r.warningCount, 0);
|
|
934
|
+
if (warnings > a.maxWarnings) {
|
|
935
|
+
err(`${warnings} warning(s) exceed the --max-warnings limit of ${a.maxWarnings}`);
|
|
936
|
+
return 1;
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
return 0;
|
|
940
|
+
}
|
|
941
|
+
// 🔴 `isMain`, NOT A STRING COMPARISON. The first version wrote
|
|
942
|
+
// if (import.meta.url === `file://${process.argv[1]}`)
|
|
943
|
+
// and the utility, launched via `node_modules/.bin/rpp`, SILENTLY EXITED WITH ZERO: npm puts a
|
|
944
|
+
// SYMLINK there, `process.argv[1]` stays the symlink's path while `import.meta.url` is the real
|
|
945
|
+
// path, and the condition is false. That is, the only way a real consumer launches the utility did
|
|
946
|
+
// not work at all — and it looked like a clean run.
|
|
947
|
+
// The helper WAS ALREADY in the package, and its docstring describes exactly this failure
|
|
948
|
+
// verbatim: "turns a CLI into a no-op that exits 0". I wrote by hand what was lying there ready.
|
|
949
|
+
if (isMain(import.meta.url))
|
|
950
|
+
process.exit(await run(process.argv.slice(2)));
|
|
951
|
+
//# sourceMappingURL=cli.js.map
|