paperlint 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/dependabot.yml +72 -0
- package/.github/workflows/ci.yml +297 -0
- package/.github/workflows/dependabot-automerge.yml +70 -0
- package/.github/workflows/pr-title.yml +59 -0
- package/.github/workflows/release.yml +54 -0
- package/CLAUDE.md +598 -0
- package/CONTRIBUTING.md +159 -0
- package/LICENSE +21 -0
- package/README.md +240 -0
- package/action.harness.mjs +287 -0
- package/action.mutations.mjs +162 -0
- package/action.yml +138 -0
- package/bin/rpp.mjs +43 -0
- package/dist/action-ref.d.ts +12 -0
- package/dist/action-ref.d.ts.map +1 -0
- package/dist/action-ref.js +16 -0
- package/dist/action-ref.js.map +1 -0
- package/dist/adapters/banal/failure.d.ts +73 -0
- package/dist/adapters/banal/failure.d.ts.map +1 -0
- package/dist/adapters/banal/failure.js +58 -0
- package/dist/adapters/banal/failure.js.map +1 -0
- package/dist/adapters/banal/index.d.ts +17 -0
- package/dist/adapters/banal/index.d.ts.map +1 -0
- package/dist/adapters/banal/index.js +56 -0
- package/dist/adapters/banal/index.js.map +1 -0
- package/dist/adapters/banal/install.d.ts +26 -0
- package/dist/adapters/banal/install.d.ts.map +1 -0
- package/dist/adapters/banal/install.js +15 -0
- package/dist/adapters/banal/install.js.map +1 -0
- package/dist/adapters/banal/invocation.d.ts +48 -0
- package/dist/adapters/banal/invocation.d.ts.map +1 -0
- package/dist/adapters/banal/invocation.js +43 -0
- package/dist/adapters/banal/invocation.js.map +1 -0
- package/dist/adapters/banal/locate.d.ts +50 -0
- package/dist/adapters/banal/locate.d.ts.map +1 -0
- package/dist/adapters/banal/locate.js +34 -0
- package/dist/adapters/banal/locate.js.map +1 -0
- package/dist/adapters/banal/output.d.ts +27 -0
- package/dist/adapters/banal/output.d.ts.map +1 -0
- package/dist/adapters/banal/output.js +112 -0
- package/dist/adapters/banal/output.js.map +1 -0
- package/dist/adapters/banal/pin.d.ts +19 -0
- package/dist/adapters/banal/pin.d.ts.map +1 -0
- package/dist/adapters/banal/pin.js +15 -0
- package/dist/adapters/banal/pin.js.map +1 -0
- package/dist/adapters/banal/probe.d.ts +12 -0
- package/dist/adapters/banal/probe.d.ts.map +1 -0
- package/dist/adapters/banal/probe.js +27 -0
- package/dist/adapters/banal/probe.js.map +1 -0
- package/dist/adapters/banal/run.d.ts +89 -0
- package/dist/adapters/banal/run.d.ts.map +1 -0
- package/dist/adapters/banal/run.js +104 -0
- package/dist/adapters/banal/run.js.map +1 -0
- package/dist/adapters/banal/settings.d.ts +18 -0
- package/dist/adapters/banal/settings.d.ts.map +1 -0
- package/dist/adapters/banal/settings.js +29 -0
- package/dist/adapters/banal/settings.js.map +1 -0
- package/dist/adapters/banal/xml.d.ts +48 -0
- package/dist/adapters/banal/xml.d.ts.map +1 -0
- package/dist/adapters/banal/xml.js +67 -0
- package/dist/adapters/banal/xml.js.map +1 -0
- package/dist/adapters/curl/download.io.d.ts +14 -0
- package/dist/adapters/curl/download.io.d.ts.map +1 -0
- package/dist/adapters/curl/download.io.js +69 -0
- package/dist/adapters/curl/download.io.js.map +1 -0
- package/dist/adapters/curl/index.d.ts +6 -0
- package/dist/adapters/curl/index.d.ts.map +1 -0
- package/dist/adapters/curl/index.js +6 -0
- package/dist/adapters/curl/index.js.map +1 -0
- package/dist/adapters/memory/index.d.ts +43 -0
- package/dist/adapters/memory/index.d.ts.map +1 -0
- package/dist/adapters/memory/index.js +79 -0
- package/dist/adapters/memory/index.js.map +1 -0
- package/dist/adapters/node/files.io.d.ts +3 -0
- package/dist/adapters/node/files.io.d.ts.map +1 -0
- package/dist/adapters/node/files.io.js +31 -0
- package/dist/adapters/node/files.io.js.map +1 -0
- package/dist/adapters/node/host.io.d.ts +3 -0
- package/dist/adapters/node/host.io.d.ts.map +1 -0
- package/dist/adapters/node/host.io.js +14 -0
- package/dist/adapters/node/host.io.js.map +1 -0
- package/dist/adapters/node/index.d.ts +25 -0
- package/dist/adapters/node/index.d.ts.map +1 -0
- package/dist/adapters/node/index.js +14 -0
- package/dist/adapters/node/index.js.map +1 -0
- package/dist/adapters/node/process.io.d.ts +14 -0
- package/dist/adapters/node/process.io.d.ts.map +1 -0
- package/dist/adapters/node/process.io.js +41 -0
- package/dist/adapters/node/process.io.js.map +1 -0
- package/dist/adapters/node/workspace.io.d.ts +4 -0
- package/dist/adapters/node/workspace.io.d.ts.map +1 -0
- package/dist/adapters/node/workspace.io.js +33 -0
- package/dist/adapters/node/workspace.io.js.map +1 -0
- package/dist/adapters/pdfjs/fill.d.ts +42 -0
- package/dist/adapters/pdfjs/fill.d.ts.map +1 -0
- package/dist/adapters/pdfjs/fill.js +91 -0
- package/dist/adapters/pdfjs/fill.js.map +1 -0
- package/dist/build-engine.d.ts +48 -0
- package/dist/build-engine.d.ts.map +1 -0
- package/dist/build-engine.js +148 -0
- package/dist/build-engine.js.map +1 -0
- package/dist/build.d.ts +163 -0
- package/dist/build.d.ts.map +1 -0
- package/dist/build.js +575 -0
- package/dist/build.js.map +1 -0
- package/dist/cli.d.ts +151 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +951 -0
- package/dist/cli.js.map +1 -0
- package/dist/doctor.d.ts +42 -0
- package/dist/doctor.d.ts.map +1 -0
- package/dist/doctor.js +280 -0
- package/dist/doctor.js.map +1 -0
- package/dist/domain/geometry.d.ts +71 -0
- package/dist/domain/geometry.d.ts.map +1 -0
- package/dist/domain/geometry.js +35 -0
- package/dist/domain/geometry.js.map +1 -0
- package/dist/domain/host.d.ts +16 -0
- package/dist/domain/host.d.ts.map +1 -0
- package/dist/domain/host.js +8 -0
- package/dist/domain/host.js.map +1 -0
- package/dist/domain/page-layout.d.ts +34 -0
- package/dist/domain/page-layout.d.ts.map +1 -0
- package/dist/domain/page-layout.js +8 -0
- package/dist/domain/page-layout.js.map +1 -0
- package/dist/domain/paths.d.ts +5 -0
- package/dist/domain/paths.d.ts.map +1 -0
- package/dist/domain/paths.js +2 -0
- package/dist/domain/paths.js.map +1 -0
- package/dist/domain/result.d.ts +23 -0
- package/dist/domain/result.d.ts.map +1 -0
- package/dist/domain/result.js +10 -0
- package/dist/domain/result.js.map +1 -0
- package/dist/domain/sha256.d.ts +7 -0
- package/dist/domain/sha256.d.ts.map +1 -0
- package/dist/domain/sha256.js +14 -0
- package/dist/domain/sha256.js.map +1 -0
- package/dist/domain/text.d.ts +6 -0
- package/dist/domain/text.d.ts.map +1 -0
- package/dist/domain/text.js +7 -0
- package/dist/domain/text.js.map +1 -0
- package/dist/engine.d.ts +93 -0
- package/dist/engine.d.ts.map +1 -0
- package/dist/engine.js +119 -0
- package/dist/engine.js.map +1 -0
- package/dist/exit-code.d.ts +22 -0
- package/dist/exit-code.d.ts.map +1 -0
- package/dist/exit-code.js +10 -0
- package/dist/exit-code.js.map +1 -0
- package/dist/facts-file.d.ts +96 -0
- package/dist/facts-file.d.ts.map +1 -0
- package/dist/facts-file.js +134 -0
- package/dist/facts-file.js.map +1 -0
- package/dist/hooks-settings.d.ts +141 -0
- package/dist/hooks-settings.d.ts.map +1 -0
- package/dist/hooks-settings.js +306 -0
- package/dist/hooks-settings.js.map +1 -0
- package/dist/init.d.ts +201 -0
- package/dist/init.d.ts.map +1 -0
- package/dist/init.js +579 -0
- package/dist/init.js.map +1 -0
- package/dist/latex-log.d.ts +80 -0
- package/dist/latex-log.d.ts.map +1 -0
- package/dist/latex-log.js +187 -0
- package/dist/latex-log.js.map +1 -0
- package/dist/latex-loop.d.ts +129 -0
- package/dist/latex-loop.d.ts.map +1 -0
- package/dist/latex-loop.js +113 -0
- package/dist/latex-loop.js.map +1 -0
- package/dist/link-skills.d.ts +51 -0
- package/dist/link-skills.d.ts.map +1 -0
- package/dist/link-skills.js +199 -0
- package/dist/link-skills.js.map +1 -0
- package/dist/new-paper.d.ts +48 -0
- package/dist/new-paper.d.ts.map +1 -0
- package/dist/new-paper.js +110 -0
- package/dist/new-paper.js.map +1 -0
- package/dist/pdf-facts.d.ts +44 -0
- package/dist/pdf-facts.d.ts.map +1 -0
- package/dist/pdf-facts.js +239 -0
- package/dist/pdf-facts.js.map +1 -0
- package/dist/pdf-geometry.d.ts +170 -0
- package/dist/pdf-geometry.d.ts.map +1 -0
- package/dist/pdf-geometry.js +158 -0
- package/dist/pdf-geometry.js.map +1 -0
- package/dist/ports/download.d.ts +9 -0
- package/dist/ports/download.d.ts.map +1 -0
- package/dist/ports/download.js +2 -0
- package/dist/ports/download.js.map +1 -0
- package/dist/ports/files.d.ts +11 -0
- package/dist/ports/files.d.ts.map +1 -0
- package/dist/ports/files.js +2 -0
- package/dist/ports/files.js.map +1 -0
- package/dist/ports/measure-geometry.d.ts +8 -0
- package/dist/ports/measure-geometry.d.ts.map +1 -0
- package/dist/ports/measure-geometry.js +2 -0
- package/dist/ports/measure-geometry.js.map +1 -0
- package/dist/ports/process.d.ts +45 -0
- package/dist/ports/process.d.ts.map +1 -0
- package/dist/ports/process.js +2 -0
- package/dist/ports/process.js.map +1 -0
- package/dist/ports/tool-installer.d.ts +29 -0
- package/dist/ports/tool-installer.d.ts.map +1 -0
- package/dist/ports/tool-installer.js +2 -0
- package/dist/ports/tool-installer.js.map +1 -0
- package/dist/ports/workspace.d.ts +18 -0
- package/dist/ports/workspace.d.ts.map +1 -0
- package/dist/ports/workspace.js +2 -0
- package/dist/ports/workspace.js.map +1 -0
- package/dist/rules-config.d.ts +34 -0
- package/dist/rules-config.d.ts.map +1 -0
- package/dist/rules-config.js +132 -0
- package/dist/rules-config.js.map +1 -0
- package/dist/structure.d.ts +34 -0
- package/dist/structure.d.ts.map +1 -0
- package/dist/structure.js +149 -0
- package/dist/structure.js.map +1 -0
- package/dist/tex-requirements.d.ts +43 -0
- package/dist/tex-requirements.d.ts.map +1 -0
- package/dist/tex-requirements.js +127 -0
- package/dist/tex-requirements.js.map +1 -0
- package/dist/toolchain.d.ts +159 -0
- package/dist/toolchain.d.ts.map +1 -0
- package/dist/toolchain.js +542 -0
- package/dist/toolchain.js.map +1 -0
- package/dist/types.d.ts +110 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/docs/configuration.md +235 -0
- package/docs/e2e.md +152 -0
- package/docs/incidents.md +59 -0
- package/docs/install.md +170 -0
- package/docs/optional-rules.md +107 -0
- package/docs/package-shape-options.md +262 -0
- package/docs/prior-art/README.md +76 -0
- package/docs/prior-art/blocking-vs-advisory.md +83 -0
- package/docs/prior-art/content-delivery.md +124 -0
- package/docs/prior-art/multi-mode-tools.md +106 -0
- package/docs/prior-art/nondeterministic-checks.md +99 -0
- package/docs/prior-art/package-location.md +422 -0
- package/docs/prior-art/paper-folder-scaffolding.md +538 -0
- package/docs/prior-art/readme-structure.md +69 -0
- package/docs/prior-art/repro/README.md +92 -0
- package/docs/prior-art/repro/claim1-allowedtools.mjs +66 -0
- package/docs/prior-art/repro/claim1-at2.mjs +40 -0
- package/docs/prior-art/repro/claim1-crosschannel.mjs +54 -0
- package/docs/prior-art/repro/claim1-frontmatter.mjs +76 -0
- package/docs/prior-art/repro/claim1-hook-payload-reporter.mjs +10 -0
- package/docs/prior-art/repro/claim1-plugin-frontmatter.mjs +27 -0
- package/docs/prior-art/repro/claim1-plugin-skill.mjs +52 -0
- package/docs/prior-art/repro/claim1-project-skill.mjs +81 -0
- package/docs/prior-art/repro/claim2-marketplace-flat-asclaimed.json +1 -0
- package/docs/prior-art/repro/claim2-marketplace-negative-control.json +1 -0
- package/docs/prior-art/repro/claim2-marketplace-nested-exact.json +9 -0
- package/docs/prior-art/repro/claim2-marketplace-nested-noversion.json +9 -0
- package/docs/prior-art/repro/claim2-marketplace-nested-range.json +1 -0
- package/docs/prior-art/repro/claim3-imports.mjs +50 -0
- package/docs/prior-art/repro/claim4-find-package-json.mjs +8 -0
- package/docs/prior-art/repro/claim4-package-dir.mjs +39 -0
- package/docs/prior-art/repro/claim4-parent-arg.mjs +17 -0
- package/docs/prior-art/repro/claim4-resolve-apis.mjs +21 -0
- package/docs/prior-art/repro/claim4-setup-consumers.mjs +45 -0
- package/docs/prior-art/repro/claim4-yarn-pnp.mjs +70 -0
- package/docs/prior-art/repro/claim5-bin-launch.mjs +39 -0
- package/docs/prior-art/repro/claim5-exports-mutation.mjs +57 -0
- package/docs/prior-art/repro/claim5-resolved-location-and-bin.mjs +33 -0
- package/docs/prior-art/repro/claim6-candidate-ambiguity.mjs +17 -0
- package/docs/prior-art/repro/claim6-doc-path-candidates.mjs +27 -0
- package/docs/prior-art/test-tooling.md +131 -0
- package/docs/rules.md +58 -0
- package/docs/texlive-install-decision.md +230 -0
- package/docs/toolchain.md +152 -0
- package/eslint-rules/doc-fields.harness.mjs +336 -0
- package/eslint-rules/doc-fields.mjs +186 -0
- package/eslint-rules/doc-fields.mutations.mjs +96 -0
- package/eslint-rules/install-path-literals.harness.mjs +121 -0
- package/eslint-rules/install-path-literals.mjs +108 -0
- package/eslint-rules/install-path-literals.mutations.mjs +62 -0
- package/eslint-rules/latex-language.harness.mjs +599 -0
- package/eslint-rules/latex-language.mjs +591 -0
- package/eslint-rules/latex-language.mutations.mjs +196 -0
- package/eslint-rules/paper-research-question.harness.mjs +146 -0
- package/eslint-rules/paper-research-question.mjs +180 -0
- package/eslint-rules/paper-research-question.mutations.mjs +127 -0
- package/eslint-rules/paper-stages.harness.mjs +356 -0
- package/eslint-rules/paper-stages.mjs +455 -0
- package/eslint-rules/paper-stages.mutations.mjs +157 -0
- package/eslint-rules/paper-typography.harness.mjs +291 -0
- package/eslint-rules/paper-typography.mjs +313 -0
- package/eslint-rules/paper-typography.mutations.mjs +131 -0
- package/eslint-rules/papers.harness.mjs +259 -0
- package/eslint-rules/papers.mjs +166 -0
- package/eslint-rules/papers.mutations.mjs +186 -0
- package/eslint-rules/pdf-last-page-balance.harness.mjs +206 -0
- package/eslint-rules/pdf-last-page-balance.mjs +208 -0
- package/eslint-rules/review-findings-cause.harness.mjs +228 -0
- package/eslint-rules/review-findings-cause.mjs +135 -0
- package/eslint-rules/review-findings-cause.mutations.mjs +72 -0
- package/eslint-rules/temp-root-realpath.harness.mjs +176 -0
- package/eslint-rules/temp-root-realpath.mjs +129 -0
- package/eslint-rules/temp-root-realpath.mutations.mjs +99 -0
- package/eslint-rules/tex-build.harness.mjs +753 -0
- package/eslint-rules/tex-build.mjs +322 -0
- package/eslint-rules/tex-build.mutations.mjs +258 -0
- package/eslint.config.mjs +521 -0
- package/fixtures/build-e2e/acmart/PIPELINE-STATUS.md +3 -0
- package/fixtures/build-e2e/acmart/paper.tex +11 -0
- package/fixtures/build-e2e/acmart/venue.json +1 -0
- package/fixtures/build-e2e/broken/PIPELINE-STATUS.md +3 -0
- package/fixtures/build-e2e/broken/paper.tex +7 -0
- package/fixtures/build-e2e/cite/PIPELINE-STATUS.md +3 -0
- package/fixtures/build-e2e/cite/build.sh +5 -0
- package/fixtures/build-e2e/cite/paper.tex +10 -0
- package/fixtures/build-e2e/cite/refs.bib +9 -0
- package/fixtures/build-e2e/empty/PIPELINE-STATUS.md +3 -0
- package/fixtures/build-e2e/empty/paper.tex +6 -0
- package/fixtures/build-e2e/fallback/PIPELINE-STATUS.md +3 -0
- package/fixtures/build-e2e/fallback/paper.tex +11 -0
- package/fixtures/build-e2e/guards/PIPELINE-STATUS.md +3 -0
- package/fixtures/build-e2e/guards/paper.tex +10 -0
- package/fixtures/build-e2e/no-source/PIPELINE-STATUS.md +3 -0
- package/fixtures/build-e2e/unbalanced/PIPELINE-STATUS.md +3 -0
- package/fixtures/build-e2e/unbalanced/paper.tex +28 -0
- package/fixtures/build-e2e/unbalanced/refs.bib +269 -0
- package/fixtures/install-path-literals/clean.fixture.mjs +3 -0
- package/fixtures/install-path-literals/clean.md +15 -0
- package/fixtures/install-path-literals/defect.fixture.mjs +3 -0
- package/fixtures/install-path-literals/defect.md +14 -0
- package/fixtures/latex-language/clean.tex +50 -0
- package/fixtures/latex-language/defect.tex +52 -0
- package/fixtures/paper-research-question/comment-only/PIPELINE-STATUS.md +9 -0
- package/fixtures/paper-research-question/comment-only/paper.tex +7 -0
- package/fixtures/paper-research-question/declared-not-in-paper/PIPELINE-STATUS.md +10 -0
- package/fixtures/paper-research-question/declared-not-in-paper/paper.tex +6 -0
- package/fixtures/paper-research-question/draft/PIPELINE-STATUS.md +6 -0
- package/fixtures/paper-research-question/draft/paper.tex +2 -0
- package/fixtures/paper-research-question/markdown-no-rq/PIPELINE-STATUS.md +9 -0
- package/fixtures/paper-research-question/markdown-no-rq/paper.md +4 -0
- package/fixtures/paper-research-question/shipped-no-rq/PIPELINE-STATUS.md +12 -0
- package/fixtures/paper-research-question/shipped-no-rq/paper.tex +3 -0
- package/fixtures/paper-research-question/shipped-with-rq/PIPELINE-STATUS.md +10 -0
- package/fixtures/paper-research-question/shipped-with-rq/paper.tex +2 -0
- package/fixtures/paper-stages/authors-ran/PIPELINE-STATUS.md +16 -0
- package/fixtures/paper-stages/marker-in-prose/PIPELINE-STATUS.md +17 -0
- package/fixtures/paper-stages/nofile/PIPELINE-STATUS.md +8 -0
- package/fixtures/paper-stages/noheader/PIPELINE-STATUS.md +1 -0
- package/fixtures/paper-stages/noheader/versions/2026-07-22-submitted.pdf +0 -0
- package/fixtures/paper-stages/nothing/PIPELINE-STATUS.md +3 -0
- package/fixtures/paper-stages/ok/PIPELINE-STATUS.md +9 -0
- package/fixtures/paper-stages/ok/versions/2026-07-22-submitted.pdf +0 -0
- package/fixtures/paper-stages/stale/PIPELINE-STATUS.md +1 -0
- package/fixtures/paper-stages/stale/versions/2026-07-22-submitted.STALE-WRONG-FILE.pdf +0 -0
- package/fixtures/paper-stages/twice/PIPELINE-STATUS.md +14 -0
- package/fixtures/paper-stages/twice/versions/2026-08-06-submitted.pdf +0 -0
- package/fixtures/paper-stages/twice/versions/2026-10-24-submitted.pdf +0 -0
- package/fixtures/paper-stages/undeclared/PIPELINE-STATUS.md +8 -0
- package/fixtures/paper-stages/undeclared/versions/2026-07-22-submitted.pdf +0 -0
- package/fixtures/paper-stages/undeclared/versions/2026-08-29-camera-ready.pdf +0 -0
- package/fixtures/paper-stages/wrongsize/PIPELINE-STATUS.md +8 -0
- package/fixtures/paper-stages/wrongsize/versions/2026-07-22-submitted.pdf +0 -0
- package/fixtures/paper-typography/clean-paper/paper.tex +29 -0
- package/fixtures/paper-typography/messy-paper/paper.tex +27 -0
- package/fixtures/pdf-facts/README.md +22 -0
- package/fixtures/pdf-facts/corrupt-font.pdf +0 -0
- package/fixtures/pdf-facts/encrypted.pdf +0 -0
- package/fixtures/pdf-facts/hidden-text.pdf +0 -0
- package/fixtures/pdf-facts/hidden-text.tex +28 -0
- package/fixtures/pdf-facts/t3-all.pdf +0 -0
- package/fixtures/pdf-facts/t3-all.tex +8 -0
- package/fixtures/pdf-facts/t3-mixed.pdf +0 -0
- package/fixtures/pdf-facts/t3-mixed.tex +9 -0
- package/fixtures/pdf-facts/ttf.pdf +2240 -1
- package/fixtures/pdf-facts/ttf.tex +6 -0
- package/fixtures/real-markdown-paper/baseline.json +24 -0
- package/fixtures/real-markdown-paper/baseline.mjs +48 -0
- package/fixtures/render-paper/build-clean.sh +25 -0
- package/fixtures/render-paper/build-defect.sh +15 -0
- package/fixtures/review-findings-cause/clean.md +17 -0
- package/fixtures/review-findings-cause/defect.md +14 -0
- package/fixtures/review-findings-cause/old-debt.md +14 -0
- package/fixtures/review-findings-cause/quiet-in-fence.md +16 -0
- package/fixtures/tex-build/clean.tex +21 -0
- package/fixtures/tex-build/defect.tex +24 -0
- package/fixtures/tex-build/frontmatter-clean.tex +25 -0
- package/fixtures/tex-build/frontmatter-defect.tex +23 -0
- package/fixtures/toolchain-mirror/catalog.txt +5 -0
- package/fixtures/toolchain-mirror/install-tl +27 -0
- package/fixtures/toolchain-mirror/release-texlive.txt +3 -0
- package/fixtures/toolchain-mirror/release-year +1 -0
- package/fixtures/toolchain-mirror/stub-kpsewhich +8 -0
- package/fixtures/toolchain-mirror/stub-pdflatex +3 -0
- package/fixtures/toolchain-mirror/stub-tlmgr +44 -0
- package/hooks/hooks.harness.mjs +713 -0
- package/hooks/hooks.mutations.mjs +337 -0
- package/hooks/paper-edit-guard.hook.d.mts +13 -0
- package/hooks/paper-edit-guard.hook.mjs +457 -0
- package/hooks/paper-skills-nudge.hook.mjs +136 -0
- package/hooks/paper-status-gates.hook.mjs +156 -0
- package/hooks/paper-status-gates.sh +91 -0
- package/lib/agent-cli-version.harness.mjs +165 -0
- package/lib/agent-cli-version.mjs +106 -0
- package/lib/agent-cli-version.mutations.mjs +109 -0
- package/lib/markdown.mjs +386 -0
- package/lib/mutation-driver.harness.mjs +227 -0
- package/lib/mutation-driver.mjs +397 -0
- package/lib/mutation-driver.mutations.mjs +68 -0
- package/lib/paper-config.d.mts +34 -0
- package/lib/paper-config.harness.mjs +286 -0
- package/lib/paper-config.mjs +142 -0
- package/lib/paper-config.mutations.mjs +143 -0
- package/lib/skill-checks.mjs +701 -0
- package/lib/skill-corpus.mjs +403 -0
- package/lib/skill-eval-fixture.mjs +63 -0
- package/lib/skill-eval-kit.mjs +257 -0
- package/lib/skill-trigger-cases.harness.mjs +170 -0
- package/lib/skill-trigger-cases.mjs +446 -0
- package/lib/skill-trigger-cases.mutations.mjs +65 -0
- package/lib/trigger-ledger.mjs +215 -0
- package/package.json +97 -0
- package/plugin/.claude-plugin/plugin.json +8 -0
- package/plugin/hooks/hooks.json +30 -0
- package/scripts/check.harness.mjs +177 -0
- package/scripts/check.mjs +239 -0
- package/scripts/check.mutations.mjs +110 -0
- package/scripts/eslint-report-guard.mjs +82 -0
- package/scripts/exclusive.mjs +138 -0
- package/scripts/harness-api.frozen.json +76 -0
- package/scripts/harness-api.test.ts +175 -0
- package/scripts/layer-legacy-frozen.d.mts +28 -0
- package/scripts/layer-legacy-frozen.mjs +152 -0
- package/scripts/layer-legacy-frozen.test.ts +115 -0
- package/scripts/layer-legacy.frozen.json +50 -0
- package/scripts/mutation-batteries-frozen.harness.mjs +204 -0
- package/scripts/mutation-batteries-frozen.mjs +238 -0
- package/scripts/mutation-batteries.frozen.json +117 -0
- package/scripts/release-config.test.ts +90 -0
- package/scripts/rules-are-content-only.harness.mjs +113 -0
- package/scripts/rules-are-content-only.mjs +138 -0
- package/scripts/rules-are-content-only.mutations.mjs +81 -0
- package/scripts/rules-see-files.harness.mjs +115 -0
- package/scripts/rules-see-files.mjs +99 -0
- package/scripts/rules-see-files.mutations.mjs +131 -0
- package/scripts/run-mutations.mjs +100 -0
- package/scripts/semantic-release-plugins.d.ts +16 -0
- package/skills/README.md +15 -0
- package/skills/analyze-sibling-paper/SKILL.md +170 -0
- package/skills/analyze-sibling-paper/SKILL.md.spec.ts +186 -0
- package/skills/analyze-sibling-paper/analyze-sibling-paper.eval.mjs +19 -0
- package/skills/analyze-sibling-paper/analyze-sibling-paper.harness.mjs +23 -0
- package/skills/argument-arc/SKILL.md +177 -0
- package/skills/argument-arc/SKILL.md.spec.ts +192 -0
- package/skills/argument-arc/argument-arc.eval.mjs +19 -0
- package/skills/argument-arc/argument-arc.harness.mjs +23 -0
- package/skills/build-benchmark/SKILL.md +213 -0
- package/skills/build-benchmark/SKILL.md.spec.ts +220 -0
- package/skills/build-benchmark/build-benchmark.eval.mjs +19 -0
- package/skills/build-benchmark/build-benchmark.harness.mjs +23 -0
- package/skills/build-benchmark/references/adversarial-cold-repro.md +68 -0
- package/skills/camera-ready/SKILL.md +148 -0
- package/skills/camera-ready/SKILL.md.spec.ts +164 -0
- package/skills/camera-ready/camera-ready.eval.mjs +19 -0
- package/skills/camera-ready/camera-ready.harness.mjs +23 -0
- package/skills/cold-read-diff/SKILL.md +160 -0
- package/skills/cold-read-diff/SKILL.md.spec.ts +166 -0
- package/skills/cold-read-diff/cold-read-diff.eval.mjs +19 -0
- package/skills/cold-read-diff/cold-read-diff.harness.mjs +23 -0
- package/skills/draft-paper/SKILL.md +152 -0
- package/skills/draft-paper/SKILL.md.spec.ts +169 -0
- package/skills/draft-paper/draft-paper.eval.mjs +19 -0
- package/skills/draft-paper/draft-paper.harness.mjs +23 -0
- package/skills/extend-paper/SKILL.md +99 -0
- package/skills/extend-paper/SKILL.md.spec.ts +116 -0
- package/skills/extend-paper/extend-paper.eval.mjs +19 -0
- package/skills/extend-paper/extend-paper.harness.mjs +23 -0
- package/skills/find-venue/SKILL.md +128 -0
- package/skills/find-venue/SKILL.md.spec.ts +145 -0
- package/skills/find-venue/find-venue.eval.mjs +19 -0
- package/skills/find-venue/find-venue.harness.mjs +23 -0
- package/skills/grade-paper-writing/SKILL.md +436 -0
- package/skills/grade-paper-writing/SKILL.md.spec.ts +453 -0
- package/skills/grade-paper-writing/fixtures/control_gopen.txt +1 -0
- package/skills/grade-paper-writing/fixtures/control_human_paper.txt +1 -0
- package/skills/grade-paper-writing/fixtures/rewrite.txt +1 -0
- package/skills/grade-paper-writing/fixtures/specimen.txt +1 -0
- package/skills/grade-paper-writing/fixtures/structure-checks.md +22 -0
- package/skills/grade-paper-writing/grade-paper-writing.eval.mjs +19 -0
- package/skills/grade-paper-writing/grade-paper-writing.harness.mjs +23 -0
- package/skills/grade-paper-writing/prose-lint.mjs +713 -0
- package/skills/harden-paper/SKILL.md +318 -0
- package/skills/harden-paper/SKILL.md.spec.ts +336 -0
- package/skills/harden-paper/check-numbers.sh +33 -0
- package/skills/harden-paper/check-release-claims.sh +35 -0
- package/skills/harden-paper/fixtures/uncited-assertions-sample.md +43 -0
- package/skills/harden-paper/fixtures/uncited-assertions-sample.tex +77 -0
- package/skills/harden-paper/harden-paper.eval.mjs +19 -0
- package/skills/harden-paper/harden-paper.harness.mjs +23 -0
- package/skills/map-prior-work/SKILL.md +211 -0
- package/skills/map-prior-work/SKILL.md.spec.ts +227 -0
- package/skills/map-prior-work/map-prior-work.eval.mjs +19 -0
- package/skills/map-prior-work/map-prior-work.harness.mjs +23 -0
- package/skills/osf-artifact-upload/SKILL.md +52 -0
- package/skills/osf-artifact-upload/SKILL.md.spec.ts +59 -0
- package/skills/osf-artifact-upload/osf-artifact-upload.eval.mjs +22 -0
- package/skills/osf-artifact-upload/osf-artifact-upload.harness.mjs +103 -0
- package/skills/paper-adversarial-review/SKILL.md +126 -0
- package/skills/paper-adversarial-review/SKILL.md.spec.ts +142 -0
- package/skills/paper-adversarial-review/paper-adversarial-review.eval.mjs +19 -0
- package/skills/paper-adversarial-review/paper-adversarial-review.harness.mjs +23 -0
- package/skills/paper-pipeline/PIPELINE-MAP.md +371 -0
- package/skills/paper-pipeline/SKILL.md +499 -0
- package/skills/paper-pipeline/SKILL.md.spec.ts +517 -0
- package/skills/paper-pipeline/description-language.eval.mjs +347 -0
- package/skills/paper-pipeline/framing-vs-vocabulary.eval.mjs +891 -0
- package/skills/paper-pipeline/grade-paper-writing-ablation.eval.mjs +1254 -0
- package/skills/paper-pipeline/paper-pipeline.eval.mjs +22 -0
- package/skills/paper-pipeline/paper-pipeline.harness.mjs +143 -0
- package/skills/paper-pipeline/pipeline-firing.baseline.json +270 -0
- package/skills/paper-pipeline/pipeline-firing.eval.mjs +664 -0
- package/skills/paper-pipeline/pipeline-language.eval.mjs +672 -0
- package/skills/paper-pipeline/references/acceptance-gate.md +329 -0
- package/skills/paper-pipeline/references/acl-venue-rules.md +142 -0
- package/skills/paper-pipeline/references/anonymization.md +68 -0
- package/skills/paper-pipeline/references/artifact-checklist.md +93 -0
- package/skills/paper-pipeline/references/body-vs-appendix.md +97 -0
- package/skills/paper-pipeline/references/credit-criteria.md +69 -0
- package/skills/paper-pipeline/references/occupancy-2026-08-06-probe/README.md +35 -0
- package/skills/paper-pipeline/references/occupancy-2026-08-06-probe/run_retext.mjs +24 -0
- package/skills/paper-pipeline/references/occupancy-2026-08-06-probe/sentences.txt +11 -0
- package/skills/paper-pipeline/references/occupancy-2026-08-06-probe/test_sentences.py +25 -0
- package/skills/paper-pipeline/references/occupancy-2026-08-06-prose-checkers.md +538 -0
- package/skills/paper-pipeline/references/occupancy-2026-08-06-reproducible-tooling.md +431 -0
- package/skills/paper-pipeline/references/occupancy-2026-08-06-staleness-and-orchestration.md +592 -0
- package/skills/paper-pipeline/references/pipeline-status-template.md +162 -0
- package/skills/paper-pipeline/references/review-ratchet.md +36 -0
- package/skills/paper-pipeline/references/sweep-2026-08-09-ideal-pipeline.md +585 -0
- package/skills/paper-pipeline/references/writing-craft.md +448 -0
- package/skills/paper-pipeline/repro/2026-08-07-description-language-control.log +63 -0
- package/skills/paper-pipeline/repro/2026-08-07-fork-check.log +52 -0
- package/skills/paper-pipeline/repro/2026-08-07-fork-check2.log +33 -0
- package/skills/paper-pipeline/repro/2026-08-07-language-eval-pilot.log +33 -0
- package/skills/paper-pipeline/repro/2026-08-07-language-eval-raw.log +166 -0
- package/skills/paper-pipeline/repro/2026-08-08-framing-vs-vocabulary-oracle.json +338 -0
- package/skills/paper-pipeline/repro/2026-08-08-framing-vs-vocabulary-oracle.log +118 -0
- package/skills/paper-pipeline/repro/2026-08-08-framing-vs-vocabulary-raw.log +245 -0
- package/skills/paper-pipeline/repro/2026-08-08-framing-vs-vocabulary.json +776 -0
- package/skills/paper-pipeline/repro/2026-08-08-grade-paper-writing-ablation-A6-oracle.log +53 -0
- package/skills/paper-pipeline/repro/2026-08-08-grade-paper-writing-ablation-A6-raw.log +89 -0
- package/skills/paper-pipeline/repro/2026-08-08-grade-paper-writing-ablation-oracle.json +450 -0
- package/skills/paper-pipeline/repro/2026-08-08-grade-paper-writing-ablation-oracle.log +136 -0
- package/skills/paper-pipeline/repro/2026-08-08-grade-paper-writing-ablation-raw.log +242 -0
- package/skills/paper-pipeline/repro/2026-08-08-grade-paper-writing-ablation-setupdiff.log +59 -0
- package/skills/paper-pipeline/repro/2026-08-08-grade-paper-writing-ablation.json +1032 -0
- package/skills/paper-pipeline/repro/2026-08-08-parent-replication-gpw.json +139 -0
- package/skills/paper-pipeline/repro/2026-08-08-parent-replication-gpw.log +98 -0
- package/skills/paper-pipeline/repro/2026-08-08-parent-replication.mjs +92 -0
- package/skills/paper-pipeline/repro/README.md +129 -0
- package/skills/paper-pipeline/repro/analyze-language-eval.py +116 -0
- package/skills/paper-pipeline/scripts/README.md +344 -0
- package/skills/paper-pipeline/scripts/announce.mjs +67 -0
- package/skills/paper-pipeline/scripts/artifact-coverage.harness.mjs +496 -0
- package/skills/paper-pipeline/scripts/artifact-coverage.mjs +397 -0
- package/skills/paper-pipeline/scripts/artifact-coverage.mutations.mjs +218 -0
- package/skills/paper-pipeline/scripts/check-provenance.mjs +184 -0
- package/skills/paper-pipeline/scripts/consumer.d.mts +32 -0
- package/skills/paper-pipeline/scripts/consumer.harness.mjs +562 -0
- package/skills/paper-pipeline/scripts/consumer.mjs +535 -0
- package/skills/paper-pipeline/scripts/consumer.mutations.mjs +190 -0
- package/skills/paper-pipeline/scripts/extract-ref-facts.harness.mjs +457 -0
- package/skills/paper-pipeline/scripts/extract-ref-facts.mjs +656 -0
- package/skills/paper-pipeline/scripts/extract-ref-facts.mutations.mjs +54 -0
- package/skills/paper-pipeline/scripts/fixtures/clean/PIPELINE-STATUS.md +51 -0
- package/skills/paper-pipeline/scripts/fixtures/dirty/PIPELINE-STATUS.md +52 -0
- package/skills/paper-pipeline/scripts/fixtures/dirty/paper.md +6 -0
- package/skills/paper-pipeline/scripts/fixtures/real-bib/refs.bib +153 -0
- package/skills/paper-pipeline/scripts/generated-code.harness.mjs +466 -0
- package/skills/paper-pipeline/scripts/generated-code.mjs +338 -0
- package/skills/paper-pipeline/scripts/generated-code.mutations.mjs +254 -0
- package/skills/paper-pipeline/scripts/ledger.mjs +623 -0
- package/skills/paper-pipeline/scripts/ledger.selftest.mjs +286 -0
- package/skills/paper-pipeline/scripts/pipeline-check.harness.mjs +389 -0
- package/skills/paper-pipeline/scripts/pipeline-check.mjs +737 -0
- package/skills/paper-pipeline/scripts/pipeline-check.mutations.mjs +54 -0
- package/skills/paper-pipeline/scripts/pipeline-edges.mjs +169 -0
- package/skills/paper-pipeline/scripts/population-map.harness.mjs +178 -0
- package/skills/paper-pipeline/scripts/population-map.mjs +181 -0
- package/skills/paper-pipeline/scripts/population-map.mutations.mjs +65 -0
- package/skills/paper-pipeline/scripts/population-map.selftest.mjs +122 -0
- package/skills/paper-pipeline/scripts/provenance.harness.mjs +240 -0
- package/skills/paper-pipeline/scripts/provenance.mutations.mjs +59 -0
- package/skills/paper-pipeline/scripts/round-diff.harness.mjs +881 -0
- package/skills/paper-pipeline/scripts/round-diff.mjs +576 -0
- package/skills/paper-pipeline/scripts/round-diff.mutations.mjs +276 -0
- package/skills/paper-pipeline/scripts/run-mechanical.mjs +633 -0
- package/skills/paper-pipeline/scripts/status.mjs +295 -0
- package/skills/paper-status/SKILL.md +183 -0
- package/skills/paper-status/SKILL.md.spec.ts +190 -0
- package/skills/paper-status/paper-status.eval.mjs +22 -0
- package/skills/paper-status/paper-status.harness.mjs +25 -0
- package/skills/pc-panel-review/SKILL.md +263 -0
- package/skills/pc-panel-review/SKILL.md.spec.ts +280 -0
- package/skills/pc-panel-review/pc-panel-review.eval.mjs +19 -0
- package/skills/pc-panel-review/pc-panel-review.harness.mjs +23 -0
- package/skills/plan-paper-timeline/SKILL.md +182 -0
- package/skills/plan-paper-timeline/SKILL.md.spec.ts +200 -0
- package/skills/plan-paper-timeline/fixtures/fake-google-calendar.mjs +239 -0
- package/skills/plan-paper-timeline/plan-paper-timeline.effects.harness.mjs +431 -0
- package/skills/plan-paper-timeline/plan-paper-timeline.effects.mutations.mjs +65 -0
- package/skills/plan-paper-timeline/plan-paper-timeline.eval.mjs +19 -0
- package/skills/plan-paper-timeline/plan-paper-timeline.harness.mjs +23 -0
- package/skills/render-paper/SKILL.md +159 -0
- package/skills/render-paper/SKILL.md.spec.ts +166 -0
- package/skills/render-paper/check-render.sh +419 -0
- package/skills/render-paper/checkers-requirements.txt +55 -0
- package/skills/render-paper/ensure-checkers.sh +69 -0
- package/skills/render-paper/extract-pdf-facts.harness.mjs +166 -0
- package/skills/render-paper/extract-pdf-facts.mjs +144 -0
- package/skills/render-paper/render-paper.eval.mjs +19 -0
- package/skills/render-paper/render-paper.harness.mjs +339 -0
- package/skills/research-ideate/SKILL.md +136 -0
- package/skills/research-ideate/SKILL.md.spec.ts +152 -0
- package/skills/research-ideate/research-ideate.eval.mjs +19 -0
- package/skills/research-ideate/research-ideate.harness.mjs +23 -0
- package/skills/skill-contract.mutations.mjs +179 -0
- package/skills/study-accepted-papers/SKILL.md +206 -0
- package/skills/study-accepted-papers/SKILL.md.spec.ts +223 -0
- package/skills/study-accepted-papers/study-accepted-papers.eval.mjs +19 -0
- package/skills/study-accepted-papers/study-accepted-papers.harness.mjs +23 -0
- package/skills/submit-paper/SKILL.md +182 -0
- package/skills/submit-paper/SKILL.md.spec.ts +199 -0
- package/skills/submit-paper/check-deanon.sh +149 -0
- package/skills/submit-paper/references/publishers/acm.md +92 -0
- package/skills/submit-paper/references/venues/agenticdev.jsonc +108 -0
- package/skills/submit-paper/references/venues/agenticdev.md +139 -0
- package/skills/submit-paper/references/venues/agenticdev.tex +19 -0
- package/skills/submit-paper/references/venues/aisec.jsonc +101 -0
- package/skills/submit-paper/references/venues/aisec.md +105 -0
- package/skills/submit-paper/references/venues/paper-guards.tex +41 -0
- package/skills/submit-paper/references/venues/realm.jsonc +81 -0
- package/skills/submit-paper/references/venues/realm.md +155 -0
- package/skills/submit-paper/references/venues/tex-base.jsonc +50 -0
- package/skills/submit-paper/references/venues/venue-profile.schema.json +74 -0
- package/skills/submit-paper/submit-paper.eval.mjs +19 -0
- package/skills/submit-paper/submit-paper.harness.mjs +23 -0
- package/skills/sweep-design-space/SKILL.md +269 -0
- package/skills/sweep-design-space/SKILL.md.spec.ts +285 -0
- package/skills/sweep-design-space/sweep-design-space.eval.mjs +19 -0
- package/skills/sweep-design-space/sweep-design-space.harness.mjs +23 -0
- package/skills/tighten-paper/SKILL.md +368 -0
- package/skills/tighten-paper/SKILL.md.spec.ts +384 -0
- package/skills/tighten-paper/structure.mjs +371 -0
- package/skills/tighten-paper/tighten-paper.eval.mjs +19 -0
- package/skills/tighten-paper/tighten-paper.harness.mjs +23 -0
- package/skills/verify-citations/SKILL.md +328 -0
- package/skills/verify-citations/SKILL.md.spec.ts +345 -0
- package/skills/verify-citations/scripts/bib-authors.mjs +479 -0
- package/skills/verify-citations/scripts/bib-authors.test.mjs +175 -0
- package/skills/verify-citations/scripts/verify-cites.mjs +1108 -0
- package/skills/verify-citations/scripts/verify-cites.test.mjs +735 -0
- package/skills/verify-citations/verify-citations.eval.mjs +19 -0
- package/skills/verify-citations/verify-citations.harness.mjs +23 -0
- package/src/CLAUDE.md +51 -0
- package/src/action-ref.test.ts +26 -0
- package/src/action-ref.ts +15 -0
- package/src/adapters/banal/failure.test.ts +63 -0
- package/src/adapters/banal/failure.ts +118 -0
- package/src/adapters/banal/index.test.ts +119 -0
- package/src/adapters/banal/index.ts +100 -0
- package/src/adapters/banal/install.test.ts +20 -0
- package/src/adapters/banal/install.ts +41 -0
- package/src/adapters/banal/invocation.test.ts +74 -0
- package/src/adapters/banal/invocation.ts +95 -0
- package/src/adapters/banal/locate.test.ts +52 -0
- package/src/adapters/banal/locate.ts +84 -0
- package/src/adapters/banal/output.test.ts +140 -0
- package/src/adapters/banal/output.ts +141 -0
- package/src/adapters/banal/pin.ts +30 -0
- package/src/adapters/banal/probe.ts +35 -0
- package/src/adapters/banal/run.test.ts +191 -0
- package/src/adapters/banal/run.ts +244 -0
- package/src/adapters/banal/settings.test.ts +31 -0
- package/src/adapters/banal/settings.ts +55 -0
- package/src/adapters/banal/xml.test.ts +111 -0
- package/src/adapters/banal/xml.ts +112 -0
- package/src/adapters/curl/download.io.ts +73 -0
- package/src/adapters/curl/download.test.ts +55 -0
- package/src/adapters/curl/index.ts +5 -0
- package/src/adapters/memory/index.ts +131 -0
- package/src/adapters/node/files.io.ts +39 -0
- package/src/adapters/node/files.test.ts +28 -0
- package/src/adapters/node/host.io.ts +15 -0
- package/src/adapters/node/index.ts +36 -0
- package/src/adapters/node/process.io.ts +49 -0
- package/src/adapters/node/process.test.ts +46 -0
- package/src/adapters/node/workspace.io.ts +40 -0
- package/src/adapters/node/workspace.test.ts +58 -0
- package/src/adapters/pdfjs/fill.test.ts +111 -0
- package/src/adapters/pdfjs/fill.ts +141 -0
- package/src/build-engine.harness.mjs +314 -0
- package/src/build-engine.ts +219 -0
- package/src/build.harness.mjs +631 -0
- package/src/build.mutations.mjs +195 -0
- package/src/build.ts +793 -0
- package/src/cli.harness.mjs +2007 -0
- package/src/cli.mutations.mjs +448 -0
- package/src/cli.ts +1189 -0
- package/src/doctor.harness.mjs +396 -0
- package/src/doctor.mutations.mjs +175 -0
- package/src/doctor.ts +356 -0
- package/src/domain/geometry.ts +108 -0
- package/src/domain/host.ts +23 -0
- package/src/domain/page-layout.ts +32 -0
- package/src/domain/paths.ts +5 -0
- package/src/domain/result.test.ts +26 -0
- package/src/domain/result.ts +29 -0
- package/src/domain/sha256.test.ts +12 -0
- package/src/domain/sha256.ts +21 -0
- package/src/domain/text.ts +11 -0
- package/src/engine.harness.mjs +252 -0
- package/src/engine.ts +176 -0
- package/src/exit-code.test.ts +21 -0
- package/src/exit-code.ts +38 -0
- package/src/facts-file.test.ts +240 -0
- package/src/facts-file.ts +241 -0
- package/src/hooks-settings.harness.mjs +386 -0
- package/src/hooks-settings.mutations.mjs +116 -0
- package/src/hooks-settings.ts +434 -0
- package/src/init.ts +900 -0
- package/src/latex-log.harness.mjs +226 -0
- package/src/latex-log.ts +234 -0
- package/src/latex-loop.harness.mjs +449 -0
- package/src/latex-loop.ts +211 -0
- package/src/link-skills.harness.mjs +273 -0
- package/src/link-skills.mutations.mjs +136 -0
- package/src/link-skills.ts +258 -0
- package/src/new-paper.harness.mjs +216 -0
- package/src/new-paper.mutations.mjs +79 -0
- package/src/new-paper.ts +158 -0
- package/src/pdf-facts.harness.mjs +188 -0
- package/src/pdf-facts.ts +327 -0
- package/src/pdf-geometry.harness.mjs +254 -0
- package/src/pdf-geometry.ts +300 -0
- package/src/ports/download.ts +10 -0
- package/src/ports/files.ts +11 -0
- package/src/ports/measure-geometry.ts +8 -0
- package/src/ports/process.ts +46 -0
- package/src/ports/tool-installer.ts +33 -0
- package/src/ports/workspace.ts +20 -0
- package/src/rules-config.harness.mjs +114 -0
- package/src/rules-config.ts +178 -0
- package/src/structure.harness.mjs +179 -0
- package/src/structure.mutations.mjs +83 -0
- package/src/structure.ts +166 -0
- package/src/tex-requirements.harness.mjs +238 -0
- package/src/tex-requirements.ts +181 -0
- package/src/toolchain.harness.mjs +651 -0
- package/src/toolchain.ts +755 -0
- package/src/types.ts +106 -0
- package/templates/paper/PIPELINE-STATUS.md +72 -0
- package/templates/paper/paper.md +4 -0
- package/templates/paper/paper.tex +8 -0
- package/tsconfig.json +23 -0
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mutation battery for `eslint-rules/papers.mjs`.
|
|
3
|
+
* Run: `node eslint-rules/papers.mutations.mjs` (not `vigiles test` — this is not a harness).
|
|
4
|
+
*
|
|
5
|
+
* 🔴 EVERY MUTATION CARRIES A "THE PATCH LANDED" ASSERTION: the file is re-read FROM DISK, the
|
|
6
|
+
* mutant must be in it, and after the rollback the contents must return byte for byte. In this
|
|
7
|
+
* project a green mutation has repeatedly meant not "the defence holds" but "the patch never
|
|
8
|
+
* landed" / "the test is looking somewhere else".
|
|
9
|
+
*
|
|
10
|
+
* WHAT A GREEN HARNESS UNDER A MUTATION MEANS: a finding about the TEST, not a conclusion
|
|
11
|
+
* about the defence. Rank the causes in this order — the test greps PROSE ABOUT a value
|
|
12
|
+
* instead of the value · the test looks in the wrong place · the test is the wrong test · the
|
|
13
|
+
* patch did not land · the set is too narrow · the run was already red BEFORE the mutation.
|
|
14
|
+
* The last one is closed by the baseline run below.
|
|
15
|
+
*/
|
|
16
|
+
import { readFileSync, writeFileSync } from "node:fs";
|
|
17
|
+
import { execFileSync } from "node:child_process";
|
|
18
|
+
|
|
19
|
+
const RULES = "eslint-rules/papers.mjs";
|
|
20
|
+
const HARNESS = "eslint-rules/papers.harness.mjs";
|
|
21
|
+
|
|
22
|
+
// Coverage mode — see `scripts/run-mutations.mjs` guard 2 and the block in
|
|
23
|
+
// `lib/mutation-driver.mjs`. Says what this battery can kill, without touching a file.
|
|
24
|
+
if (process.env.MUTATIONS_REPORT_COVERAGE) {
|
|
25
|
+
console.log(`MUTATION-COVERS\t${HARNESS}`);
|
|
26
|
+
process.exit(0);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const PRISTINE = readFileSync(RULES, "utf8");
|
|
30
|
+
|
|
31
|
+
const M = [
|
|
32
|
+
[
|
|
33
|
+
"existsSync/the central refusal",
|
|
34
|
+
"stop checking that the root is on disk — a wrong root stops being an error and becomes a " +
|
|
35
|
+
"run that lints zero files and exits 0, which reads exactly like a clean pass",
|
|
36
|
+
" if (!existsSync(resolve(baseDir, root)))",
|
|
37
|
+
" if (false)",
|
|
38
|
+
],
|
|
39
|
+
[
|
|
40
|
+
"baseDir/ignored in favour of cwd",
|
|
41
|
+
"resolve against `process.cwd()` instead of the given base — passes every run started from " +
|
|
42
|
+
"the repository root and fails in a worktree or from an editor started elsewhere",
|
|
43
|
+
" if (!existsSync(resolve(baseDir, root)))",
|
|
44
|
+
" if (!existsSync(resolve(process.cwd(), root)))",
|
|
45
|
+
],
|
|
46
|
+
[
|
|
47
|
+
"default/absence",
|
|
48
|
+
"drop the default — a consumer that declares nothing gets `undefined` instead of `papers`",
|
|
49
|
+
" const root = declared === undefined ? DEFAULT_PAPERS_ROOT : declared;",
|
|
50
|
+
" const root = declared;",
|
|
51
|
+
],
|
|
52
|
+
[
|
|
53
|
+
"declared === undefined/null read as absence",
|
|
54
|
+
'go back to `??` — an explicit `"papersDir": null` is read as «nothing was declared» and ' +
|
|
55
|
+
"silently falls back to the default, i.e. a typed keystroke treated as an absence",
|
|
56
|
+
" const root = declared === undefined ? DEFAULT_PAPERS_ROOT : declared;",
|
|
57
|
+
" const root = declared ?? DEFAULT_PAPERS_ROOT;",
|
|
58
|
+
],
|
|
59
|
+
[
|
|
60
|
+
"type guard/empty string",
|
|
61
|
+
"accept any string, including the empty one — every glob becomes `/*/paper.tex`, rooted at " +
|
|
62
|
+
"the filesystem, matching nothing, silently",
|
|
63
|
+
' if (typeof root !== "string" || root.length === 0)',
|
|
64
|
+
' if (typeof root !== "string")',
|
|
65
|
+
],
|
|
66
|
+
[
|
|
67
|
+
"return shape/absolute path",
|
|
68
|
+
"return the RESOLVED root — ESLint interprets `files:` globs relative to the config, so an " +
|
|
69
|
+
"absolute path there is a different pattern, and the breakage shows up as silence",
|
|
70
|
+
" return root;\n}",
|
|
71
|
+
" return resolve(baseDir, root);\n}",
|
|
72
|
+
],
|
|
73
|
+
[
|
|
74
|
+
"message/which value was declared",
|
|
75
|
+
"drop the declared value from the error — the reader is told something is missing but not " +
|
|
76
|
+
"which of several candidate paths the tool was looking at",
|
|
77
|
+
'the papers root "${root}" does not exist under ${baseDir}.',
|
|
78
|
+
"the papers root does not exist.",
|
|
79
|
+
],
|
|
80
|
+
[
|
|
81
|
+
"message/default vs declared",
|
|
82
|
+
"collapse the two failures into one message — «you declared the wrong thing» and «you " +
|
|
83
|
+
"declared nothing and the default does not fit» have different fixes",
|
|
84
|
+
" (declared === undefined",
|
|
85
|
+
" (false",
|
|
86
|
+
],
|
|
87
|
+
[
|
|
88
|
+
"paperFiles/one glob set stops interpolating the root",
|
|
89
|
+
"hard-code the `tex` set — the block keeps matching whatever layout the author had in mind",
|
|
90
|
+
" tex: [`${root}/*/paper.tex`],",
|
|
91
|
+
" tex: [`docs/papers/*/paper.tex`],",
|
|
92
|
+
],
|
|
93
|
+
[
|
|
94
|
+
"paperFiles/a set disappears",
|
|
95
|
+
"drop `status` — a consumer's block named it, so that block now lints nothing at all",
|
|
96
|
+
" status: [`${root}/*/PIPELINE-STATUS.md`],",
|
|
97
|
+
"",
|
|
98
|
+
],
|
|
99
|
+
];
|
|
100
|
+
|
|
101
|
+
const runHarness = () => {
|
|
102
|
+
try {
|
|
103
|
+
execFileSync("npx", ["vigiles", "test", HARNESS], {
|
|
104
|
+
encoding: "utf8",
|
|
105
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
106
|
+
});
|
|
107
|
+
return { failed: false, out: "" };
|
|
108
|
+
} catch (e) {
|
|
109
|
+
return { failed: true, out: (e.stdout ?? "") + (e.stderr ?? "") };
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
// 🔴 A BASELINE RUN BEFORE ANY MUTATION. A red harness makes EVERY mutation look "killed", and
|
|
114
|
+
// the battery would print a confident green verdict about a defence that is not there.
|
|
115
|
+
{
|
|
116
|
+
const base = runHarness();
|
|
117
|
+
if (base.failed) {
|
|
118
|
+
console.log(
|
|
119
|
+
"❌ THE HARNESS IS RED BEFORE ANY MUTATION — the battery cannot tell a killed mutation " +
|
|
120
|
+
"from that:\n" +
|
|
121
|
+
base.out.slice(-1500),
|
|
122
|
+
);
|
|
123
|
+
process.exit(1);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
let ok = 0;
|
|
128
|
+
let bad = 0;
|
|
129
|
+
const rows = [];
|
|
130
|
+
for (const [label, what, from, to] of M) {
|
|
131
|
+
const hits = PRISTINE.split(from).length - 1;
|
|
132
|
+
if (hits !== 1) {
|
|
133
|
+
console.log(
|
|
134
|
+
`❌ ${label}: TARGET ${hits === 0 ? "NOT FOUND" : `NOT UNIQUE (${hits})`} — ${from.slice(0, 70)}`,
|
|
135
|
+
);
|
|
136
|
+
bad++;
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
writeFileSync(RULES, PRISTINE.replace(from, to));
|
|
140
|
+
const on = readFileSync(RULES, "utf8");
|
|
141
|
+
if (!on.includes(to) || on.includes(from)) {
|
|
142
|
+
console.log(
|
|
143
|
+
`❌ ${label}: THE MUTATION DID NOT LAND (checked by re-reading the file)`,
|
|
144
|
+
);
|
|
145
|
+
bad++;
|
|
146
|
+
writeFileSync(RULES, PRISTINE);
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
const { failed, out } = runHarness();
|
|
150
|
+
writeFileSync(RULES, PRISTINE);
|
|
151
|
+
if (readFileSync(RULES, "utf8") !== PRISTINE) {
|
|
152
|
+
console.log(`❌ ${label}: THE ROLLBACK FAILED`);
|
|
153
|
+
bad++;
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
// The verdict carries the HARNESS LINE and the assertion text: "killed" without saying by
|
|
157
|
+
// what is half an answer. The battery must show that each mutation died on ITS OWN assertion.
|
|
158
|
+
const at = (out.match(/papers\.harness\.mjs:(\d+)/) ?? [])[1];
|
|
159
|
+
const msg =
|
|
160
|
+
(out.match(/AssertionError[^:]*: ([^\n]+)/) ?? [])[1] ??
|
|
161
|
+
// Not every mutation dies on an assertion: removing the `try/catch` crashes the linter
|
|
162
|
+
// itself, and then "what it died on" is a thrown error. Without this branch the column
|
|
163
|
+
// would come out EMPTY — "killed by something unknown", which is the half-answer this
|
|
164
|
+
// battery forbids.
|
|
165
|
+
(out.match(/((?:Error|ENOENT)[^\n]*)/) ?? [])[1] ??
|
|
166
|
+
(out.match(
|
|
167
|
+
/([^\n]*(?:must|the wrong|drifted|was lost|did not fire)[^\n]*)/,
|
|
168
|
+
) ?? [])[1] ??
|
|
169
|
+
"";
|
|
170
|
+
const where = (at ? `harness:${at} · ` : "") + msg;
|
|
171
|
+
if (failed) {
|
|
172
|
+
ok++;
|
|
173
|
+
rows.push([label, what, where.trim().slice(0, 130)]);
|
|
174
|
+
} else {
|
|
175
|
+
console.log(
|
|
176
|
+
`🔴 ${label}: THE HARNESS IS GREEN UNDER THE MUTATION — a finding about the TEST, not a conclusion about the defence`,
|
|
177
|
+
);
|
|
178
|
+
bad++;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
console.log("\n| property | mutation | what the harness died on |");
|
|
182
|
+
console.log("|---|---|---|");
|
|
183
|
+
for (const [a, b, c] of rows)
|
|
184
|
+
console.log(`| \`${a}\` | ${b} | ${c.replace(/\|/g, "\\|")} |`);
|
|
185
|
+
console.log(`\n${ok} mutation(s) killed, ${bad} problem(s)`);
|
|
186
|
+
process.exit(bad ? 1 : 0);
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Colocated test for `eslint-rules/pdf-last-page-balance.mjs` (`pdf/last-page-balance`).
|
|
3
|
+
* Run: `npx vigiles test eslint-rules/pdf-last-page-balance.harness.mjs`
|
|
4
|
+
*
|
|
5
|
+
* The rule is driven the way a consumer drives it: through rpp's own `buildConfig`, with the
|
|
6
|
+
* rule turned on by a `rules` block in the settings — not through a config invented here. Each
|
|
7
|
+
* paper is a temporary directory holding `paper.tex`, `paper.pdf` and the facts `paperlint build`
|
|
8
|
+
* would write beside them.
|
|
9
|
+
*
|
|
10
|
+
* Both halves: it FIRES on an unbalanced page and on every input it cannot judge (no facts, a
|
|
11
|
+
* foreign schema, facts about another PDF), and it is QUIET on a balanced page, a stub, a review
|
|
12
|
+
* build, a file that is not paper.tex — and when nobody turned it on.
|
|
13
|
+
*/
|
|
14
|
+
import assert from "node:assert/strict";
|
|
15
|
+
import { createHash } from "node:crypto";
|
|
16
|
+
import {
|
|
17
|
+
mkdirSync,
|
|
18
|
+
mkdtempSync,
|
|
19
|
+
realpathSync,
|
|
20
|
+
rmSync,
|
|
21
|
+
writeFileSync,
|
|
22
|
+
} from "node:fs";
|
|
23
|
+
import { tmpdir } from "node:os";
|
|
24
|
+
import { dirname, join } from "node:path";
|
|
25
|
+
import { fileURLToPath } from "node:url";
|
|
26
|
+
import { ESLint } from "eslint";
|
|
27
|
+
import { texLanguage } from "./latex-language.mjs";
|
|
28
|
+
|
|
29
|
+
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
30
|
+
const { parseFacts, judgeColumns, DEFAULT_TOLERANCE_PT } = await import(
|
|
31
|
+
join(HERE, "pdf-last-page-balance.mjs")
|
|
32
|
+
);
|
|
33
|
+
const { buildConfig } = await import(join(HERE, "..", "src", "cli.ts"));
|
|
34
|
+
|
|
35
|
+
let n = 0;
|
|
36
|
+
const check = (label, cond, detail = "") => {
|
|
37
|
+
assert.ok(cond, detail ? `${label} — ${detail}` : label);
|
|
38
|
+
n++;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const TEX =
|
|
42
|
+
"% a paper\n\\documentclass[sigconf]{acmart}\n\\begin{document}x\\end{document}\n";
|
|
43
|
+
const PDF = "%PDF-1.5 pretend";
|
|
44
|
+
const sha = (s) => createHash("sha256").update(s).digest("hex");
|
|
45
|
+
const facts = (lastPage, over = {}) => ({
|
|
46
|
+
schema: 2,
|
|
47
|
+
pdf: "paper.pdf",
|
|
48
|
+
pdf_sha256: sha(PDF),
|
|
49
|
+
last_page: lastPage,
|
|
50
|
+
...over,
|
|
51
|
+
});
|
|
52
|
+
const MEASURED = (l, r) => ({ kind: "measured", columns_pt: [l, r] });
|
|
53
|
+
|
|
54
|
+
const root = realpathSync(mkdtempSync(join(tmpdir(), "rpp-balance-rule-")));
|
|
55
|
+
/** A paper directory with paper.tex, paper.pdf and, unless `null`, `_build/paper.facts.json`. */
|
|
56
|
+
const paper = (name, f, { pdf = PDF } = {}) => {
|
|
57
|
+
const dir = join(root, "papers", name);
|
|
58
|
+
mkdirSync(join(dir, "_build"), { recursive: true });
|
|
59
|
+
writeFileSync(join(dir, "paper.tex"), TEX);
|
|
60
|
+
writeFileSync(join(dir, "NOTES.md"), "# notes\n");
|
|
61
|
+
if (pdf !== null) writeFileSync(join(dir, "paper.pdf"), pdf);
|
|
62
|
+
if (f !== null)
|
|
63
|
+
writeFileSync(
|
|
64
|
+
join(dir, "_build", "paper.facts.json"),
|
|
65
|
+
typeof f === "string" ? f : JSON.stringify(f),
|
|
66
|
+
);
|
|
67
|
+
return dir;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
/** Lint files with rpp's config, the rule turned on for `papers/**` (unless `rules` says otherwise). */
|
|
71
|
+
async function lint(
|
|
72
|
+
files,
|
|
73
|
+
rules = [
|
|
74
|
+
{
|
|
75
|
+
basePath: root,
|
|
76
|
+
files: ["papers/**"],
|
|
77
|
+
rules: { "pdf/last-page-balance": "error" },
|
|
78
|
+
},
|
|
79
|
+
],
|
|
80
|
+
) {
|
|
81
|
+
const eslint = new ESLint({
|
|
82
|
+
cwd: root,
|
|
83
|
+
overrideConfigFile: true,
|
|
84
|
+
overrideConfig: buildConfig({ rules }, texLanguage),
|
|
85
|
+
});
|
|
86
|
+
const results = await eslint.lintFiles(files);
|
|
87
|
+
return results.flatMap((r) =>
|
|
88
|
+
r.messages.map((m) => ({ file: r.filePath, ...m })),
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
const only = (msgs) => msgs.filter((m) => m.ruleId === "pdf/last-page-balance");
|
|
92
|
+
|
|
93
|
+
try {
|
|
94
|
+
// ── pure: parsing the facts and judging the columns ──────────────────────────────────
|
|
95
|
+
check(
|
|
96
|
+
"parseFacts: a schema-1 file is refused by name",
|
|
97
|
+
parseFacts(JSON.stringify({ schema: 1 })).messageId === "schema",
|
|
98
|
+
);
|
|
99
|
+
check(
|
|
100
|
+
"parseFacts: not JSON is `factsBroken`, not a crash",
|
|
101
|
+
parseFacts("{").messageId === "factsBroken",
|
|
102
|
+
);
|
|
103
|
+
check(
|
|
104
|
+
"parseFacts: a measured page without two finite heights is `factsBroken`",
|
|
105
|
+
parseFacts(JSON.stringify(facts({ kind: "measured", columns_pt: [1] })))
|
|
106
|
+
.messageId === "factsBroken",
|
|
107
|
+
);
|
|
108
|
+
check(
|
|
109
|
+
"judgeColumns: at the tolerance it is balanced; one point more is not",
|
|
110
|
+
judgeColumns({ kind: "measured", left: 220, right: 100 }, 120) === null &&
|
|
111
|
+
judgeColumns({ kind: "measured", left: 221, right: 100 }, 120)
|
|
112
|
+
?.messageId === "unbalanced",
|
|
113
|
+
);
|
|
114
|
+
check("the default tolerance is 120 pt", DEFAULT_TOLERANCE_PT === 120);
|
|
115
|
+
|
|
116
|
+
// ── fires ───────────────────────────────────────────────────────────────────────────
|
|
117
|
+
const unb = paper("unbalanced", facts(MEASURED(621.5, 264.8)));
|
|
118
|
+
const u = only(await lint([join(unb, "paper.tex")]));
|
|
119
|
+
check(
|
|
120
|
+
"🔴 an unbalanced last page is a finding, with both heights and the difference",
|
|
121
|
+
u.length === 1 &&
|
|
122
|
+
u[0].severity === 2 &&
|
|
123
|
+
/621\.5 and 264\.8 pt, 356\.7 pt apart \(tolerance 120 pt\)/.test(
|
|
124
|
+
u[0].message,
|
|
125
|
+
),
|
|
126
|
+
JSON.stringify(u),
|
|
127
|
+
);
|
|
128
|
+
check(
|
|
129
|
+
"…at the \\documentclass line, where the class option that fixes it goes",
|
|
130
|
+
u[0]?.line === 2,
|
|
131
|
+
String(u[0]?.line),
|
|
132
|
+
);
|
|
133
|
+
check(
|
|
134
|
+
"…and the message says how to fix it by hand: pbalance, flushend, \\balance in the first column",
|
|
135
|
+
/pbalance/.test(u[0]?.message) &&
|
|
136
|
+
/flushend/.test(u[0]?.message) &&
|
|
137
|
+
/FIRST column of the last page/.test(u[0]?.message),
|
|
138
|
+
);
|
|
139
|
+
const loose = only(
|
|
140
|
+
await lint(
|
|
141
|
+
[join(unb, "paper.tex")],
|
|
142
|
+
[
|
|
143
|
+
{
|
|
144
|
+
basePath: root,
|
|
145
|
+
files: ["papers/**"],
|
|
146
|
+
rules: { "pdf/last-page-balance": ["warn", { tolerancePt: 400 }] },
|
|
147
|
+
},
|
|
148
|
+
],
|
|
149
|
+
),
|
|
150
|
+
);
|
|
151
|
+
check(
|
|
152
|
+
"the tolerance is the consumer's: 400 pt lets 356.7 pass",
|
|
153
|
+
loose.length === 0,
|
|
154
|
+
JSON.stringify(loose),
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
const noFacts = paper("no-facts", null);
|
|
158
|
+
check(
|
|
159
|
+
"🔴 on, but no facts file: a finding telling the author to build — not silence",
|
|
160
|
+
only(await lint([join(noFacts, "paper.tex")]))[0]?.messageId === "noFacts",
|
|
161
|
+
);
|
|
162
|
+
const stale = paper("stale", facts(MEASURED(400, 400)), {
|
|
163
|
+
pdf: "%PDF-1.5 rebuilt since",
|
|
164
|
+
});
|
|
165
|
+
check(
|
|
166
|
+
"🔴 facts about a different PDF than the one on disk are refused as stale",
|
|
167
|
+
only(await lint([join(stale, "paper.tex")]))[0]?.messageId === "stale",
|
|
168
|
+
);
|
|
169
|
+
const gone = paper("gone", facts(MEASURED(400, 400)), { pdf: null });
|
|
170
|
+
check(
|
|
171
|
+
"facts whose PDF is gone (a failed build removes it) are refused",
|
|
172
|
+
only(await lint([join(gone, "paper.tex")]))[0]?.messageId === "pdfMissing",
|
|
173
|
+
);
|
|
174
|
+
const old = paper("old-schema", facts(MEASURED(400, 400), { schema: 1 }));
|
|
175
|
+
check(
|
|
176
|
+
"schema-1 facts are refused, not read with new meanings",
|
|
177
|
+
only(await lint([join(old, "paper.tex")]))[0]?.messageId === "schema",
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
// ── quiet ───────────────────────────────────────────────────────────────────────────
|
|
181
|
+
const bal = paper("balanced", facts(MEASURED(464.2, 461.5)));
|
|
182
|
+
const stub = paper("stub", facts({ kind: "stub", words: 12 }));
|
|
183
|
+
const review = paper("review", facts({ kind: "review", line_numbers: 60 }));
|
|
184
|
+
const quiet = only(
|
|
185
|
+
await lint([bal, stub, review].map((d) => join(d, "paper.tex"))),
|
|
186
|
+
);
|
|
187
|
+
check(
|
|
188
|
+
"quiet on a balanced page, a stub last page and a review build",
|
|
189
|
+
quiet.length === 0,
|
|
190
|
+
JSON.stringify(quiet),
|
|
191
|
+
);
|
|
192
|
+
check(
|
|
193
|
+
"quiet on a file that is not paper.tex, even inside the glob",
|
|
194
|
+
only(await lint([join(unb, "NOTES.md")])).length === 0,
|
|
195
|
+
);
|
|
196
|
+
check(
|
|
197
|
+
"🔴 OFF BY DEFAULT: with no `rules` block, an unbalanced paper gets no finding",
|
|
198
|
+
only(await lint([join(unb, "paper.tex")], [])).length === 0,
|
|
199
|
+
);
|
|
200
|
+
} finally {
|
|
201
|
+
rmSync(root, { recursive: true, force: true });
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
console.log(
|
|
205
|
+
`✓ ${String(n)} assertions passed — pdf/last-page-balance: fires on unbalanced and unjudgeable, quiet on balanced/stub/review/off`,
|
|
206
|
+
);
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pdf/last-page-balance — the two columns of a paper's last page end at about the same height.
|
|
3
|
+
*
|
|
4
|
+
* OPTIONAL, OFF BY DEFAULT. Balance is a requirement of SOME venues, not of papers: some ACM
|
|
5
|
+
* production vendors return a camera-ready for it (Conference Publishing Consulting, which produces
|
|
6
|
+
* ICSE/ASE/SIGSOFT/SIGPLAN proceedings; Sheridan for ACM SIG conferences such as CCS), IEEEtran
|
|
7
|
+
* advises it for camera-ready work, and ACL, USENIX, AAAI and ICML say nothing. No standard format
|
|
8
|
+
* checker (HotCRP's banal, aclpubcheck, IEEE PDF eXpress) tests it. So a consumer turns it on for
|
|
9
|
+
* the papers whose venue asks, in package.json:
|
|
10
|
+
*
|
|
11
|
+
* "paperlint": { "rules": [ { "files": ["papers/my-paper/**"],
|
|
12
|
+
* "rules": { "pdf/last-page-balance": ["error", { "tolerancePt": 120 }] } } ] }
|
|
13
|
+
*
|
|
14
|
+
* (That is a line comment's worth of JSON in a block comment only because it has no asterisk
|
|
15
|
+
* followed by a slash; see rule 8 of CLAUDE.md before editing it.)
|
|
16
|
+
*
|
|
17
|
+
* ── WHAT IT READS: THE FACTS THE BUILD WROTE, NOT THE PDF ───────────────────────
|
|
18
|
+
* `paperlint build` measures the PDF with pdf.js and writes `<paper>/_build/paper.facts.json`. This rule
|
|
19
|
+
* runs on `paper.tex` — the one file every LaTeX paper has, and the one ESLint already parses — and
|
|
20
|
+
* judges that paper's facts file beside it. It never opens the PDF to measure it: it hashes it, to
|
|
21
|
+
* refuse facts about a different build than the one on disk.
|
|
22
|
+
*
|
|
23
|
+
* ── IT DOES NOT FIX, AND SAYS HOW TO ────────────────────────────────────────────
|
|
24
|
+
* paperlint used to search for a `\balance` position itself and fail the build when none worked. Every
|
|
25
|
+
* mechanism is documented as unreliable by its own authors (acmart: `balance` "may lead to
|
|
26
|
+
* problems", `pbalance` is "experimental"; pbalance: "this package is a hack"), and for ACM TAPS
|
|
27
|
+
* venues ACM recompiles the source, so an edit to a generated `.bbl` may never reach the proceedings.
|
|
28
|
+
* The message names the manual fixes the vendor itself lists, in its order.
|
|
29
|
+
*
|
|
30
|
+
* Silent, by design, where there is nothing to judge: a stub last page (a few lines) and a review
|
|
31
|
+
* build (numbered lines in the margins make both columns measure full height). Loud where the input
|
|
32
|
+
* is missing: no facts file, a foreign schema, facts about a PDF that is not on disk or changed
|
|
33
|
+
* since — a rule that is on and reads nothing must not look like a rule that passed.
|
|
34
|
+
*/
|
|
35
|
+
import { createHash } from "node:crypto";
|
|
36
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
37
|
+
import { basename, dirname, isAbsolute, join } from "node:path";
|
|
38
|
+
|
|
39
|
+
/** The difference, in points, that two columns may end apart. See the harness for why 120. */
|
|
40
|
+
export const DEFAULT_TOLERANCE_PT = 120;
|
|
41
|
+
/** The schema of `paper.facts.json` this rule reads (`src/facts-file.ts`, FACTS_SCHEMA). */
|
|
42
|
+
export const FACTS_SCHEMA = 2;
|
|
43
|
+
export const FACTS_REL = join("_build", "paper.facts.json");
|
|
44
|
+
|
|
45
|
+
const isHeight = (x) => typeof x === "number" && Number.isFinite(x);
|
|
46
|
+
|
|
47
|
+
/** The `last_page` field, parsed. */
|
|
48
|
+
function lastPageOf(v) {
|
|
49
|
+
if (v?.kind === "stub" || v?.kind === "review") return { kind: v.kind };
|
|
50
|
+
const c = v?.columns_pt;
|
|
51
|
+
if (
|
|
52
|
+
v?.kind === "measured" &&
|
|
53
|
+
Array.isArray(c) &&
|
|
54
|
+
c.length === 2 &&
|
|
55
|
+
c.every(isHeight)
|
|
56
|
+
)
|
|
57
|
+
return { kind: "measured", left: c[0], right: c[1] };
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* The facts file's text → what this rule needs, or the reason it cannot be judged. Pure.
|
|
63
|
+
*
|
|
64
|
+
* @returns {{ ok: true, pdf: string, sha: string, last: object } | { ok: false, messageId: string, data: object }}
|
|
65
|
+
*/
|
|
66
|
+
export function parseFacts(text) {
|
|
67
|
+
let d;
|
|
68
|
+
try {
|
|
69
|
+
d = JSON.parse(text);
|
|
70
|
+
} catch (e) {
|
|
71
|
+
return { ok: false, messageId: "factsBroken", data: { why: e.message } };
|
|
72
|
+
}
|
|
73
|
+
if (d?.schema !== FACTS_SCHEMA)
|
|
74
|
+
return {
|
|
75
|
+
ok: false,
|
|
76
|
+
messageId: "schema",
|
|
77
|
+
data: { got: JSON.stringify(d?.schema ?? null) },
|
|
78
|
+
};
|
|
79
|
+
const last = lastPageOf(d.last_page);
|
|
80
|
+
if (
|
|
81
|
+
typeof d.pdf !== "string" ||
|
|
82
|
+
!/^[0-9a-f]{64}$/.test(d.pdf_sha256 ?? "") ||
|
|
83
|
+
!last
|
|
84
|
+
)
|
|
85
|
+
return {
|
|
86
|
+
ok: false,
|
|
87
|
+
messageId: "factsBroken",
|
|
88
|
+
data: {
|
|
89
|
+
why: "no `pdf`, `pdf_sha256` or `last_page` in the expected shape",
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
return { ok: true, pdf: d.pdf, sha: d.pdf_sha256, last };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Whether the facts describe the PDF on disk: null when they do, else the reason. */
|
|
96
|
+
function staleness(paperDir, facts) {
|
|
97
|
+
const pdf = isAbsolute(facts.pdf) ? facts.pdf : join(paperDir, facts.pdf);
|
|
98
|
+
let got;
|
|
99
|
+
try {
|
|
100
|
+
got = createHash("sha256").update(readFileSync(pdf)).digest("hex");
|
|
101
|
+
} catch {
|
|
102
|
+
return { messageId: "pdfMissing", data: { pdf: facts.pdf } };
|
|
103
|
+
}
|
|
104
|
+
return got === facts.sha
|
|
105
|
+
? null
|
|
106
|
+
: { messageId: "stale", data: { pdf: facts.pdf } };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** The finding for a measured page, or null when it is balanced. Pure. */
|
|
110
|
+
export function judgeColumns(last, tolerancePt) {
|
|
111
|
+
if (last.kind !== "measured") return null;
|
|
112
|
+
const diff = Math.abs(last.left - last.right);
|
|
113
|
+
if (diff <= tolerancePt) return null;
|
|
114
|
+
return {
|
|
115
|
+
messageId: "unbalanced",
|
|
116
|
+
data: {
|
|
117
|
+
left: last.left.toFixed(1),
|
|
118
|
+
right: last.right.toFixed(1),
|
|
119
|
+
diff: diff.toFixed(1),
|
|
120
|
+
tol: String(tolerancePt),
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** Where a finding goes: the `\documentclass` line, where the class options that fix it live. */
|
|
126
|
+
function reportLine(file) {
|
|
127
|
+
try {
|
|
128
|
+
const lines = readFileSync(file, "utf8").split("\n");
|
|
129
|
+
const i = lines.findIndex((l) => /^\s*\\documentclass\b/.test(l));
|
|
130
|
+
return i >= 0 ? i + 1 : 1;
|
|
131
|
+
} catch {
|
|
132
|
+
return 1;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** Everything the rule decides for one paper, as a finding or null. */
|
|
137
|
+
function verdict(paperDir, tolerancePt) {
|
|
138
|
+
const factsFile = join(paperDir, FACTS_REL);
|
|
139
|
+
if (!existsSync(factsFile))
|
|
140
|
+
return { messageId: "noFacts", data: { file: FACTS_REL } };
|
|
141
|
+
const facts = parseFacts(readFileSync(factsFile, "utf8"));
|
|
142
|
+
if (!facts.ok) return { messageId: facts.messageId, data: facts.data };
|
|
143
|
+
return staleness(paperDir, facts) ?? judgeColumns(facts.last, tolerancePt);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const rule = {
|
|
147
|
+
meta: {
|
|
148
|
+
type: "problem",
|
|
149
|
+
docs: {
|
|
150
|
+
description:
|
|
151
|
+
"the last page's two columns end at about the same height (optional; for venues that require it)",
|
|
152
|
+
},
|
|
153
|
+
schema: [
|
|
154
|
+
{
|
|
155
|
+
type: "object",
|
|
156
|
+
properties: {
|
|
157
|
+
tolerancePt: { type: "number", exclusiveMinimum: 0 },
|
|
158
|
+
},
|
|
159
|
+
additionalProperties: false,
|
|
160
|
+
},
|
|
161
|
+
],
|
|
162
|
+
messages: {
|
|
163
|
+
unbalanced:
|
|
164
|
+
"the last page is unbalanced: its columns end at {{left}} and {{right}} pt, {{diff}} pt " +
|
|
165
|
+
"apart (tolerance {{tol}} pt). Some ACM production vendors send such a camera-ready back. " +
|
|
166
|
+
"Conference Publishing Consulting's advice, in its order: \"For the ACMART style, use " +
|
|
167
|
+
"option 'balance' or, if this does not work, option 'pbalance'\" — acmart already has " +
|
|
168
|
+
"`balance` on by default, and it calls \\balance from the second column, where it does " +
|
|
169
|
+
"nothing when the last page is all bibliography, so try `pbalance`; then " +
|
|
170
|
+
"`\\usepackage{flushend}`; then `\\balance` (balance.sty) placed in what would be the " +
|
|
171
|
+
"FIRST column of the last page — in the bibliography, before the \\bibitem that starts " +
|
|
172
|
+
"that column. Rebuild (`paperlint build`) and lint again.",
|
|
173
|
+
noFacts:
|
|
174
|
+
"pdf/last-page-balance is on for this paper, but {{file}} does not exist beside it. The rule " +
|
|
175
|
+
"judges the facts `paperlint build` writes after a successful compile — build the paper first.",
|
|
176
|
+
factsBroken:
|
|
177
|
+
"{{why}} in _build/paper.facts.json — rebuild the paper (`paperlint build`) to rewrite it",
|
|
178
|
+
schema:
|
|
179
|
+
"_build/paper.facts.json has schema {{got}}; this rule reads schema 2 — rebuild the paper " +
|
|
180
|
+
"(`paperlint build`) to rewrite it",
|
|
181
|
+
pdfMissing:
|
|
182
|
+
"_build/paper.facts.json describes {{pdf}}, which is not on disk (a failed build removes " +
|
|
183
|
+
"it) — rebuild the paper",
|
|
184
|
+
stale:
|
|
185
|
+
"_build/paper.facts.json describes a DIFFERENT {{pdf}} than the one on disk (its SHA-256 " +
|
|
186
|
+
"differs), so judging it would judge an earlier build — rebuild the paper",
|
|
187
|
+
},
|
|
188
|
+
},
|
|
189
|
+
create(context) {
|
|
190
|
+
// Judged once per paper, on its paper.tex: the paper directory is the file's directory.
|
|
191
|
+
if (basename(context.filename) !== "paper.tex") return {};
|
|
192
|
+
const tolerancePt = context.options[0]?.tolerancePt ?? DEFAULT_TOLERANCE_PT;
|
|
193
|
+
return {
|
|
194
|
+
root() {
|
|
195
|
+
const v = verdict(dirname(context.filename), tolerancePt);
|
|
196
|
+
if (!v) return;
|
|
197
|
+
const line = reportLine(context.filename);
|
|
198
|
+
context.report({
|
|
199
|
+
loc: { start: { line, column: 1 }, end: { line, column: 2 } },
|
|
200
|
+
messageId: v.messageId,
|
|
201
|
+
data: v.data,
|
|
202
|
+
});
|
|
203
|
+
},
|
|
204
|
+
};
|
|
205
|
+
},
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
export default { rules: { "last-page-balance": rule } };
|