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,753 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Colocated test for `eslint-rules/tex-build.mjs` (`tex/future-promise`).
|
|
3
|
+
* Run: `npx vigiles test eslint-rules/tex-build.harness.mjs`
|
|
4
|
+
*
|
|
5
|
+
* FIVE PARTS, AND THE ORDER IS LOAD-BEARING:
|
|
6
|
+
* I. FIXTURES ON DISK — `fixtures/tex-build/clean.tex` must be silent, `defect.tex` must
|
|
7
|
+
* fire, and both are checked through the real `eslint.config.mjs`, not through a
|
|
8
|
+
* config invented here. A frozen finding set, positions and captured text included.
|
|
9
|
+
* II. QUIET ON A CORRECT INPUT, and EACH reason for the silence is checked separately.
|
|
10
|
+
* It comes before "fires on a planted defect" deliberately: a rule that fails a correct
|
|
11
|
+
* input is more dangerous than no rule at all — it gets switched off the same day.
|
|
12
|
+
* III. FIRES ON A PLANTED DEFECT — on the very one the check was added for on 2026-08-24,
|
|
13
|
+
* plus one input per branch of the promise vocabulary.
|
|
14
|
+
* IV. PROPERTIES NOBODY "SEES", WITHOUT WHICH THE RULE LIES.
|
|
15
|
+
* V. CONFIG: the rule is declared on `.tex`, its severity is the measured one, and the
|
|
16
|
+
* glob it is declared on is NOT EMPTY.
|
|
17
|
+
* VI. THE SECOND RULE OF THIS MODULE — `tex/acm-frontmatter-override` — with the same five
|
|
18
|
+
* parts in the same order, kept in one self-contained block rather than interleaved.
|
|
19
|
+
* The two rules share a file and share nothing else: one reads the PROSE of a shipped
|
|
20
|
+
* build, the other reads the PREAMBLE of any acmart build, and a test that mixed them
|
|
21
|
+
* would make each rule's silence depend on the other's input.
|
|
22
|
+
*
|
|
23
|
+
* 🔴 ZERO ON A CLEAN FIXTURE IS NOT "CHECKED". Part I on its own cannot tell a clean input
|
|
24
|
+
* from a dead rule: for an advisory check, silence IS its success state. Part III is what
|
|
25
|
+
* distinguishes them — the planted defect. Both halves are mandatory, and separately neither
|
|
26
|
+
* proves anything.
|
|
27
|
+
*
|
|
28
|
+
* ── PARITY WITH THE PREVIOUS IMPLEMENTATION (measured 2026-09-07, BEFORE its removal) ──
|
|
29
|
+
* There is deliberately NO copy of the old script's `checkFuturePromises` here: vendoring it
|
|
30
|
+
* for the sake of a test would re-create the second source of truth the move was meant to
|
|
31
|
+
* remove. What lives here instead is the RESULT of the comparison, taken by a differential run
|
|
32
|
+
* over 15 inputs (10 real `.tex` files of the source corpus + 5 synthetic mutations on top of
|
|
33
|
+
* a real paper):
|
|
34
|
+
* the finding sets agreed on ALL FIFTEEN, positions and captured text included
|
|
35
|
+
* (`284:will be released`, `284:Upon acceptance` + `285:will be made publicly available`).
|
|
36
|
+
*
|
|
37
|
+
* 🔴 AND ONE DISAGREEMENT, WHICH IS NOT IN THE LOGIC BUT IN THE INPUT — and it is the whole
|
|
38
|
+
* gain. The old implementation picked its file by looking for `paper.md` / `draft.md` and
|
|
39
|
+
* otherwise taking the FIRST `.tex` in `readdirSync` order. Measured 2026-09-07:
|
|
40
|
+
* paper A → a 536-BYTE file of nine `\def`s with venue numbers. The check, added
|
|
41
|
+
* specifically BECAUSE of paper A, was not reading paper A;
|
|
42
|
+
* paper B → a four-month-old draft, one of seven `.tex` files in that directory;
|
|
43
|
+
* paper C → `paper.tex`, the only `.tex` there; it matched.
|
|
44
|
+
* The finding sets agreed anyway (all empty), but they agreed BY ACCIDENT, not by
|
|
45
|
+
* construction. A config glob removes the dependency of the address on directory order.
|
|
46
|
+
*
|
|
47
|
+
* ⚠️ WHAT WAS LOST WHEN THIS TEST LEFT THE PRIVATE CORPUS BEHIND, stated rather than glossed:
|
|
48
|
+
* part I used to freeze the finding set of three REAL papers, and one assertion used a real
|
|
49
|
+
* paper as a LIVE SUBJECT for the commented-out-`review` case. Fixtures cannot reproduce "this
|
|
50
|
+
* is what the corpus actually looks like today"; they reproduce the mechanism only. The
|
|
51
|
+
* measurements from those runs are kept in the comments, the papers are not.
|
|
52
|
+
*
|
|
53
|
+
* 🔴 Assertions live at MODULE TOP LEVEL: `vigiles test` imports the file and treats "it did
|
|
54
|
+
* not throw" as success; an exported `tests` object is run by nothing.
|
|
55
|
+
*/
|
|
56
|
+
import { ESLint } from "eslint";
|
|
57
|
+
import assert from "node:assert/strict";
|
|
58
|
+
import {
|
|
59
|
+
existsSync,
|
|
60
|
+
mkdirSync,
|
|
61
|
+
mkdtempSync,
|
|
62
|
+
readFileSync,
|
|
63
|
+
rmSync,
|
|
64
|
+
writeFileSync,
|
|
65
|
+
realpathSync,
|
|
66
|
+
} from "node:fs";
|
|
67
|
+
import { tmpdir } from "node:os";
|
|
68
|
+
import { dirname, join, relative } from "node:path";
|
|
69
|
+
import { fileURLToPath } from "node:url";
|
|
70
|
+
|
|
71
|
+
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
72
|
+
const ROOT = join(HERE, ".."); // eslint-rules → repository root
|
|
73
|
+
const RULES = join(HERE, "tex-build.mjs");
|
|
74
|
+
assert.ok(existsSync(RULES), `no entry point for the test: ${RULES}`);
|
|
75
|
+
|
|
76
|
+
const TMP = realpathSync(mkdtempSync(join(tmpdir(), "tex-build-")));
|
|
77
|
+
// Cleanup is registered IMMEDIATELY, not at the end of the file: assertions throw, and an
|
|
78
|
+
// `rmSync` at the bottom never runs in exactly the runs that are red.
|
|
79
|
+
process.on("exit", () => rmSync(TMP, { recursive: true, force: true }));
|
|
80
|
+
|
|
81
|
+
const { texLanguage } = await import(join(HERE, "latex-language.mjs"));
|
|
82
|
+
const texBuild = (await import(RULES)).default;
|
|
83
|
+
|
|
84
|
+
// A new rule with no declared severity must not slip through unnoticed.
|
|
85
|
+
assert.deepEqual(
|
|
86
|
+
Object.keys(texBuild).sort(),
|
|
87
|
+
["acm-frontmatter-override", "future-promise"],
|
|
88
|
+
"the module's rule set changed — update the config and this harness",
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
const makeEslint = (cwd) =>
|
|
92
|
+
new ESLint({
|
|
93
|
+
cwd,
|
|
94
|
+
overrideConfigFile: true,
|
|
95
|
+
overrideConfig: [
|
|
96
|
+
{
|
|
97
|
+
files: ["**/*.tex"],
|
|
98
|
+
plugins: {
|
|
99
|
+
tex: { languages: { latex: texLanguage }, rules: texBuild },
|
|
100
|
+
},
|
|
101
|
+
language: "tex/latex",
|
|
102
|
+
rules: { "tex/future-promise": "warn" },
|
|
103
|
+
},
|
|
104
|
+
],
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
/** Findings for one file as `{line, column, text}` — and not one of them fatal. */
|
|
108
|
+
async function lintTex(eslint, file) {
|
|
109
|
+
const [res] = await eslint.lintFiles([file]);
|
|
110
|
+
assert.deepEqual(
|
|
111
|
+
res.messages.filter((m) => m.fatal || !m.ruleId),
|
|
112
|
+
[],
|
|
113
|
+
`${file}: the language failed to parse or the rule threw — that is a linter crash, not a finding`,
|
|
114
|
+
);
|
|
115
|
+
return res.messages.map((m) => ({
|
|
116
|
+
rule: m.ruleId,
|
|
117
|
+
line: m.line,
|
|
118
|
+
column: m.column,
|
|
119
|
+
// The captured promise, taken out of the message itself. What matters is WHAT was caught,
|
|
120
|
+
// not only how much: a rule catching the wrong substring yields the same count.
|
|
121
|
+
text: /«([^»]+)»/.exec(m.message)?.[1] ?? null,
|
|
122
|
+
}));
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
let fixtureN = 0;
|
|
126
|
+
/** Writes a `.tex` into a temp directory and returns the findings. */
|
|
127
|
+
async function findings(name, text) {
|
|
128
|
+
const dir = join(TMP, `f${fixtureN++}-${name.replace(/[^a-z0-9-]/gi, "_")}`);
|
|
129
|
+
mkdirSync(dir, { recursive: true });
|
|
130
|
+
const file = join(dir, "paper.tex");
|
|
131
|
+
writeFileSync(file, text);
|
|
132
|
+
return lintTex(makeEslint(TMP), file);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** A minimal build: NOT review mode, with somewhere to put prose. */
|
|
136
|
+
const build = (
|
|
137
|
+
body,
|
|
138
|
+
{ cls = "\\documentclass[sigconf,screen]{acmart}" } = {},
|
|
139
|
+
) =>
|
|
140
|
+
[
|
|
141
|
+
cls,
|
|
142
|
+
"\\begin{document}",
|
|
143
|
+
"\\begin{abstract}",
|
|
144
|
+
body,
|
|
145
|
+
"\\end{abstract}",
|
|
146
|
+
"\\end{document}",
|
|
147
|
+
"",
|
|
148
|
+
].join("\n");
|
|
149
|
+
|
|
150
|
+
// ═════════════════════════════════════════════════════════════════════════════
|
|
151
|
+
// I. FIXTURES ON DISK, THROUGH THE REPOSITORY'S OWN CONFIG
|
|
152
|
+
// ═════════════════════════════════════════════════════════════════════════════
|
|
153
|
+
// 🔴 Deliberately NOT `makeEslint` here. Everywhere else in this file the config is built
|
|
154
|
+
// in-process, which tests the rule and says nothing about whether the rule is WIRED UP. These
|
|
155
|
+
// two runs go through `eslint.config.mjs` as it sits on disk, so a rule that is correct and
|
|
156
|
+
// unreferenced fails here.
|
|
157
|
+
const CLEAN_FIXTURE = join(ROOT, "fixtures/tex-build/clean.tex");
|
|
158
|
+
const DEFECT_FIXTURE = join(ROOT, "fixtures/tex-build/defect.tex");
|
|
159
|
+
const real = new ESLint({ cwd: ROOT });
|
|
160
|
+
|
|
161
|
+
for (const f of [CLEAN_FIXTURE, DEFECT_FIXTURE])
|
|
162
|
+
assert.ok(existsSync(f), `missing fixture: ${f}`);
|
|
163
|
+
|
|
164
|
+
assert.deepEqual(
|
|
165
|
+
await lintTex(real, CLEAN_FIXTURE),
|
|
166
|
+
[],
|
|
167
|
+
"fixtures/tex-build/clean.tex produced findings — a check that fails a correct input gets " +
|
|
168
|
+
"switched off the same day",
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
// 🔴 AND THE REASON FOR THAT ZERO IS CHECKED SEPARATELY. "Zero because the build is clean" and
|
|
172
|
+
// "zero because the build is exempt as review mode" are indistinguishable from the outside; the
|
|
173
|
+
// only thing telling them apart is that the exemption sign is genuinely absent from the file.
|
|
174
|
+
// Without this assertion, someone editing the fixture into a review build would turn the quiet
|
|
175
|
+
// half into a tautology and nothing would notice.
|
|
176
|
+
assert.doesNotMatch(
|
|
177
|
+
readFileSync(CLEAN_FIXTURE, "utf8"),
|
|
178
|
+
/^[^%\n]*(?:\\documentclass\[[^\]]*\breview\b|printacmref=false)/m,
|
|
179
|
+
"fixtures/tex-build/clean.tex became a review build — its zero stopped meaning `the rule ran " +
|
|
180
|
+
"and found nothing` and started meaning `the rule was exempt`",
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
// The frozen finding set of the defect fixture: not "some findings", but exactly these two,
|
|
184
|
+
// at these addresses, with these captured substrings.
|
|
185
|
+
assert.deepEqual(
|
|
186
|
+
await lintTex(real, DEFECT_FIXTURE),
|
|
187
|
+
[
|
|
188
|
+
{
|
|
189
|
+
rule: "tex/future-promise",
|
|
190
|
+
line: 15,
|
|
191
|
+
column: 59,
|
|
192
|
+
text: "at camera-ready",
|
|
193
|
+
},
|
|
194
|
+
{
|
|
195
|
+
rule: "tex/future-promise",
|
|
196
|
+
line: 22,
|
|
197
|
+
column: 52,
|
|
198
|
+
text: "will be made publicly available",
|
|
199
|
+
},
|
|
200
|
+
],
|
|
201
|
+
"fixtures/tex-build/defect.tex: the finding set drifted from the frozen measurement",
|
|
202
|
+
);
|
|
203
|
+
|
|
204
|
+
// ═════════════════════════════════════════════════════════════════════════════
|
|
205
|
+
// II. QUIET ON A CORRECT INPUT — each reason for the silence on its own
|
|
206
|
+
// ═════════════════════════════════════════════════════════════════════════════
|
|
207
|
+
assert.deepEqual(
|
|
208
|
+
await findings("clean-build", build("We measured how often a rule fails.")),
|
|
209
|
+
[],
|
|
210
|
+
"a build with no promises must be silent",
|
|
211
|
+
);
|
|
212
|
+
|
|
213
|
+
// A promise made DURING PEER REVIEW is true and ordinary. This is not leniency, it is the
|
|
214
|
+
// subject: the defect is only a promise that outlived its own delivery.
|
|
215
|
+
for (const [label, cls] of [
|
|
216
|
+
["review option", "\\documentclass[sigconf,review]{acmart}"],
|
|
217
|
+
[
|
|
218
|
+
"printacmref=false",
|
|
219
|
+
"\\documentclass[sigconf]{acmart}\n\\settopmatter{printacmref=false}",
|
|
220
|
+
],
|
|
221
|
+
])
|
|
222
|
+
assert.deepEqual(
|
|
223
|
+
await findings(
|
|
224
|
+
`review-${label}`,
|
|
225
|
+
build("The harness ships at camera-ready.", { cls }),
|
|
226
|
+
),
|
|
227
|
+
[],
|
|
228
|
+
`${label}: a review build must be exempt`,
|
|
229
|
+
);
|
|
230
|
+
|
|
231
|
+
// 🔴 A LaTeX COMMENT IS A LEGITIMATE PLACE FOR THESE WORDS. Author notes of the form
|
|
232
|
+
// `% camera-ready blocker` live in real sources, and a finding on them would be exactly the
|
|
233
|
+
// false positive that kills a check.
|
|
234
|
+
assert.deepEqual(
|
|
235
|
+
await findings(
|
|
236
|
+
"promise-in-comment",
|
|
237
|
+
build("% blocker: the harness ships at camera-ready."),
|
|
238
|
+
),
|
|
239
|
+
[],
|
|
240
|
+
"a promise inside a LaTeX comment is not a finding",
|
|
241
|
+
);
|
|
242
|
+
|
|
243
|
+
// ═════════════════════════════════════════════════════════════════════════════
|
|
244
|
+
// III. FIRES ON A PLANTED DEFECT
|
|
245
|
+
// ═════════════════════════════════════════════════════════════════════════════
|
|
246
|
+
{
|
|
247
|
+
// THE defect: the line printed in an accepted camera-ready on 2026-08-24 in both of the most
|
|
248
|
+
// read positions, while the Availability paragraph three lines below said the opposite.
|
|
249
|
+
const ms = await findings(
|
|
250
|
+
"the-2026-08-24-defect",
|
|
251
|
+
build("We report the finding (full harness at camera-ready)."),
|
|
252
|
+
);
|
|
253
|
+
assert.equal(ms.length, 1, "the historical defect must be caught");
|
|
254
|
+
assert.equal(ms[0].rule, "tex/future-promise");
|
|
255
|
+
assert.equal(ms[0].text, "at camera-ready", "the wrong substring was caught");
|
|
256
|
+
// The address is what the move to a lint rule bought, so it is checked rather than declared:
|
|
257
|
+
// line 4 of the fixture, and a column at the start of the match, not at the start of the line.
|
|
258
|
+
assert.equal(
|
|
259
|
+
ms[0].line,
|
|
260
|
+
4,
|
|
261
|
+
"the finding must point at the line carrying the promise",
|
|
262
|
+
);
|
|
263
|
+
assert.equal(
|
|
264
|
+
ms[0].column,
|
|
265
|
+
"We report the finding (full harness ".length + 1,
|
|
266
|
+
"the column must point at the start of the match",
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Every branch of the promise vocabulary gets its own input. Without that, "a vocabulary of six
|
|
271
|
+
// forms" is a claim verified by one of the six.
|
|
272
|
+
for (const [sentence, want] of [
|
|
273
|
+
["The harness ships at camera-ready.", "at camera-ready"],
|
|
274
|
+
["The harness ships at the camera ready stage.", "at the camera ready"],
|
|
275
|
+
["Upon acceptance we open the repository.", "Upon acceptance"],
|
|
276
|
+
["On acceptance we open the repository.", "On acceptance"],
|
|
277
|
+
["The harness will be released later.", "will be released"],
|
|
278
|
+
["The harness will be published later.", "will be published"],
|
|
279
|
+
[
|
|
280
|
+
"The data will be made publicly available.",
|
|
281
|
+
"will be made publicly available",
|
|
282
|
+
],
|
|
283
|
+
["The data will be made available.", "will be made available"],
|
|
284
|
+
["The harness will be public.", "will be public"],
|
|
285
|
+
["The harness will be open-sourced.", "will be open-sourced"],
|
|
286
|
+
]) {
|
|
287
|
+
const ms = await findings(`vocab-${want}`, build(sentence));
|
|
288
|
+
assert.equal(
|
|
289
|
+
ms.length,
|
|
290
|
+
1,
|
|
291
|
+
`vocabulary branch «${want}» did not fire on «${sentence}»`,
|
|
292
|
+
);
|
|
293
|
+
assert.equal(
|
|
294
|
+
ms[0].text,
|
|
295
|
+
want,
|
|
296
|
+
`vocabulary branch «${want}» caught the wrong substring`,
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Two promises on different lines do not collapse into one (the rule reports EVERY place).
|
|
301
|
+
{
|
|
302
|
+
const ms = await findings(
|
|
303
|
+
"two-lines",
|
|
304
|
+
build(
|
|
305
|
+
"Upon acceptance we ship.\nThe data will be made publicly available.",
|
|
306
|
+
),
|
|
307
|
+
);
|
|
308
|
+
assert.deepEqual(
|
|
309
|
+
ms.map((m) => `${m.line}:${m.text}`),
|
|
310
|
+
["4:Upon acceptance", "5:will be made publicly available"],
|
|
311
|
+
"two promises on different lines must give two findings with their own addresses",
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// ═════════════════════════════════════════════════════════════════════════════
|
|
316
|
+
// IV. PROPERTIES NOBODY "SEES", WITHOUT WHICH THE RULE LIES
|
|
317
|
+
// ═════════════════════════════════════════════════════════════════════════════
|
|
318
|
+
// 🔴 A COMMENTED-OUT `\documentclass[…review…]` DOES NOT MAKE A BUILD A REVIEW BUILD. In the
|
|
319
|
+
// source corpus this was a live property, not an invented case: one paper carried
|
|
320
|
+
// `% was: \documentclass[sigconf,review]{acmart}` a few lines above its real
|
|
321
|
+
// `\documentclass[sigconf,screen]{acmart}`. Without the `^[^%\n]*` anchor in `REVIEW_MODE_RE`
|
|
322
|
+
// that whole paper would have been silently exempted by the history of its own edits.
|
|
323
|
+
// ⚠️ The live half of this assertion (reading the real paper) did not come along with the
|
|
324
|
+
// extraction — the paper is private. What is left is the synthetic half, which pins the
|
|
325
|
+
// mechanism but no longer proves the case still occurs in the wild.
|
|
326
|
+
{
|
|
327
|
+
const ms = await findings(
|
|
328
|
+
"commented-out-review-class",
|
|
329
|
+
build("The harness ships at camera-ready.", {
|
|
330
|
+
cls: "% was: \\documentclass[sigconf,review]{acmart}\n\\documentclass[sigconf,screen]{acmart}",
|
|
331
|
+
}),
|
|
332
|
+
);
|
|
333
|
+
assert.equal(
|
|
334
|
+
ms.length,
|
|
335
|
+
1,
|
|
336
|
+
"a commented-out review class exempted the build — the `^[^%\\n]*` anchor was lost",
|
|
337
|
+
);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// An escaped `\%` is NOT a comment: a line where a percent sign precedes a promise must still
|
|
341
|
+
// be checked in full.
|
|
342
|
+
{
|
|
343
|
+
const ms = await findings(
|
|
344
|
+
"escaped-percent",
|
|
345
|
+
build("We cover 95\\% of cases and the harness will be published later."),
|
|
346
|
+
);
|
|
347
|
+
assert.equal(
|
|
348
|
+
ms.length,
|
|
349
|
+
1,
|
|
350
|
+
"`\\%` ate the rest of the line — the promise behind it went invisible",
|
|
351
|
+
);
|
|
352
|
+
assert.equal(ms[0].text, "will be published");
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// No file on disk (a run through stdin): the rule stays quiet rather than judging content it
|
|
356
|
+
// never saw.
|
|
357
|
+
{
|
|
358
|
+
const [res] = await makeEslint(TMP).lintText(
|
|
359
|
+
build("The harness ships at camera-ready."),
|
|
360
|
+
{ filePath: join(TMP, "no-such-dir", "paper.tex") },
|
|
361
|
+
);
|
|
362
|
+
assert.deepEqual(
|
|
363
|
+
res.messages.filter((m) => m.fatal),
|
|
364
|
+
[],
|
|
365
|
+
"the rule threw on a non-existent path",
|
|
366
|
+
);
|
|
367
|
+
assert.deepEqual(
|
|
368
|
+
res.messages.map((m) => m.ruleId),
|
|
369
|
+
[],
|
|
370
|
+
"the rule reads the file FROM DISK (the preamble is blanked in the projection), and on a " +
|
|
371
|
+
"path with no file it must stay quiet rather than judge",
|
|
372
|
+
);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// ═════════════════════════════════════════════════════════════════════════════
|
|
376
|
+
// V. CONFIG — the rule is on, at the measured severity, over a NON-EMPTY glob
|
|
377
|
+
// ═════════════════════════════════════════════════════════════════════════════
|
|
378
|
+
{
|
|
379
|
+
const config = (await import(join(ROOT, "eslint.config.mjs"))).default;
|
|
380
|
+
const tex = config.find((b) => b.rules?.["tex/future-promise"]);
|
|
381
|
+
assert.ok(
|
|
382
|
+
tex,
|
|
383
|
+
"eslint.config.mjs declares no block carrying `tex/future-promise`",
|
|
384
|
+
);
|
|
385
|
+
assert.equal(
|
|
386
|
+
tex.rules["tex/future-promise"],
|
|
387
|
+
"warn",
|
|
388
|
+
"the severity of `tex/future-promise` changed. `warn` is an analysis, not caution: the " +
|
|
389
|
+
"finding is not binary (a promise in a camera-ready is sometimes honest, and the verdict " +
|
|
390
|
+
"«does it contradict Availability» is a human one), plus there is a demonstrable class of " +
|
|
391
|
+
"false positives — a sentence about SOMEONE ELSE's work («their replication will be " +
|
|
392
|
+
"published in 2027»)",
|
|
393
|
+
);
|
|
394
|
+
assert.ok(
|
|
395
|
+
tex.plugins?.tex?.rules === texBuild,
|
|
396
|
+
"the `.tex` block no longer wires up `tex-build.mjs` — the rule stopped executing",
|
|
397
|
+
);
|
|
398
|
+
|
|
399
|
+
// 🔴 AND THE HALF WITHOUT WHICH ALL OF THE ABOVE IS DECORATION: the glob must match files
|
|
400
|
+
// that exist. A rule whose glob matches nothing is never invoked, and the run is green with
|
|
401
|
+
// zero findings — byte-identical to a rule that passed. See `scripts/rules-see-files.mjs`,
|
|
402
|
+
// which checks this for every declared rule; the assertion here is the cheap local copy so
|
|
403
|
+
// that this rule's own wiring fails in this rule's own test.
|
|
404
|
+
const seen = (await real.lintFiles(["."])).map((r) =>
|
|
405
|
+
relative(ROOT, r.filePath),
|
|
406
|
+
);
|
|
407
|
+
const texSeen = seen.filter((f) => f.endsWith(".tex"));
|
|
408
|
+
assert.ok(
|
|
409
|
+
texSeen.length > 0,
|
|
410
|
+
"the `.tex` glob of eslint.config.mjs matched NO files on disk — the rule was never " +
|
|
411
|
+
"invoked, and this run's zero findings mean nothing. Files linted: " +
|
|
412
|
+
seen.join(", "),
|
|
413
|
+
);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// ═════════════════════════════════════════════════════════════════════════════
|
|
417
|
+
// VI. tex/acm-frontmatter-override — the module's second rule, tested end to end
|
|
418
|
+
// ═════════════════════════════════════════════════════════════════════════════
|
|
419
|
+
// 🔴 ITS OWN ESLint FACTORY, with ONLY this rule enabled. Sharing `makeEslint` would make the
|
|
420
|
+
// two rules' findings land in one array, and then "quiet" for one of them would depend on the
|
|
421
|
+
// other's input — the class of tautology part I already guards against on the fixtures.
|
|
422
|
+
const makeEslintFm = (cwd) =>
|
|
423
|
+
new ESLint({
|
|
424
|
+
cwd,
|
|
425
|
+
overrideConfigFile: true,
|
|
426
|
+
overrideConfig: [
|
|
427
|
+
{
|
|
428
|
+
files: ["**/*.tex"],
|
|
429
|
+
plugins: {
|
|
430
|
+
tex: { languages: { latex: texLanguage }, rules: texBuild },
|
|
431
|
+
},
|
|
432
|
+
language: "tex/latex",
|
|
433
|
+
rules: { "tex/acm-frontmatter-override": "warn" },
|
|
434
|
+
},
|
|
435
|
+
],
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
let fmN = 0;
|
|
439
|
+
async function fmFindings(name, text) {
|
|
440
|
+
const dir = join(TMP, `fm${fmN++}-${name.replace(/[^a-z0-9-]/gi, "_")}`);
|
|
441
|
+
mkdirSync(dir, { recursive: true });
|
|
442
|
+
const file = join(dir, "paper.tex");
|
|
443
|
+
writeFileSync(file, text);
|
|
444
|
+
return lintTex(makeEslintFm(TMP), file);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/** A minimal acmart preamble plus a body, so the language has something to parse. */
|
|
448
|
+
const acm = (
|
|
449
|
+
preambleTail,
|
|
450
|
+
{ cls = "\\documentclass[sigconf,screen]{acmart}" } = {},
|
|
451
|
+
) =>
|
|
452
|
+
[
|
|
453
|
+
cls,
|
|
454
|
+
preambleTail,
|
|
455
|
+
"\\begin{document}",
|
|
456
|
+
"\\begin{abstract}",
|
|
457
|
+
"We measured one thing and report it plainly.",
|
|
458
|
+
"\\end{abstract}",
|
|
459
|
+
"\\end{document}",
|
|
460
|
+
"",
|
|
461
|
+
].join("\n");
|
|
462
|
+
|
|
463
|
+
// ── VI.I FIXTURES ON DISK, THROUGH THE REPOSITORY'S OWN CONFIG ───────────────
|
|
464
|
+
const FM_CLEAN = join(ROOT, "fixtures/tex-build/frontmatter-clean.tex");
|
|
465
|
+
const FM_DEFECT = join(ROOT, "fixtures/tex-build/frontmatter-defect.tex");
|
|
466
|
+
for (const f of [FM_CLEAN, FM_DEFECT])
|
|
467
|
+
assert.ok(existsSync(f), `missing fixture: ${f}`);
|
|
468
|
+
|
|
469
|
+
assert.deepEqual(
|
|
470
|
+
await lintTex(real, FM_CLEAN),
|
|
471
|
+
[],
|
|
472
|
+
"fixtures/tex-build/frontmatter-clean.tex produced findings — a check that fails a correct " +
|
|
473
|
+
"input gets switched off the same day",
|
|
474
|
+
);
|
|
475
|
+
|
|
476
|
+
// 🔴 AND THE REASONS FOR THAT ZERO, EACH CHECKED SEPARATELY. Three different edits to the
|
|
477
|
+
// fixture would turn its silence into a tautology, and from the outside all three look the
|
|
478
|
+
// same as "the rule ran and the build is clean":
|
|
479
|
+
// (a) it stops being an acmart build → the rule returns before looking at anything;
|
|
480
|
+
// (b) it acquires `nonacm` → the rule is exempt;
|
|
481
|
+
// (c) the commented override block disappears → the comment-blanking half of the rule stops
|
|
482
|
+
// being exercised at all, and that half is the one that protects the accepted sibling
|
|
483
|
+
// paper of the source corpus, which keeps exactly such a block as a record.
|
|
484
|
+
{
|
|
485
|
+
const src = readFileSync(FM_CLEAN, "utf8");
|
|
486
|
+
assert.match(
|
|
487
|
+
src,
|
|
488
|
+
/^\\documentclass(?:\s*\[[^\]]*\])?\s*\{acmart\}/m,
|
|
489
|
+
"(a) fixtures/tex-build/frontmatter-clean.tex stopped being an acmart build — its zero now " +
|
|
490
|
+
"means «the rule returned early», not «the rule ran and found nothing»",
|
|
491
|
+
);
|
|
492
|
+
assert.doesNotMatch(
|
|
493
|
+
src,
|
|
494
|
+
/^\\documentclass\s*\[[^\]]*\bnonacm\b/m,
|
|
495
|
+
"(b) fixtures/tex-build/frontmatter-clean.tex became a `nonacm` build — its zero now means " +
|
|
496
|
+
"«exempt», not «clean»",
|
|
497
|
+
);
|
|
498
|
+
assert.match(
|
|
499
|
+
src,
|
|
500
|
+
/^%.*\\setcopyright\{none\}/m,
|
|
501
|
+
"(c) fixtures/tex-build/frontmatter-clean.tex lost its COMMENTED override block. That block " +
|
|
502
|
+
"is the fixture's point: it proves a finding is not raised on an author's record of what " +
|
|
503
|
+
"the preamble used to do. Without it the quiet half no longer exercises comment blanking",
|
|
504
|
+
);
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// The frozen finding set of the defect fixture: not "some findings", but exactly these three,
|
|
508
|
+
// at these addresses, with these captured macros.
|
|
509
|
+
assert.deepEqual(
|
|
510
|
+
await lintTex(real, FM_DEFECT),
|
|
511
|
+
[
|
|
512
|
+
{
|
|
513
|
+
rule: "tex/acm-frontmatter-override",
|
|
514
|
+
line: 10,
|
|
515
|
+
column: 1,
|
|
516
|
+
text: "\\setcopyright{none}",
|
|
517
|
+
},
|
|
518
|
+
{
|
|
519
|
+
rule: "tex/acm-frontmatter-override",
|
|
520
|
+
line: 11,
|
|
521
|
+
column: 1,
|
|
522
|
+
text: "\\renewcommand\\footnotetextcopyrightpermission",
|
|
523
|
+
},
|
|
524
|
+
{
|
|
525
|
+
rule: "tex/acm-frontmatter-override",
|
|
526
|
+
line: 12,
|
|
527
|
+
column: 1,
|
|
528
|
+
text: "\\pagestyle{plain}",
|
|
529
|
+
},
|
|
530
|
+
],
|
|
531
|
+
"fixtures/tex-build/frontmatter-defect.tex: the finding set drifted from the frozen measurement",
|
|
532
|
+
);
|
|
533
|
+
|
|
534
|
+
// ── VI.II QUIET ON A CORRECT INPUT — each reason for the silence on its own ──
|
|
535
|
+
assert.deepEqual(
|
|
536
|
+
await fmFindings("plain-acmart", acm("\\setcopyright{cc}")),
|
|
537
|
+
[],
|
|
538
|
+
"an acmart build that leaves the front matter alone must be silent",
|
|
539
|
+
);
|
|
540
|
+
|
|
541
|
+
// OUTSIDE acmart every one of these macros is an ordinary, correct line. `\pagestyle{plain}`
|
|
542
|
+
// in an `article` is not a defect in any sense, and a rule that said so would be switched off
|
|
543
|
+
// by the first person who writes a non-ACM document.
|
|
544
|
+
for (const cls of [
|
|
545
|
+
"\\documentclass{article}",
|
|
546
|
+
"\\documentclass[11pt]{report}",
|
|
547
|
+
"\\documentclass[conference]{IEEEtran}",
|
|
548
|
+
])
|
|
549
|
+
assert.deepEqual(
|
|
550
|
+
await fmFindings(
|
|
551
|
+
`non-acmart-${cls.slice(-12)}`,
|
|
552
|
+
acm("\\pagestyle{plain}", { cls }),
|
|
553
|
+
),
|
|
554
|
+
[],
|
|
555
|
+
`${cls}: the macros only mean something inside acmart — outside it the rule must not fire`,
|
|
556
|
+
);
|
|
557
|
+
|
|
558
|
+
// `nonacm` is acmart's OWN way of saying "this is not going to an ACM venue", and in that mode
|
|
559
|
+
// the class disables the ACM Reference Format itself (acmart.cls:110-121). A build that says
|
|
560
|
+
// so the supported way is exempt — otherwise the rule would punish the very escape hatch its
|
|
561
|
+
// own message recommends.
|
|
562
|
+
for (const cls of [
|
|
563
|
+
"\\documentclass[sigconf,nonacm]{acmart}",
|
|
564
|
+
"\\documentclass[nonacm,screen]{acmart}",
|
|
565
|
+
])
|
|
566
|
+
assert.deepEqual(
|
|
567
|
+
await fmFindings(
|
|
568
|
+
`nonacm-${cls.length}`,
|
|
569
|
+
acm("\\setcopyright{none}\n\\pagestyle{plain}", { cls }),
|
|
570
|
+
),
|
|
571
|
+
[],
|
|
572
|
+
`${cls}: a declared non-ACM build must be exempt`,
|
|
573
|
+
);
|
|
574
|
+
|
|
575
|
+
// A LaTeX COMMENT IS A LEGITIMATE PLACE FOR THESE MACROS — and not a hypothetical one: the
|
|
576
|
+
// accepted paper of the source corpus keeps the whole block commented out as a record of what
|
|
577
|
+
// it used to do. Measured 2026-09-11 on the real files: the rejected paper yields three
|
|
578
|
+
// findings (181,182,183), the accepted one yields ZERO with the same three macros present as
|
|
579
|
+
// comments.
|
|
580
|
+
assert.deepEqual(
|
|
581
|
+
await fmFindings(
|
|
582
|
+
"override-in-comment",
|
|
583
|
+
acm("% was: \\setcopyright{none} + \\pagestyle{plain}\n\\setcopyright{cc}"),
|
|
584
|
+
),
|
|
585
|
+
[],
|
|
586
|
+
"a commented-out override must not fire — an author's record of a removed line is not the line",
|
|
587
|
+
);
|
|
588
|
+
|
|
589
|
+
// 🔴 AND THE SAME FOR THE ARMING SIDE, which is the half a simpler implementation gets wrong:
|
|
590
|
+
// a `\documentclass` quoted inside a comment must not turn a non-acmart file into an acmart
|
|
591
|
+
// one. Here the REAL class is `article`, and only the comment says `acmart`.
|
|
592
|
+
assert.deepEqual(
|
|
593
|
+
await fmFindings(
|
|
594
|
+
"acmart-only-in-comment",
|
|
595
|
+
acm("% was: \\documentclass[sigconf]{acmart}\n\\pagestyle{plain}", {
|
|
596
|
+
cls: "\\documentclass{article}",
|
|
597
|
+
}),
|
|
598
|
+
),
|
|
599
|
+
[],
|
|
600
|
+
"a `\\documentclass{acmart}` quoted in a comment must not arm the rule on a non-acmart build",
|
|
601
|
+
);
|
|
602
|
+
|
|
603
|
+
// ── VI.III FIRES ON A PLANTED DEFECT — one input per member of the set ───────
|
|
604
|
+
for (const [line, want] of [
|
|
605
|
+
["\\setcopyright{none}", "\\setcopyright{none}"],
|
|
606
|
+
["\\setcopyright{ none }", "\\setcopyright{ none }"],
|
|
607
|
+
[
|
|
608
|
+
"\\renewcommand\\footnotetextcopyrightpermission[1]{}",
|
|
609
|
+
"\\renewcommand\\footnotetextcopyrightpermission",
|
|
610
|
+
],
|
|
611
|
+
[
|
|
612
|
+
"\\renewcommand{\\footnotetextcopyrightpermission}[1]{}",
|
|
613
|
+
"\\renewcommand{\\footnotetextcopyrightpermission",
|
|
614
|
+
],
|
|
615
|
+
["\\pagestyle{plain}", "\\pagestyle{plain}"],
|
|
616
|
+
["\\pagestyle{empty}", "\\pagestyle{empty}"],
|
|
617
|
+
["\\thispagestyle{empty}", "\\thispagestyle{empty}"],
|
|
618
|
+
]) {
|
|
619
|
+
const got = await fmFindings(`fire-${want.slice(1, 20)}`, acm(line));
|
|
620
|
+
assert.deepEqual(
|
|
621
|
+
got.map((f) => f.text),
|
|
622
|
+
[want],
|
|
623
|
+
`«${line}» must produce exactly one finding capturing «${want}» — got ${JSON.stringify(got)}`,
|
|
624
|
+
);
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
// The stage does NOT exempt, and this is the assertion that pins the measurement the rule's
|
|
628
|
+
// comment rests on. The obvious design — "only flag these in review mode, the class handles
|
|
629
|
+
// it" — is wrong in BOTH directions, so both directions are frozen here: a review build fires,
|
|
630
|
+
// and so does a camera-ready one. `acmart.cls:93-99`: the `review` option turns on line
|
|
631
|
+
// numbers and `\@ACM@printfoliostrue`, and touches neither the copyright statement nor the
|
|
632
|
+
// permission footnote.
|
|
633
|
+
for (const cls of [
|
|
634
|
+
"\\documentclass[sigconf,review,anonymous]{acmart}",
|
|
635
|
+
"\\documentclass[sigconf,screen]{acmart}",
|
|
636
|
+
"\\documentclass[manuscript]{acmart}",
|
|
637
|
+
])
|
|
638
|
+
assert.deepEqual(
|
|
639
|
+
(
|
|
640
|
+
await fmFindings(
|
|
641
|
+
`stage-${cls.length}`,
|
|
642
|
+
acm("\\setcopyright{none}", { cls }),
|
|
643
|
+
)
|
|
644
|
+
).map((f) => f.text),
|
|
645
|
+
["\\setcopyright{none}"],
|
|
646
|
+
`${cls}: the finding must not depend on the build stage — the overrides strip the same ` +
|
|
647
|
+
"furniture in every mode, and in a camera-ready that is worse, because that is the " +
|
|
648
|
+
"version the digital library keeps",
|
|
649
|
+
);
|
|
650
|
+
|
|
651
|
+
// ── VI.IV PROPERTIES NOBODY "SEES", WITHOUT WHICH THE RULE LIES ─────────────
|
|
652
|
+
// 🔴 THE MODULE-LEVEL REGEXES ARE `/g`, SO THEY CARRY `lastIndex`. Without the reset in the
|
|
653
|
+
// rule, the SECOND file linted in the same process starts searching from wherever the previous
|
|
654
|
+
// file's match ended — i.e. findings would depend on the ORDER files were linted in, which is
|
|
655
|
+
// the exact class of defect that made the previous implementation read the wrong `.tex`. Two
|
|
656
|
+
// runs of the same input through the same process must agree.
|
|
657
|
+
{
|
|
658
|
+
const once = (
|
|
659
|
+
await fmFindings("lastindex-a", acm("\\setcopyright{none}"))
|
|
660
|
+
).map((f) => f.text);
|
|
661
|
+
const twice = (
|
|
662
|
+
await fmFindings("lastindex-b", acm("\\setcopyright{none}"))
|
|
663
|
+
).map((f) => f.text);
|
|
664
|
+
assert.deepEqual(
|
|
665
|
+
[once, twice],
|
|
666
|
+
[["\\setcopyright{none}"], ["\\setcopyright{none}"]],
|
|
667
|
+
"the second lint in the same process disagreed with the first — a `/g` regex kept its " +
|
|
668
|
+
"`lastIndex` across files, and findings became order-dependent",
|
|
669
|
+
);
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
// TWO OVERRIDES ON ONE LINE are two findings, at two columns. A rule reporting once per line
|
|
673
|
+
// would silently under-count a compacted preamble.
|
|
674
|
+
{
|
|
675
|
+
const got = await fmFindings(
|
|
676
|
+
"two-on-one-line",
|
|
677
|
+
acm("\\setcopyright{none}\\pagestyle{plain}"),
|
|
678
|
+
);
|
|
679
|
+
assert.deepEqual(
|
|
680
|
+
got.map((f) => [f.line, f.column, f.text]),
|
|
681
|
+
[
|
|
682
|
+
[2, 1, "\\setcopyright{none}"],
|
|
683
|
+
[2, 20, "\\pagestyle{plain}"],
|
|
684
|
+
],
|
|
685
|
+
"two overrides on one line must produce two findings at two columns",
|
|
686
|
+
);
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
// 🔴 AND TWO MATCHES OF THE **SAME** PATTERN ON ONE LINE, which the case above cannot reach:
|
|
690
|
+
// there each finding comes from a different entry of the table, so a rule that took only the
|
|
691
|
+
// FIRST match per pattern would still produce two. `\pagestyle` and `\thispagestyle` are one
|
|
692
|
+
// regex, so this is the only input that distinguishes `while (re.exec(...))` from `if`.
|
|
693
|
+
{
|
|
694
|
+
const got = await fmFindings(
|
|
695
|
+
"same-pattern-twice",
|
|
696
|
+
acm("\\pagestyle{plain}\\thispagestyle{empty}"),
|
|
697
|
+
);
|
|
698
|
+
assert.deepEqual(
|
|
699
|
+
got.map((f) => [f.column, f.text]),
|
|
700
|
+
[
|
|
701
|
+
[1, "\\pagestyle{plain}"],
|
|
702
|
+
[18, "\\thispagestyle{empty}"],
|
|
703
|
+
],
|
|
704
|
+
"two matches of the SAME pattern on one line must both be reported — the rule stopped at " +
|
|
705
|
+
"the first match per pattern",
|
|
706
|
+
);
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
// A FILE THE RULE CANNOT READ MUST BE SILENT, NOT THROWN. The rule reads from disk because the
|
|
710
|
+
// language's projection blanks the preamble; the price is that a run with no real file behind
|
|
711
|
+
// it (stdin, a deleted path) has nothing to judge, and judging nothing must not crash the lint.
|
|
712
|
+
{
|
|
713
|
+
const dir = join(TMP, "unreadable");
|
|
714
|
+
mkdirSync(dir, { recursive: true });
|
|
715
|
+
const file = join(dir, "paper.tex");
|
|
716
|
+
writeFileSync(file, acm("\\setcopyright{none}"));
|
|
717
|
+
const eslint = makeEslintFm(TMP);
|
|
718
|
+
const before = (await lintTex(eslint, file)).length;
|
|
719
|
+
assert.equal(
|
|
720
|
+
before,
|
|
721
|
+
1,
|
|
722
|
+
"setup failed: the file should fire before it is removed",
|
|
723
|
+
);
|
|
724
|
+
rmSync(file);
|
|
725
|
+
const [res] = await eslint.lintText(acm("\\setcopyright{none}"), {
|
|
726
|
+
filePath: file,
|
|
727
|
+
});
|
|
728
|
+
assert.deepEqual(
|
|
729
|
+
res.messages.filter((m) => m.fatal),
|
|
730
|
+
[],
|
|
731
|
+
"a file that cannot be read made the rule throw — an unreadable path must be silent, not fatal",
|
|
732
|
+
);
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
// ── VI.V CONFIG — the rule is on, at the measured severity ───────────────────
|
|
736
|
+
{
|
|
737
|
+
const config = (await import(join(ROOT, "eslint.config.mjs"))).default;
|
|
738
|
+
const tex = config.find((b) => b.rules?.["tex/acm-frontmatter-override"]);
|
|
739
|
+
assert.ok(
|
|
740
|
+
tex,
|
|
741
|
+
"eslint.config.mjs declares no block carrying `tex/acm-frontmatter-override` — the rule is " +
|
|
742
|
+
"correct and unreferenced, which lints exactly like a rule that passed",
|
|
743
|
+
);
|
|
744
|
+
assert.equal(
|
|
745
|
+
tex.rules["tex/acm-frontmatter-override"],
|
|
746
|
+
"warn",
|
|
747
|
+
"the severity of `tex/acm-frontmatter-override` changed IN THIS REPOSITORY. `warn` here is " +
|
|
748
|
+
"not a statement about confidence — the finding is binary and has a named exemption. It " +
|
|
749
|
+
"is a statement about the corpus: this repository lints fixtures that are broken by " +
|
|
750
|
+
"construction, so `error` would only mean `npx eslint .` exits non-zero on a healthy " +
|
|
751
|
+
"checkout. A consumer linting a real paper sets `error`",
|
|
752
|
+
);
|
|
753
|
+
}
|