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,322 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* tex-build — rules about the BUILT LaTeX paper: about what the preamble declares, not about
|
|
3
|
+
* how the prose is written.
|
|
4
|
+
*
|
|
5
|
+
* ── WHY ITS OWN NAMESPACE, AND NOT `paper/*` ────────────────────────────────
|
|
6
|
+
* In the source corpus the `paper/*` family is declared on BOTH `paper.md` and `paper.tex`,
|
|
7
|
+
* and that parity is pinned by an assertion: "a rule added for markdown must not silently
|
|
8
|
+
* fail to reach LaTeX (or the other way round)". The subject here is the BUILD MODE
|
|
9
|
+
* (`\documentclass[review]`, `printacmref=false`), and in markdown it does not exist: a draft
|
|
10
|
+
* has no mechanical sign saying "this build is the final one". Writing
|
|
11
|
+
* `paper/future-promise: "off"` on markdown would be an exemption justified by "there is no
|
|
12
|
+
* subject", and that same parity assertion requires the rule to be ALIVE on the other side —
|
|
13
|
+
* i.e. the machinery exists but is built for the opposite direction (off on `.tex`, alive on
|
|
14
|
+
* `.md`).
|
|
15
|
+
*
|
|
16
|
+
* Hence a namespace of its own, the same way an external word-counter gets one: it has a word
|
|
17
|
+
* count markdown does not have, we have a preamble markdown does not have. Where the config
|
|
18
|
+
* enumerates non-`paper/*` rules on `.tex`, the list is CLOSED and `tex/future-promise` is
|
|
19
|
+
* named explicitly, so that a new namespace arrives by decision rather than as a side effect
|
|
20
|
+
* of editing a config.
|
|
21
|
+
*
|
|
22
|
+
* ⚠️ THE MEASUREMENT THIS DECISION RESTS ON (source corpus, 2026-09-06 → 2026-09-07): had the
|
|
23
|
+
* rule been declared on `paper.md` it would produce ZERO there TODAY — not one match of
|
|
24
|
+
* `FUTURE_PROMISE_RE` in the markdown draft of the one paper that has one. So the choice is
|
|
25
|
+
* about semantics, not noise: markdown has no input distinguishing a draft from a shipped
|
|
26
|
+
* build, and the rule would fire on a draft's honest promise.
|
|
27
|
+
*
|
|
28
|
+
* ── THE RULE READS THE FILE FROM DISK, NOT THE PROJECTION. This is load-bearing ──
|
|
29
|
+
* The LaTeX language in `latex-language.mjs` hands rules a PROJECTION of `.tex` into a
|
|
30
|
+
* markdown-like text, and the first thing that projection does is blank the whole preamble
|
|
31
|
+
* (`blank(0, preEnd)`) plus `\documentclass` and `\settopmatter` as OPAQUE macros. Both signs
|
|
32
|
+
* of the build mode are therefore ERASED in the projection, and this check is inexpressible
|
|
33
|
+
* over `sourceCode.getText()`. `readFileSync(context.filename)` is the same thing sibling
|
|
34
|
+
* rules in the source corpus do when they read a registry file next to the paper; the only
|
|
35
|
+
* difference is that here the file being read is the linted file itself. The one side effect
|
|
36
|
+
* is named: in an editor with an unsaved buffer the rule judges the saved version. On a CLI
|
|
37
|
+
* run (and in CI) there is no such difference.
|
|
38
|
+
*
|
|
39
|
+
* ── WHAT THE MOVE TO A LINT RULE FIXED BESIDES THE FORM (measured 2026-09-07) ──
|
|
40
|
+
* 🔴 The previous implementation, a script, picked its input like this: look for `paper.md` /
|
|
41
|
+
* `draft.md`, and failing that take `readdirSync(dir).find((f) => f.endsWith(".tex"))` — the
|
|
42
|
+
* FIRST `.tex` in directory order. Measured across the three papers of the source corpus:
|
|
43
|
+
* paper A → a 536-BYTE file holding nine `\def`s with venue numbers. The check, added
|
|
44
|
+
* specifically BECAUSE of paper A, was not reading paper A;
|
|
45
|
+
* paper B → a four-month-old draft, one of SEVEN `.tex` files in that directory (the real
|
|
46
|
+
* `paper.tex` happened to get the same verdict — "review mode" — so the set of
|
|
47
|
+
* findings matched by accident, not by construction);
|
|
48
|
+
* paper C → `paper.tex`, the only `.tex` there; it matched.
|
|
49
|
+
* An ESLint rule runs over the glob declared in the config, so the address stopped depending
|
|
50
|
+
* on `readdirSync` order and on how many drafts happen to lie next to the paper.
|
|
51
|
+
*/
|
|
52
|
+
import { readFileSync } from "node:fs";
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* A copy rather than an import, for the same reason the prose lexicons are copies: a rule may
|
|
56
|
+
* not depend on the script it is replacing, or removing the check from that script becomes
|
|
57
|
+
* impossible.
|
|
58
|
+
*/
|
|
59
|
+
const FUTURE_PROMISE_RE =
|
|
60
|
+
/\b(?:at (?:the )?camera[-\s]?ready|upon acceptance|on acceptance|will be (?:released|published|made (?:publicly )?available|public|open[-\s]?sourced))\b/i;
|
|
61
|
+
|
|
62
|
+
/** The same two signs a render script uses to tell a review build from a camera-ready one. */
|
|
63
|
+
const REVIEW_MODE_RE =
|
|
64
|
+
/^[^%\n]*(?:\\documentclass\[[^\]]*\breview\b|printacmref=false)/m;
|
|
65
|
+
|
|
66
|
+
/** Is this an `acmart` build at all? Outside acmart none of the macros below mean anything. */
|
|
67
|
+
const ACMART_RE = /\\documentclass(?:\s*\[[^\]]*\])?\s*\{acmart\}/;
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* acmart's OWN escape hatch for "this is not going to an ACM venue": in `nonacm` mode the
|
|
71
|
+
* class disables the ACM Reference Format itself (`acmart.cls:110-121`, "in 'nonacm' mode we
|
|
72
|
+
* disable the \"ACM Reference Format\""). A build that declares it is not an ACM submission
|
|
73
|
+
* is exempt — that is the supported way to say so, and it is a class option rather than a
|
|
74
|
+
* hand-rolled blanking of the front matter.
|
|
75
|
+
*/
|
|
76
|
+
const NONACM_RE = /\\documentclass\s*\[[^\]]*\bnonacm\b/;
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* The overrides whose ENTIRE effect is to remove furniture the template puts on page 1.
|
|
80
|
+
* Each entry names what disappears, because a message that only says "do not do this" gets
|
|
81
|
+
* read as style advice.
|
|
82
|
+
*/
|
|
83
|
+
const FRONTMATTER_OVERRIDES = [
|
|
84
|
+
{
|
|
85
|
+
re: /\\setcopyright\s*\{\s*none\s*\}/g,
|
|
86
|
+
removes: "the copyright statement (acmart.cls:1857, copyright mode 0)",
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
re: /\\renewcommand\s*\*?\s*\{?\s*\\footnotetextcopyrightpermission/g,
|
|
90
|
+
removes:
|
|
91
|
+
"the permission/copyright footnote block on page 1 (acmart.cls:2180)",
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
re: /\\(?:this)?pagestyle\s*\{\s*(?:plain|empty)\s*\}/g,
|
|
95
|
+
removes:
|
|
96
|
+
"acmart's own page style, i.e. the running heads and folios it restores at " +
|
|
97
|
+
"\\begin{document} (acmart.cls:2876-2881)",
|
|
98
|
+
},
|
|
99
|
+
];
|
|
100
|
+
|
|
101
|
+
// ═════════════════════════════════════════════════════════════════════════════
|
|
102
|
+
// future-promise — a shipped build promises what it has already handed over
|
|
103
|
+
// ═════════════════════════════════════════════════════════════════════════════
|
|
104
|
+
// 🔴 Added 2026-08-24, because it would have caught a real blocker hours earlier. An accepted
|
|
105
|
+
// paper printed "(full harness at camera-ready)" in BOTH of its most-read positions — the end
|
|
106
|
+
// of the abstract and the end of the conclusion — while its own Availability paragraph three
|
|
107
|
+
// lines below said the harness was already public. The document contradicted itself two
|
|
108
|
+
// against one, and the losing side was the abstract: exactly the part a digital library
|
|
109
|
+
// indexes and the only part most people read.
|
|
110
|
+
//
|
|
111
|
+
// Worse than a typo: a reviewer had listed it as a weakness — "The experimental harness is not
|
|
112
|
+
// yet available (it is expected for the camera-ready version)" — so the printed camera-ready
|
|
113
|
+
// kept asserting the very thing the reviewer complained about.
|
|
114
|
+
//
|
|
115
|
+
// ⚠️ Here is why it survived, and it generalises. The promise WAS swept out the same day — out
|
|
116
|
+
// of the ARTIFACT (`README:34`, `reproduce.py:166`). Nobody made the same pass over
|
|
117
|
+
// `paper.tex`. One class of defect, two carriers, one of them cleaned. A human pass goes where
|
|
118
|
+
// the human is looking; this check goes to both places, because it does not know where anyone
|
|
119
|
+
// is looking.
|
|
120
|
+
//
|
|
121
|
+
// The scope is DELIBERATELY NARROW, because a noisy gate is a silenced gate:
|
|
122
|
+
// - `.tex` only (see the file header: markdown has no sign of "this build is final");
|
|
123
|
+
// - a review-mode build is EXEMPT, and that is the whole point: during peer review
|
|
124
|
+
// "at camera-ready" is a true and ordinary promise. The defect is only a promise that
|
|
125
|
+
// outlived its own delivery;
|
|
126
|
+
// - LaTeX comments are blanked FIRST: author notes of the form `% camera-ready blocker`
|
|
127
|
+
// legitimately live in the source, and a finding on those would be precisely the false
|
|
128
|
+
// positive that kills a check.
|
|
129
|
+
//
|
|
130
|
+
// SEVERITY is `warn`, and that is an analysis rather than caution. The finding is NOT binary:
|
|
131
|
+
// a promise in a camera-ready is sometimes honest (something genuinely not released yet), and
|
|
132
|
+
// the verdict "does it contradict the Availability paragraph" is made by a human — which is
|
|
133
|
+
// what the message text says. There is also a demonstrable class of false positives: "their
|
|
134
|
+
// replication will be published in 2027" is a sentence about SOMEONE ELSE's work. An `error`
|
|
135
|
+
// that fails on a correct input gets switched off the same day, and after that the binary
|
|
136
|
+
// checks stop being read too.
|
|
137
|
+
const futurePromise = {
|
|
138
|
+
meta: {
|
|
139
|
+
type: "problem",
|
|
140
|
+
docs: {
|
|
141
|
+
description:
|
|
142
|
+
"the build is not in review mode, yet the text promises a future release of something already handed over",
|
|
143
|
+
},
|
|
144
|
+
schema: [],
|
|
145
|
+
messages: {
|
|
146
|
+
promise:
|
|
147
|
+
"this build is NOT in review mode, but the text promises a future release: " +
|
|
148
|
+
"«{{text}}». Check it against Availability: if the thing is already published, the " +
|
|
149
|
+
"paper contradicts itself, and the abstract is the side that loses — the abstract is " +
|
|
150
|
+
"what the digital library indexes. That is exactly how «(full harness at " +
|
|
151
|
+
"camera-ready)» survived a sweep that went through the artifact and never went " +
|
|
152
|
+
"through paper.tex",
|
|
153
|
+
},
|
|
154
|
+
},
|
|
155
|
+
create(context) {
|
|
156
|
+
return {
|
|
157
|
+
root() {
|
|
158
|
+
// 🔴 ONE GUARD, NOT THREE. The first draft had `if (!file || !existsSync(file))`
|
|
159
|
+
// BEFORE the read — and a mutation run on 2026-09-07 showed that removing that line is
|
|
160
|
+
// caught by NOTHING: the `try/catch` around the read covers exactly the same cases (no
|
|
161
|
+
// path, a `<text>` run through stdin, no permission). Two guards doing one job is a
|
|
162
|
+
// guard no mutation can kill, i.e. a dead assertion dressed as robustness.
|
|
163
|
+
// What is left is the read inside `try`: stay quiet rather than judge content we never
|
|
164
|
+
// saw.
|
|
165
|
+
let text;
|
|
166
|
+
try {
|
|
167
|
+
text = readFileSync(context.filename, "utf8");
|
|
168
|
+
} catch {
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
if (REVIEW_MODE_RE.test(text)) return;
|
|
172
|
+
// Comments are blanked by CUTTING THE TAIL of the line, exactly as the previous
|
|
173
|
+
// implementation did. Columns do not shift: a match is only possible to the LEFT of
|
|
174
|
+
// `%`, and the left part of the line stays in place character for character. An
|
|
175
|
+
// escaped `\%` is not a comment.
|
|
176
|
+
const lines = text
|
|
177
|
+
.split("\n")
|
|
178
|
+
.map((l) => l.replace(/(^|[^\\])%.*$/, "$1"));
|
|
179
|
+
lines.forEach((line, i) => {
|
|
180
|
+
const m = line.match(FUTURE_PROMISE_RE);
|
|
181
|
+
if (!m) return;
|
|
182
|
+
const column = m.index + 1;
|
|
183
|
+
context.report({
|
|
184
|
+
loc: {
|
|
185
|
+
start: { line: i + 1, column },
|
|
186
|
+
end: { line: i + 1, column: column + m[0].length },
|
|
187
|
+
},
|
|
188
|
+
messageId: "promise",
|
|
189
|
+
data: { text: m[0] },
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
},
|
|
193
|
+
};
|
|
194
|
+
},
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
198
|
+
// acm-frontmatter-override — the build blanks template furniture off page 1
|
|
199
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
200
|
+
// 🔴 Added 2026-09-11, from a DESK REJECT. A submission was rejected before any content
|
|
201
|
+
// review with the reason "substantially deviating from the specified template… front page
|
|
202
|
+
// lacks elements included in the template". Its preamble carried three lines whose entire
|
|
203
|
+
// effect is to take elements off the front page:
|
|
204
|
+
//
|
|
205
|
+
// \setcopyright{none}
|
|
206
|
+
// \renewcommand\footnotetextcopyrightpermission[1]{}
|
|
207
|
+
// \pagestyle{plain}
|
|
208
|
+
//
|
|
209
|
+
// The same three lines sit COMMENTED OUT in a sibling paper of the same corpus — the one that
|
|
210
|
+
// was accepted. The safer pattern was already known in the same directory and was not carried
|
|
211
|
+
// over. That is the shape this check exists for: a defect that is invisible in the source,
|
|
212
|
+
// visible only on the rendered first page, and fatal before anyone reads a word.
|
|
213
|
+
//
|
|
214
|
+
// ⚠️ THE RULE IS NOT CONDITIONED ON REVIEW MODE, AND THAT IS A MEASUREMENT, NOT A CHOICE.
|
|
215
|
+
// The obvious framing — "flag these when `\documentclass[review]`, because review mode already
|
|
216
|
+
// handles the front matter" — does not survive reading `acmart.cls`. The `review` option does
|
|
217
|
+
// exactly two things (`acmart.cls:93-99`): it turns on line numbers and sets
|
|
218
|
+
// `\@ACM@printfoliostrue`. It does not touch the copyright statement, the permission footnote
|
|
219
|
+
// or the page style. So the overrides are not fighting review mode; they strip the same
|
|
220
|
+
// furniture in EVERY mode — and stripping it in the camera-ready is worse, because that is
|
|
221
|
+
// the version the digital library keeps.
|
|
222
|
+
//
|
|
223
|
+
// The guards are therefore about the CLASS, not the stage:
|
|
224
|
+
// - `acmart` only. Outside it `\pagestyle{plain}` is an ordinary, correct line;
|
|
225
|
+
// - `nonacm` exempts, because that is acmart's own supported way to say "not an ACM venue",
|
|
226
|
+
// and in that mode the class disables the ACM Reference Format itself (`acmart.cls:110`).
|
|
227
|
+
//
|
|
228
|
+
// 🔴 `printacmref=false` IS DELIBERATELY NOT IN THE SET, though it was in the rejected preamble
|
|
229
|
+
// and it too removes a block from page 1. Two reasons, and the first is structural: the
|
|
230
|
+
// sibling rule in this very file reads `printacmref=false` as a SIGN OF REVIEW MODE
|
|
231
|
+
// (`REVIEW_MODE_RE`). A module where one rule treats a token as a legitimate marker and
|
|
232
|
+
// another calls the same token a defect is a module that contradicts itself, and the reader
|
|
233
|
+
// cannot tell which half to believe. The second is that ACM sanctions it for preprints. If it
|
|
234
|
+
// ever needs catching, it needs catching together with a decision about `REVIEW_MODE_RE` —
|
|
235
|
+
// not as a fourth line in this array.
|
|
236
|
+
//
|
|
237
|
+
// SEVERITY is for the CONSUMER to set, and the two configs differ on purpose: this repository
|
|
238
|
+
// lints fixtures that are broken by construction, a consumer lints a real paper. The finding
|
|
239
|
+
// is binary — the macro is present or it is not — it has a named exemption, and the cost of a
|
|
240
|
+
// miss is a rejection with no content review. In a consumer that is `error`.
|
|
241
|
+
const acmFrontmatterOverride = {
|
|
242
|
+
meta: {
|
|
243
|
+
type: "problem",
|
|
244
|
+
docs: {
|
|
245
|
+
description:
|
|
246
|
+
"an acmart build overrides ACM's front-matter commands, removing template elements from page 1",
|
|
247
|
+
},
|
|
248
|
+
schema: [],
|
|
249
|
+
messages: {
|
|
250
|
+
override:
|
|
251
|
+
"«{{text}}» removes {{removes}} from the front page of an `acmart` build. A submission " +
|
|
252
|
+
"was desk-rejected for exactly this — «substantially deviating from the specified " +
|
|
253
|
+
"template… front page lacks elements included in the template» — before any content " +
|
|
254
|
+
"review happened. The `review` option does not suppress this furniture itself " +
|
|
255
|
+
"(acmart.cls:93-99 turns on line numbers and folios, nothing else), so the line is not " +
|
|
256
|
+
"compensating for the class: it is fighting the template. Delete it. If this paper " +
|
|
257
|
+
"genuinely is not going to an ACM venue, say so the supported way — " +
|
|
258
|
+
"`\\documentclass[…,nonacm]{acmart}` — and the class drops the ACM Reference Format " +
|
|
259
|
+
"on its own",
|
|
260
|
+
},
|
|
261
|
+
},
|
|
262
|
+
create(context) {
|
|
263
|
+
return {
|
|
264
|
+
root() {
|
|
265
|
+
// Same single guard as the sibling rule, and for the same measured reason: a separate
|
|
266
|
+
// `existsSync` before the read is covered by this `catch` in every case it could
|
|
267
|
+
// fire, i.e. it is an assertion no mutation can kill.
|
|
268
|
+
let text;
|
|
269
|
+
try {
|
|
270
|
+
text = readFileSync(context.filename, "utf8");
|
|
271
|
+
} catch {
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
// Comments are blanked BEFORE the guards, not only before the search — unlike the
|
|
275
|
+
// sibling rule, which carries the `^[^%\n]*` prefix inside each pattern instead.
|
|
276
|
+
// It matters here: the accepted sibling paper of the source corpus keeps this exact
|
|
277
|
+
// block commented out as a record of what it used to do, and a `\documentclass` line
|
|
278
|
+
// quoted inside a comment must not arm the rule any more than a commented
|
|
279
|
+
// `\setcopyright{none}` must trip it. Columns do not shift: a match is only possible
|
|
280
|
+
// to the LEFT of `%`, and the left part of the line survives character for character.
|
|
281
|
+
// An escaped `\%` is not a comment.
|
|
282
|
+
const lines = text
|
|
283
|
+
.split("\n")
|
|
284
|
+
.map((l) => l.replace(/(^|[^\\])%.*$/, "$1"));
|
|
285
|
+
const code = lines.join("\n");
|
|
286
|
+
if (!ACMART_RE.test(code)) return;
|
|
287
|
+
if (NONACM_RE.test(code)) return;
|
|
288
|
+
lines.forEach((line, i) => {
|
|
289
|
+
for (const { re, removes } of FRONTMATTER_OVERRIDES) {
|
|
290
|
+
// 🔴 NO `re.lastIndex = 0` HERE, AND THAT IS A MEASUREMENT. These regexes are
|
|
291
|
+
// module-level and `/g`, so they carry `lastIndex` across files, and the obvious
|
|
292
|
+
// defensive line is to reset it. A mutation run on 2026-09-11 removed that line
|
|
293
|
+
// and the harness stayed GREEN — because the loop below always drains the regex,
|
|
294
|
+
// and a `/g` `exec` that returns `null` resets `lastIndex` to 0 by itself. The
|
|
295
|
+
// reset was a guard no mutation could kill, i.e. the same dead assertion dressed
|
|
296
|
+
// as robustness that the sibling rule's single-`try` comment describes. What
|
|
297
|
+
// keeps the property honest instead is an ASSERTION, not a line of code: the
|
|
298
|
+
// harness lints the same input twice in one process and requires the two runs to
|
|
299
|
+
// agree. Add a `break` to this loop and that assertion is what will fail.
|
|
300
|
+
let m;
|
|
301
|
+
while ((m = re.exec(line)) !== null) {
|
|
302
|
+
const column = m.index + 1;
|
|
303
|
+
context.report({
|
|
304
|
+
loc: {
|
|
305
|
+
start: { line: i + 1, column },
|
|
306
|
+
end: { line: i + 1, column: column + m[0].length },
|
|
307
|
+
},
|
|
308
|
+
messageId: "override",
|
|
309
|
+
data: { text: m[0], removes },
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
});
|
|
314
|
+
},
|
|
315
|
+
};
|
|
316
|
+
},
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
export default {
|
|
320
|
+
"future-promise": futurePromise,
|
|
321
|
+
"acm-frontmatter-override": acmFrontmatterOverride,
|
|
322
|
+
};
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mutation battery for `eslint-rules/tex-build.mjs`.
|
|
3
|
+
* Run: `node eslint-rules/tex-build.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 the
|
|
7
|
+
* repository this came from, a green mutation has FOUR TIMES meant not "the defence holds" but
|
|
8
|
+
* "the patch never landed" / "the test is looking somewhere else".
|
|
9
|
+
*
|
|
10
|
+
* WHAT A GREEN HARNESS UNDER A MUTATION MEANS: it is 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 instead
|
|
12
|
+
* of the value · the test looks in the wrong place · the test is the wrong test · the patch did
|
|
13
|
+
* not land · the set is too narrow · the run was already red BEFORE the mutation. The last one
|
|
14
|
+
* 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/tex-build.mjs";
|
|
20
|
+
const HARNESS = "eslint-rules/tex-build.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
|
+
"vocabulary/camera-ready",
|
|
34
|
+
"remove the «at camera-ready» branch — the very one the check was added for on 2026-08-24",
|
|
35
|
+
" /\\b(?:at (?:the )?camera[-\\s]?ready|upon acceptance|on acceptance|will be (?:released|published|made (?:publicly )?available|public|open[-\\s]?sourced))\\b/i;",
|
|
36
|
+
" /\\b(?:upon acceptance|on acceptance|will be (?:released|published|made (?:publicly )?available|public|open[-\\s]?sourced))\\b/i; /* MUT */",
|
|
37
|
+
],
|
|
38
|
+
[
|
|
39
|
+
"vocabulary/will be …",
|
|
40
|
+
"keep only camera-ready — the «will be released/published/public» family dies",
|
|
41
|
+
" /\\b(?:at (?:the )?camera[-\\s]?ready|upon acceptance|on acceptance|will be (?:released|published|made (?:publicly )?available|public|open[-\\s]?sourced))\\b/i;",
|
|
42
|
+
" /\\b(?:at (?:the )?camera[-\\s]?ready)\\b/i; /* MUT */",
|
|
43
|
+
],
|
|
44
|
+
[
|
|
45
|
+
"review/exemption",
|
|
46
|
+
"stop exempting a review build — a promise made during peer review becomes a finding",
|
|
47
|
+
" if (REVIEW_MODE_RE.test(text)) return;",
|
|
48
|
+
" if (false) return; /* MUT */",
|
|
49
|
+
],
|
|
50
|
+
[
|
|
51
|
+
"review/anchor against comments",
|
|
52
|
+
"drop `^[^%\\n]*` — a COMMENTED-OUT `\\documentclass[review]` starts exempting the build " +
|
|
53
|
+
"(a live case in the source corpus: a paper carrying its previous class a few lines above the real one)",
|
|
54
|
+
" /^[^%\\n]*(?:\\\\documentclass\\[[^\\]]*\\breview\\b|printacmref=false)/m;",
|
|
55
|
+
" /(?:\\\\documentclass\\[[^\\]]*\\breview\\b|printacmref=false)/m; /* MUT */",
|
|
56
|
+
],
|
|
57
|
+
[
|
|
58
|
+
"review/printacmref",
|
|
59
|
+
"remove the second sign of the mode — a build with `printacmref=false` stops being exempt",
|
|
60
|
+
" /^[^%\\n]*(?:\\\\documentclass\\[[^\\]]*\\breview\\b|printacmref=false)/m;",
|
|
61
|
+
" /^[^%\\n]*(?:\\\\documentclass\\[[^\\]]*\\breview\\b)/m; /* MUT */",
|
|
62
|
+
],
|
|
63
|
+
[
|
|
64
|
+
"comments/blanking",
|
|
65
|
+
"stop cutting LaTeX comments — an author note `% camera-ready blocker` becomes a finding",
|
|
66
|
+
' .map((l) => l.replace(/(^|[^\\\\])%.*$/, "$1"));\n lines.forEach((line, i) => {',
|
|
67
|
+
" .map((l) => l); /* MUT */\n lines.forEach((line, i) => {",
|
|
68
|
+
],
|
|
69
|
+
[
|
|
70
|
+
"comments/escaped percent",
|
|
71
|
+
"treat `\\%` as the start of a comment — the rest of the line behind the percent goes invisible",
|
|
72
|
+
' .map((l) => l.replace(/(^|[^\\\\])%.*$/, "$1"));\n lines.forEach((line, i) => {',
|
|
73
|
+
' .map((l) => l.replace(/%.*$/, "")); /* MUT */\n lines.forEach((line, i) => {',
|
|
74
|
+
],
|
|
75
|
+
[
|
|
76
|
+
"address/line",
|
|
77
|
+
"report every finding on line 1 — the address, which is what the move to a lint rule bought, is lost",
|
|
78
|
+
" loc: {\n start: { line: i + 1, column },",
|
|
79
|
+
" loc: {\n start: { line: 1, column: 1 }, /* MUT */",
|
|
80
|
+
],
|
|
81
|
+
[
|
|
82
|
+
"address/column",
|
|
83
|
+
"report the start of the line instead of the start of the match",
|
|
84
|
+
" if (!m) return;\n const column = m.index + 1;",
|
|
85
|
+
" if (!m) return;\n const column = 1; /* MUT */",
|
|
86
|
+
],
|
|
87
|
+
[
|
|
88
|
+
"capture/finding text",
|
|
89
|
+
"print the whole line in the message instead of the captured substring — «how many» would still match, «what» would not",
|
|
90
|
+
" data: { text: m[0] },",
|
|
91
|
+
" data: { text: line.trim() }, /* MUT */",
|
|
92
|
+
],
|
|
93
|
+
// ── tex/acm-frontmatter-override ───────────────────────────────────────────
|
|
94
|
+
[
|
|
95
|
+
"frontmatter/acmart guard",
|
|
96
|
+
"drop the `acmart` guard — `\\pagestyle{plain}` in an ordinary `article` becomes a finding, " +
|
|
97
|
+
"which is the false positive that gets a rule switched off on its first day",
|
|
98
|
+
" if (!ACMART_RE.test(code)) return;",
|
|
99
|
+
" if (false) return; /* MUT */",
|
|
100
|
+
],
|
|
101
|
+
[
|
|
102
|
+
"frontmatter/nonacm exemption",
|
|
103
|
+
"stop exempting `nonacm` — the rule punishes the very escape hatch its own message recommends",
|
|
104
|
+
" if (NONACM_RE.test(code)) return;",
|
|
105
|
+
" if (false) return; /* MUT */",
|
|
106
|
+
],
|
|
107
|
+
[
|
|
108
|
+
"frontmatter/setcopyright",
|
|
109
|
+
"remove `\\setcopyright{none}` from the set — the first of the three lines of the " +
|
|
110
|
+
"desk-rejected preamble stops being caught",
|
|
111
|
+
" re: /\\\\setcopyright\\s*\\{\\s*none\\s*\\}/g,",
|
|
112
|
+
" re: /\\\\setcopyright\\s*\\{\\s*NEVER\\s*\\}/g, /* MUT */",
|
|
113
|
+
],
|
|
114
|
+
[
|
|
115
|
+
"frontmatter/permission footnote",
|
|
116
|
+
"remove the `\\renewcommand\\footnotetextcopyrightpermission` branch — the permission block " +
|
|
117
|
+
"on page 1 can be blanked silently",
|
|
118
|
+
" re: /\\\\renewcommand\\s*\\*?\\s*\\{?\\s*\\\\footnotetextcopyrightpermission/g,",
|
|
119
|
+
" re: /\\\\renewcommand\\s*\\*?\\s*\\{?\\s*\\\\NEVERfootnotetext/g, /* MUT */",
|
|
120
|
+
],
|
|
121
|
+
[
|
|
122
|
+
"frontmatter/pagestyle empty",
|
|
123
|
+
"narrow the page-style branch to `plain` — `\\pagestyle{empty}` strips the same furniture and " +
|
|
124
|
+
"goes unnoticed",
|
|
125
|
+
" re: /\\\\(?:this)?pagestyle\\s*\\{\\s*(?:plain|empty)\\s*\\}/g,",
|
|
126
|
+
" re: /\\\\(?:this)?pagestyle\\s*\\{\\s*(?:plain)\\s*\\}/g, /* MUT */",
|
|
127
|
+
],
|
|
128
|
+
[
|
|
129
|
+
"frontmatter/thispagestyle",
|
|
130
|
+
"drop `(?:this)?` — `\\thispagestyle{empty}`, which targets page 1 SPECIFICALLY, stops being caught",
|
|
131
|
+
" re: /\\\\(?:this)?pagestyle\\s*\\{\\s*(?:plain|empty)\\s*\\}/g,",
|
|
132
|
+
" re: /\\\\pagestyle\\s*\\{\\s*(?:plain|empty)\\s*\\}/g, /* MUT */",
|
|
133
|
+
],
|
|
134
|
+
[
|
|
135
|
+
"frontmatter/comment blanking",
|
|
136
|
+
"stop cutting LaTeX comments — the accepted sibling paper's COMMENTED record of its old " +
|
|
137
|
+
"preamble becomes three findings, and the arming `\\documentclass` may come from a comment too",
|
|
138
|
+
' .map((l) => l.replace(/(^|[^\\\\])%.*$/, "$1"));\n const code = lines.join("\\n");',
|
|
139
|
+
' .map((l) => l);\n const code = lines.join("\\n"); /* MUT */',
|
|
140
|
+
],
|
|
141
|
+
// ⚠️ `frontmatter/lastIndex reset` STOOD HERE AND WAS REMOVED, not silenced. It mutated away
|
|
142
|
+
// an explicit `re.lastIndex = 0` and the harness stayed GREEN — which by this file's own
|
|
143
|
+
// ranking is a finding about the TEST or about the CODE, never a pass. It was the code: a
|
|
144
|
+
// `/g` `exec` that returns `null` resets `lastIndex` itself, and the loop always drains the
|
|
145
|
+
// regex, so the line was a guard nothing could kill. The line is gone; the property is held
|
|
146
|
+
// by the harness assertion that lints one input twice in a single process.
|
|
147
|
+
[
|
|
148
|
+
"frontmatter/line",
|
|
149
|
+
"report every finding on line 1 — the address of the offending preamble line is lost",
|
|
150
|
+
" start: { line: i + 1, column },",
|
|
151
|
+
" start: { line: 1, column: 1 }, /* MUT */",
|
|
152
|
+
],
|
|
153
|
+
[
|
|
154
|
+
"frontmatter/all matches per line",
|
|
155
|
+
"take only the FIRST match of each pattern per line — a compacted preamble is under-counted",
|
|
156
|
+
" while ((m = re.exec(line)) !== null) {",
|
|
157
|
+
" if ((m = re.exec(line)) !== null) { /* MUT */",
|
|
158
|
+
],
|
|
159
|
+
[
|
|
160
|
+
"frontmatter/column",
|
|
161
|
+
"report the start of the line instead of the start of the match (second rule's copy)",
|
|
162
|
+
" const column = m.index + 1;",
|
|
163
|
+
" const column = 1; /* MUT */",
|
|
164
|
+
],
|
|
165
|
+
[
|
|
166
|
+
"disk read/quiet instead of throwing",
|
|
167
|
+
"remove the `try/catch` around the read — the rule THROWS on a stdin run where there is no file",
|
|
168
|
+
' try {\n text = readFileSync(context.filename, "utf8");\n } catch {\n return;\n }\n if (REVIEW_MODE_RE.test(text)) return;',
|
|
169
|
+
' text = readFileSync(context.filename, "utf8"); /* MUT */\n if (REVIEW_MODE_RE.test(text)) return;',
|
|
170
|
+
],
|
|
171
|
+
];
|
|
172
|
+
|
|
173
|
+
const runHarness = () => {
|
|
174
|
+
try {
|
|
175
|
+
execFileSync("npx", ["vigiles", "test", HARNESS], {
|
|
176
|
+
encoding: "utf8",
|
|
177
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
178
|
+
});
|
|
179
|
+
return { failed: false, out: "" };
|
|
180
|
+
} catch (e) {
|
|
181
|
+
return { failed: true, out: (e.stdout ?? "") + (e.stderr ?? "") };
|
|
182
|
+
}
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
// 🔴 A BASELINE RUN BEFORE ANY MUTATION. A red harness makes EVERY mutation look "killed", and
|
|
186
|
+
// the battery would print a confident green verdict about a defence that is not there.
|
|
187
|
+
{
|
|
188
|
+
const base = runHarness();
|
|
189
|
+
if (base.failed) {
|
|
190
|
+
console.log(
|
|
191
|
+
"❌ THE HARNESS IS RED BEFORE ANY MUTATION — the battery cannot tell a killed mutation " +
|
|
192
|
+
"from that:\n" +
|
|
193
|
+
base.out.slice(-1500),
|
|
194
|
+
);
|
|
195
|
+
process.exit(1);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
let ok = 0;
|
|
200
|
+
let bad = 0;
|
|
201
|
+
const rows = [];
|
|
202
|
+
for (const [label, what, from, to] of M) {
|
|
203
|
+
const hits = PRISTINE.split(from).length - 1;
|
|
204
|
+
if (hits !== 1) {
|
|
205
|
+
console.log(
|
|
206
|
+
`❌ ${label}: TARGET ${hits === 0 ? "NOT FOUND" : `NOT UNIQUE (${hits})`} — ${from.slice(0, 70)}`,
|
|
207
|
+
);
|
|
208
|
+
bad++;
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
writeFileSync(RULES, PRISTINE.replace(from, to));
|
|
212
|
+
const on = readFileSync(RULES, "utf8");
|
|
213
|
+
if (!on.includes(to) || on.includes(from)) {
|
|
214
|
+
console.log(
|
|
215
|
+
`❌ ${label}: THE MUTATION DID NOT LAND (checked by re-reading the file)`,
|
|
216
|
+
);
|
|
217
|
+
bad++;
|
|
218
|
+
writeFileSync(RULES, PRISTINE);
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
const { failed, out } = runHarness();
|
|
222
|
+
writeFileSync(RULES, PRISTINE);
|
|
223
|
+
if (readFileSync(RULES, "utf8") !== PRISTINE) {
|
|
224
|
+
console.log(`❌ ${label}: THE ROLLBACK FAILED`);
|
|
225
|
+
bad++;
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
// The verdict carries the HARNESS LINE and the assertion text: "killed" without saying by
|
|
229
|
+
// what is half an answer. The battery must show that each mutation died on ITS OWN assertion.
|
|
230
|
+
const at = (out.match(/tex-build\.harness\.mjs:(\d+)/) ?? [])[1];
|
|
231
|
+
const msg =
|
|
232
|
+
(out.match(/AssertionError[^:]*: ([^\n]+)/) ?? [])[1] ??
|
|
233
|
+
// Not every mutation dies on an assertion: removing the `try/catch` crashes the linter
|
|
234
|
+
// itself, and then "what it died on" is a thrown error. Without this branch the column
|
|
235
|
+
// would come out EMPTY — "killed by something unknown", which is the half-answer this
|
|
236
|
+
// battery forbids.
|
|
237
|
+
(out.match(/((?:Error|ENOENT)[^\n]*)/) ?? [])[1] ??
|
|
238
|
+
(out.match(
|
|
239
|
+
/([^\n]*(?:must|the wrong|drifted|was lost|did not fire)[^\n]*)/,
|
|
240
|
+
) ?? [])[1] ??
|
|
241
|
+
"";
|
|
242
|
+
const where = (at ? `harness:${at} · ` : "") + msg;
|
|
243
|
+
if (failed) {
|
|
244
|
+
ok++;
|
|
245
|
+
rows.push([label, what, where.trim().slice(0, 130)]);
|
|
246
|
+
} else {
|
|
247
|
+
console.log(
|
|
248
|
+
`🔴 ${label}: THE HARNESS IS GREEN UNDER THE MUTATION — a finding about the TEST, not a conclusion about the defence`,
|
|
249
|
+
);
|
|
250
|
+
bad++;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
console.log("\n| property | mutation | what the harness died on |");
|
|
254
|
+
console.log("|---|---|---|");
|
|
255
|
+
for (const [a, b, c] of rows)
|
|
256
|
+
console.log(`| \`${a}\` | ${b} | ${c.replace(/\|/g, "\\|")} |`);
|
|
257
|
+
console.log(`\n${ok} mutation(s) killed, ${bad} problem(s)`);
|
|
258
|
+
process.exit(bad ? 1 : 0);
|