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,291 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Both halves for `paper/typography`, plus the halves the RATCHET needs — which are the ones a
|
|
3
|
+
* naive test forgets. A rule that only ever gets asserted "fires on the defective fixture" says
|
|
4
|
+
* nothing about the mechanism that actually decides whether a human sees it.
|
|
5
|
+
*
|
|
6
|
+
* Four sub-checks, and every one of them has a documented way of being silently wrong:
|
|
7
|
+
* sectionSign counted only the literal glyph once and returned ZERO on the paper whose
|
|
8
|
+
* reviewer raised it — the source writes the MACRO form
|
|
9
|
+
* bareDecimal must not eat an arXiv id, where the dot follows a digit
|
|
10
|
+
* figMixed fires only when BOTH spellings appear; either alone is a style, not a defect
|
|
11
|
+
* unreachable must accept url and arXiv id, not only doi — three venues issue no doi at all
|
|
12
|
+
* So each gets a fixture line that would flip it, and the clean fixture carries the near-misses.
|
|
13
|
+
*/
|
|
14
|
+
import assert from "node:assert/strict";
|
|
15
|
+
import { join, dirname } from "node:path";
|
|
16
|
+
import { fileURLToPath } from "node:url";
|
|
17
|
+
import { ESLint } from "eslint";
|
|
18
|
+
import markdown from "@eslint/markdown";
|
|
19
|
+
|
|
20
|
+
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
21
|
+
const ROOT = join(HERE, "..");
|
|
22
|
+
const FIX = join(ROOT, "fixtures", "paper-typography");
|
|
23
|
+
|
|
24
|
+
const { texLanguage } = await import(join(HERE, "latex-language.mjs"));
|
|
25
|
+
const typographyPlugin = (await import(join(HERE, "paper-typography.mjs")))
|
|
26
|
+
.default;
|
|
27
|
+
|
|
28
|
+
let n = 0;
|
|
29
|
+
const check = (label, cond) => {
|
|
30
|
+
assert.ok(cond, label);
|
|
31
|
+
n++;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
// A new rule in this module with no declared severity must not slip through unnoticed.
|
|
35
|
+
assert.deepEqual(
|
|
36
|
+
Object.keys(typographyPlugin.rules).sort(),
|
|
37
|
+
["typography"],
|
|
38
|
+
"the module's rule set changed — update the config and this harness",
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
const lint = async (file, debt) => {
|
|
42
|
+
const eslint = new ESLint({
|
|
43
|
+
cwd: FIX,
|
|
44
|
+
overrideConfigFile: true,
|
|
45
|
+
overrideConfig: [
|
|
46
|
+
{
|
|
47
|
+
files: ["**/*.tex"],
|
|
48
|
+
plugins: {
|
|
49
|
+
tex: { languages: { latex: texLanguage } },
|
|
50
|
+
paper: typographyPlugin,
|
|
51
|
+
},
|
|
52
|
+
language: "tex/latex",
|
|
53
|
+
rules: { "paper/typography": ["warn", { debt }] },
|
|
54
|
+
},
|
|
55
|
+
],
|
|
56
|
+
});
|
|
57
|
+
const [res] = await eslint.lintFiles([file]);
|
|
58
|
+
assert.deepEqual(
|
|
59
|
+
res.messages.filter((m) => m.fatal || !m.ruleId),
|
|
60
|
+
[],
|
|
61
|
+
`${file}: the language failed to parse or the rule threw — a linter crash, not a finding`,
|
|
62
|
+
);
|
|
63
|
+
return res.messages.map((m) => m.message);
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const CLEAN = join(FIX, "clean-paper", "paper.tex");
|
|
67
|
+
const MESSY = join(FIX, "messy-paper", "paper.tex");
|
|
68
|
+
|
|
69
|
+
// ── half one: it FIRES, and on each of the four independently ──────────────────────────────
|
|
70
|
+
const fresh = await lint(MESSY, {});
|
|
71
|
+
check(
|
|
72
|
+
"fires on the section sign",
|
|
73
|
+
fresh.some((m) => m.includes("§")),
|
|
74
|
+
);
|
|
75
|
+
check(
|
|
76
|
+
"fires on the bare decimal",
|
|
77
|
+
fresh.some((m) => m.includes("leading zero")),
|
|
78
|
+
);
|
|
79
|
+
check(
|
|
80
|
+
"fires on mixed Fig./Figure",
|
|
81
|
+
fresh.some((m) => m.includes("mixed in one document")),
|
|
82
|
+
);
|
|
83
|
+
check(
|
|
84
|
+
"fires on unreachable bibliography entries",
|
|
85
|
+
fresh.some((m) => m.includes("bibliography entries")),
|
|
86
|
+
);
|
|
87
|
+
check("four sub-checks, four findings", fresh.length === 4);
|
|
88
|
+
|
|
89
|
+
// 🔴 The MACRO form is counted, not just the glyph. This is the half that was missing when the
|
|
90
|
+
// check first shipped: it returned zero on the very paper the reviewer wrote about.
|
|
91
|
+
const signs = fresh.find((m) => m.includes("§"));
|
|
92
|
+
check(
|
|
93
|
+
"the section sign count includes the macro form, not only the glyph",
|
|
94
|
+
/^3 ×/.test(signs),
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
// ── half two: it STAYS QUIET on clean input, including the near-misses ─────────────────────
|
|
98
|
+
const clean = await lint(CLEAN, {});
|
|
99
|
+
check("silent on a clean paper", clean.length === 0);
|
|
100
|
+
// Each near-miss is in the clean fixture on purpose; naming them keeps the reason alive.
|
|
101
|
+
check(
|
|
102
|
+
"an arXiv id is not a bare decimal",
|
|
103
|
+
!clean.some((m) => m.includes("leading zero")),
|
|
104
|
+
);
|
|
105
|
+
check(
|
|
106
|
+
"`Figure` used consistently is not a defect",
|
|
107
|
+
!clean.some((m) => m.includes("mixed in one document")),
|
|
108
|
+
);
|
|
109
|
+
check(
|
|
110
|
+
"url and arXiv id count as reachable, not only doi",
|
|
111
|
+
!clean.some((m) => m.includes("bibliography entries")),
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
// ── bareDecimal counts what the READER sees, and nothing else ─────────────────────────────
|
|
115
|
+
// The count used to be a regex over the raw source. On a real corpus it found 22 decimals and
|
|
116
|
+
// none was a defect: every one sat in markup (an option, a column spec, a comment, a listing, a
|
|
117
|
+
// tikz coordinate). The cases below pin both directions with EXACT counts — "some finding" would
|
|
118
|
+
// pass a counter that reads markup AND prose, which is precisely the old defect.
|
|
119
|
+
const INLINE = join(FIX, "inline-paper");
|
|
120
|
+
const lintSource = async (text, file) => {
|
|
121
|
+
const eslint = new ESLint({
|
|
122
|
+
cwd: FIX,
|
|
123
|
+
overrideConfigFile: true,
|
|
124
|
+
overrideConfig: [
|
|
125
|
+
{
|
|
126
|
+
files: ["**/*.tex"],
|
|
127
|
+
plugins: {
|
|
128
|
+
tex: { languages: { latex: texLanguage } },
|
|
129
|
+
paper: typographyPlugin,
|
|
130
|
+
},
|
|
131
|
+
language: "tex/latex",
|
|
132
|
+
rules: { "paper/typography": ["warn", { debt: {} }] },
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
files: ["**/*.md"],
|
|
136
|
+
plugins: { markdown, paper: typographyPlugin },
|
|
137
|
+
language: "markdown/gfm",
|
|
138
|
+
languageOptions: { frontmatter: "yaml" },
|
|
139
|
+
rules: { "paper/typography": ["warn", { debt: {} }] },
|
|
140
|
+
},
|
|
141
|
+
],
|
|
142
|
+
});
|
|
143
|
+
const [res] = await eslint.lintText(text, { filePath: join(INLINE, file) });
|
|
144
|
+
assert.deepEqual(
|
|
145
|
+
res.messages.filter((m) => m.fatal || !m.ruleId),
|
|
146
|
+
[],
|
|
147
|
+
`${file}: the language failed to parse or the rule threw on ${JSON.stringify(text)}`,
|
|
148
|
+
);
|
|
149
|
+
return res.messages.map((m) => m.message);
|
|
150
|
+
};
|
|
151
|
+
/** The bare-decimal count for one source, 0 when the rule is silent about it. */
|
|
152
|
+
const bare = async (text, file = "paper.tex") => {
|
|
153
|
+
const hit = (await lintSource(text, file)).find((m) =>
|
|
154
|
+
m.includes("leading zero"),
|
|
155
|
+
);
|
|
156
|
+
return hit ? Number(/^(\d+) ×/.exec(hit)[1]) : 0;
|
|
157
|
+
};
|
|
158
|
+
const doc = (body) =>
|
|
159
|
+
`\\documentclass{acmart}\n\\begin{document}\n${body}\n\\end{document}\n`;
|
|
160
|
+
|
|
161
|
+
// CAUGHT — prose, inline math (where p-values live) and a table cell. One each, exactly.
|
|
162
|
+
const caughtTex = [
|
|
163
|
+
["a p-value in inline math", "We call an effect significant at $p < .05$."],
|
|
164
|
+
["a p-value after other math", "(bugfix $-31\\%$, $p=.002$)"],
|
|
165
|
+
[
|
|
166
|
+
"a table cell",
|
|
167
|
+
"\\begin{tabular}{ll}\ntask & p \\\\\nbugfix & .037 \\\\\n\\end{tabular}",
|
|
168
|
+
],
|
|
169
|
+
["plain prose", "We use a threshold of .05 throughout."],
|
|
170
|
+
];
|
|
171
|
+
for (const [what, body] of caughtTex)
|
|
172
|
+
check(`LaTeX CAUGHT, exactly one: ${what}`, (await bare(doc(body))) === 1);
|
|
173
|
+
check(
|
|
174
|
+
"LaTeX CAUGHT: all four in one document count four",
|
|
175
|
+
(await bare(doc(caughtTex.map(([, b]) => b).join("\n\n")))) === 4,
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
// SILENT — markup the reader never sees as a number. Each alone must count ZERO.
|
|
179
|
+
const silentTex = [
|
|
180
|
+
["a figure width option", "\\includegraphics[width=.48\\columnwidth]{f}"],
|
|
181
|
+
[
|
|
182
|
+
"a tabular column spec",
|
|
183
|
+
"\\begin{tabular}{p{.25\\linewidth}l}\na & b \\\\\n\\end{tabular}",
|
|
184
|
+
],
|
|
185
|
+
["a comment", "Text.\n% p<.01 in the old draft\nMore text."],
|
|
186
|
+
["a listing", "\\begin{lstlisting}\nx = .25\n\\end{lstlisting}"],
|
|
187
|
+
["inline listing", "Call \\lstinline{.25} here."],
|
|
188
|
+
["a length argument", "A\\hspace{.3em}B\\hspace{.35em}C"],
|
|
189
|
+
["a macro definition body", "\\def\\x{.85}"],
|
|
190
|
+
[
|
|
191
|
+
"tikz coordinates",
|
|
192
|
+
"\\begin{tikzpicture}\\draw (.35,.65);\\end{tikzpicture}",
|
|
193
|
+
],
|
|
194
|
+
["an arXiv id", "See arXiv 2310.05736 for details."],
|
|
195
|
+
// unified-latex has no signature for these, so their arguments arrive as sibling nodes.
|
|
196
|
+
[
|
|
197
|
+
"an unsigned environment's width",
|
|
198
|
+
"\\begin{subfigure}[b]{.48\\textwidth}\nPanel.\n\\end{subfigure}",
|
|
199
|
+
],
|
|
200
|
+
[
|
|
201
|
+
"an unsigned table's column spec",
|
|
202
|
+
"\\begin{longtable}{p{.25\\linewidth}l}\na & b \\\\\n\\end{longtable}",
|
|
203
|
+
],
|
|
204
|
+
[
|
|
205
|
+
"an unknown macro's options",
|
|
206
|
+
"\\adjustbox{width=.48\\linewidth}{x} \\foo[scale=.75]{y}",
|
|
207
|
+
],
|
|
208
|
+
];
|
|
209
|
+
for (const [what, body] of silentTex)
|
|
210
|
+
check(`LaTeX SILENT: ${what}`, (await bare(doc(body))) === 0);
|
|
211
|
+
check(
|
|
212
|
+
"LaTeX SILENT: the preamble and the inline bibliography are not the reader's text",
|
|
213
|
+
(await bare(
|
|
214
|
+
"\\documentclass{acmart}\n\\renewcommand{\\arraystretch}{.85}\n" +
|
|
215
|
+
"\\begin{filecontents*}{refs.bib}\n@misc{k, note = {p = .05}, url = {https://x.org}}\n" +
|
|
216
|
+
"\\end{filecontents*}\n\\begin{document}\nNothing here.\n\\end{document}\n",
|
|
217
|
+
)) === 0,
|
|
218
|
+
);
|
|
219
|
+
check(
|
|
220
|
+
"LaTeX: every silent case plus one real p-value counts exactly one",
|
|
221
|
+
(await bare(
|
|
222
|
+
doc(
|
|
223
|
+
silentTex.map(([, b]) => b).join("\n\n") +
|
|
224
|
+
"\n\nThe effect held at $p=.002$.",
|
|
225
|
+
),
|
|
226
|
+
)) === 1,
|
|
227
|
+
);
|
|
228
|
+
|
|
229
|
+
check(
|
|
230
|
+
"LaTeX CAUGHT: the body of an unsigned environment is still read",
|
|
231
|
+
(await bare(
|
|
232
|
+
doc(
|
|
233
|
+
"\\begin{subfigure}{.48\\textwidth}\nHeld at $p=.05$.\n\\end{subfigure}",
|
|
234
|
+
),
|
|
235
|
+
)) === 1,
|
|
236
|
+
);
|
|
237
|
+
|
|
238
|
+
// Markdown, on the markdown AST: text nodes including table cells; code, html and front matter
|
|
239
|
+
// are not prose.
|
|
240
|
+
check(
|
|
241
|
+
"Markdown CAUGHT: a p-value in prose and one in a table cell count two",
|
|
242
|
+
(await bare(
|
|
243
|
+
"We call it significant at p<.05.\n\n| task | p |\n| --- | --- |\n| bugfix | .002 |\n",
|
|
244
|
+
"paper.md",
|
|
245
|
+
)) === 2,
|
|
246
|
+
);
|
|
247
|
+
check(
|
|
248
|
+
"Markdown SILENT: fenced code, inline code, html and front matter count zero",
|
|
249
|
+
(await bare(
|
|
250
|
+
"---\nthreshold: .05\n---\n\nRun `.25` here.\n\n```py\nx = .25\n```\n\n<!-- p<.01 -->\n",
|
|
251
|
+
"paper.md",
|
|
252
|
+
)) === 0,
|
|
253
|
+
);
|
|
254
|
+
|
|
255
|
+
// ── the RATCHET: three halves, because this is what decides if a human ever sees it ────────
|
|
256
|
+
const KEY = "messy-paper";
|
|
257
|
+
|
|
258
|
+
// (a) debt equal to the count → silence. Old sin, already recorded.
|
|
259
|
+
const paid = await lint(MESSY, {
|
|
260
|
+
[KEY]: { sectionSign: 3, bareDecimal: 2, figMixed: 1, unreachable: 2 },
|
|
261
|
+
});
|
|
262
|
+
check("known debt, unchanged, is silent", paid.length === 0);
|
|
263
|
+
|
|
264
|
+
// (b) debt ABOVE the count → still silent, and it does not re-baseline noisily. Lowering is free.
|
|
265
|
+
const lowered = await lint(MESSY, {
|
|
266
|
+
[KEY]: { sectionSign: 99, bareDecimal: 99, figMixed: 99, unreachable: 99 },
|
|
267
|
+
});
|
|
268
|
+
check("paying debt down is silent", lowered.length === 0);
|
|
269
|
+
|
|
270
|
+
// (c) debt BELOW the count → speaks, and says both numbers. A bare "3 ×" would not tell a
|
|
271
|
+
// reader whether anything changed, which is the entire point of the ratchet.
|
|
272
|
+
const grew = await lint(MESSY, {
|
|
273
|
+
[KEY]: { sectionSign: 1, bareDecimal: 99, figMixed: 99, unreachable: 99 },
|
|
274
|
+
});
|
|
275
|
+
check("growth over known debt is reported", grew.length === 1);
|
|
276
|
+
check("growth names the before and after", grew[0].includes("was 1, now 3"));
|
|
277
|
+
|
|
278
|
+
// (d) debt for ANOTHER paper must not silence this one — the key is the paper's own directory.
|
|
279
|
+
const wrongKey = await lint(MESSY, {
|
|
280
|
+
"clean-paper": {
|
|
281
|
+
sectionSign: 99,
|
|
282
|
+
bareDecimal: 99,
|
|
283
|
+
figMixed: 99,
|
|
284
|
+
unreachable: 99,
|
|
285
|
+
},
|
|
286
|
+
});
|
|
287
|
+
check("debt is keyed per paper, not shared", wrongKey.length === 4);
|
|
288
|
+
|
|
289
|
+
console.log(
|
|
290
|
+
`✓ ${String(n)} assertions passed — paper/typography, both halves and the ratchet`,
|
|
291
|
+
);
|
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `paper/typography` — four MECHANICAL conventions a reviewer already complained about,
|
|
3
|
+
* counted against a declared debt so that a legacy paper does not drown the new finding.
|
|
4
|
+
*
|
|
5
|
+
* ── PROVENANCE: every one is a real review finding, not an invented style ───────────────
|
|
6
|
+
* From HotCRP #20 (2026-08-23). Reviewer B listed three as literal to-dos, Reviewer A the
|
|
7
|
+
* fourth:
|
|
8
|
+
* B#11 "§ -> Section" one paper in the source corpus carried 246
|
|
9
|
+
* B#12 ".05 -> 0.05" four in the submitted text, fixed by hand
|
|
10
|
+
* A#2 "not every entry has a DOI/link" one paper: 0 of 47 entries reachable
|
|
11
|
+
* (own) `Fig.` and `Figure` side by side
|
|
12
|
+
*
|
|
13
|
+
* ── RATCHET, and it is the load-bearing part ───────────────────────────────────────────
|
|
14
|
+
* 🔴 A check that reports 246 findings on its first run is a check that gets muted, and the
|
|
15
|
+
* next REAL instance then hides among the old ones. The consumer's repo has already killed a
|
|
16
|
+
* check exactly that way. So the rule takes a `debt` option — what each paper owes today —
|
|
17
|
+
* and speaks only when a paper is ABSENT from it (a new paper owes nothing) or when its count
|
|
18
|
+
* GREW. Lowering a count is free and silently re-baselines.
|
|
19
|
+
*
|
|
20
|
+
* The debt itself is DATA about one corpus, so it does not live here: the consumer passes it
|
|
21
|
+
* in options. The mechanism is general, the numbers are not.
|
|
22
|
+
*
|
|
23
|
+
* ── WHY THREE COUNTS READ `sourceCode.raw` AND NOT THE PROSE PROJECTION ────────────────
|
|
24
|
+
* `sectionSign`, `figMixed` and `unreachable` are about MARKUP. The projection blanks macros —
|
|
25
|
+
* that is its purpose, it is what lets thirty prose rules read LaTeX without knowing LaTeX — so
|
|
26
|
+
* `\S\ref{}` and `Fig.~\ref{}` reach a normal rule as spaces. Offsets coincide between the
|
|
27
|
+
* two (blanking preserves length), so scanning `raw` and reporting the offset found there
|
|
28
|
+
* still points at the right byte.
|
|
29
|
+
*
|
|
30
|
+
* ── WHY `bareDecimal` READS NEITHER, BUT THE PARSED TREE (rpp#44) ─────────────────────────
|
|
31
|
+
* Origin. On a workshop paper (HotCRP #20) Reviewer B wrote: `Numbers should be completed:
|
|
32
|
+
* e.g. ".05" -> 0.05`. The submitted PDF really carried `p < .05`, `p=.002`, `p=.006` and
|
|
33
|
+
* `p=.037` in its text (measured with pdftotext on the submitted version); the camera-ready
|
|
34
|
+
* fixed them by hand.
|
|
35
|
+
*
|
|
36
|
+
* The convention. ISO 80000-1 ("the decimal sign shall be preceded by a zero" for magnitudes
|
|
37
|
+
* below 1) and the IEEE Editorial Style Manual ("0.25, not .25") require the zero. Other styles
|
|
38
|
+
* (APA, AMA) drop it for p-values. The rule measures against IEEE/ISO, because that is what the
|
|
39
|
+
* venues this package targets expect.
|
|
40
|
+
*
|
|
41
|
+
* Why parsed, not raw. The count used to be one regex over the raw source. An independent
|
|
42
|
+
* review ran it on every real input available: 22 findings on the current corpus, 0 true
|
|
43
|
+
* positives. That is survivorship — the real cases had been fixed by hand — and synthetic LaTeX
|
|
44
|
+
* showed where the 22 come from: the regex fires on `[width=.48\columnwidth]`,
|
|
45
|
+
* `p{.25\linewidth}`, comments, code listings and tikz coordinates. The projection is no
|
|
46
|
+
* answer either: it blanks math (where p-values live) and whole tables (where they are
|
|
47
|
+
* reported). So this count walks the TREE and keeps only what the reader sees as a number:
|
|
48
|
+
* LaTeX prose, inline and display math, table cells, and the text arguments of a known
|
|
49
|
+
* set of text macros (`\emph`, `\caption`, `\footnote`, headings…). Not: any other
|
|
50
|
+
* macro's arguments, environment arguments (column specs, widths), comments,
|
|
51
|
+
* verbatim/listings/`\lstinline`, tikz, the preamble, the inline bibliography.
|
|
52
|
+
* Markdown text nodes, table cells included. Not: code, inline code, html, front matter.
|
|
53
|
+
* Only then is the lexeme matched, one run of visible text at a time. The arXiv lookbehind
|
|
54
|
+
* stays: in `2310.05736` the dot follows a digit.
|
|
55
|
+
*
|
|
56
|
+
* What still gets through. A decimal inside a quotation, or a paragraph number such as
|
|
57
|
+
* `¶¶.42`, is prose to the parser and is counted. The per-paper debt below absorbs known cases.
|
|
58
|
+
* And the other direction, chosen on purpose: a text macro outside the known set (`\hl{.05}`, an
|
|
59
|
+
* author's `\pval{.05}`) is read as a parameter and NOT counted — for a warning that people
|
|
60
|
+
* learn to mute, a miss costs less than a finding on markup.
|
|
61
|
+
*/
|
|
62
|
+
import { relative, dirname } from "node:path";
|
|
63
|
+
import { getParser } from "@unified-latex/unified-latex-util-parse";
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* The inline bibliography is counted differently from the prose, and in this corpus it lives
|
|
67
|
+
* INSIDE the `.tex` in a `filecontents` environment rather than in a separate `.bib`.
|
|
68
|
+
*/
|
|
69
|
+
function splitPaperText(text) {
|
|
70
|
+
const m = text.match(
|
|
71
|
+
/\\begin\{filecontents\*?\}(?:\[[^\]]*\])?\{[^}]*\.bib\}\r?\n([\s\S]*?)\\end\{filecontents\*?\}/,
|
|
72
|
+
);
|
|
73
|
+
return m
|
|
74
|
+
? { body: text.replace(m[0], ""), bib: m[1] }
|
|
75
|
+
: { body: text, bib: "" };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function typographyCounts(text, bareDecimal) {
|
|
79
|
+
const { body, bib } = splitPaperText(text);
|
|
80
|
+
|
|
81
|
+
// 🔴 BOTH forms, and the second one is why this check was ever wrong. The first version
|
|
82
|
+
// counted only the literal `§` and returned ZERO on the very paper whose reviewer raised it:
|
|
83
|
+
// the source writes `\S\ref{sec:threats}`, which RENDERS as `§5`. Measured on the held-out
|
|
84
|
+
// submitted build: source hits 0, rendered PDF hits 10. A source-level check calibrated
|
|
85
|
+
// against a rendered complaint reports clean on the exact defect it was written for.
|
|
86
|
+
const sectionSign =
|
|
87
|
+
(body.match(/§/g) || []).length +
|
|
88
|
+
(body.match(/\\S(?=\s*\\ref|~\\ref|\d)/g) || []).length;
|
|
89
|
+
|
|
90
|
+
// Mixed `Fig.~\ref` and `Figure~\ref` in ONE document. Consistent use of either is fine, so
|
|
91
|
+
// this counts only when both appear; an absolute rule here would be taste, not a defect.
|
|
92
|
+
const figShort = (body.match(/\bFig\.~?\\(?:ref|autoref)/g) || []).length;
|
|
93
|
+
const figLong = (body.match(/\bFigure~?\\(?:ref|autoref)/g) || []).length;
|
|
94
|
+
const figMixed =
|
|
95
|
+
figShort > 0 && figLong > 0 ? Math.min(figShort, figLong) : 0;
|
|
96
|
+
|
|
97
|
+
// A bibliography entry a reader cannot follow: no doi, no url, no arXiv id.
|
|
98
|
+
// ⚠️ NOT "no doi". Measured 2026-08-24: ICLR/NeurIPS/TMLR issue no DOIs at all, so a
|
|
99
|
+
// doi-only rule would demand something that does not exist and get muted for lying.
|
|
100
|
+
let unreachable = 0;
|
|
101
|
+
for (const e of bib.split(/^@/m).slice(1)) {
|
|
102
|
+
if (!/\b(doi|url)\s*=/.test(e) && !/arxiv[:\s]*\d{4}\.\d{4,5}/i.test(e))
|
|
103
|
+
unreachable++;
|
|
104
|
+
}
|
|
105
|
+
return { sectionSign, bareDecimal, figMixed, unreachable };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// ── bareDecimal: the text a READER sees, taken from the tree ─────────────────────────────
|
|
109
|
+
|
|
110
|
+
// The lexeme, matched inside ONE run of visible text — never over the source. The lookbehind
|
|
111
|
+
// keeps arXiv ids (2310.05736) out: there the dot follows a digit.
|
|
112
|
+
const BARE_DECIMAL = /(?<![\d.\w])\.\d{2,}\b/g;
|
|
113
|
+
const countBareDecimals = (runs) =>
|
|
114
|
+
runs.reduce((n, run) => n + (run.match(BARE_DECIMAL) || []).length, 0);
|
|
115
|
+
|
|
116
|
+
// Environments whose body is not typeset as text: code, drawings, the inline bibliography.
|
|
117
|
+
const TEX_HIDDEN_ENV =
|
|
118
|
+
/^(verbatim|Verbatim|lstlisting|minted|tikzpicture|filecontents\*?|comment)$/;
|
|
119
|
+
// Text macros whose mandatory argument IS prose. Every other macro's arguments are parameters
|
|
120
|
+
// (`\includegraphics[width=…]`, `\hspace{…}`, `\label{…}`) and are not read.
|
|
121
|
+
const TEX_PROSE_ARG =
|
|
122
|
+
/^(emph|textbf|textit|textsl|textsc|textup|textmd|textrm|textsf|textnormal|underline|text|mbox|caption|footnote|footnotetext|thanks|title|textsuperscript|textsubscript|part|chapter|section|subsection|subsubsection|paragraph|subparagraph)$/;
|
|
123
|
+
// Macros whose LAST mandatory argument is what the reader sees: a table cell, coloured text,
|
|
124
|
+
// a link's text.
|
|
125
|
+
const TEX_LAST_ARG = /^(multicolumn|multirow|textcolor|href)$/;
|
|
126
|
+
// In math every macro argument is typeset (`\frac{.05}{2}`, `\sqrt{…}`) except these.
|
|
127
|
+
const TEX_MATH_HIDDEN =
|
|
128
|
+
/^(label|ref|eqref|autoref|cite|hspace|vspace|hskip|vskip|kern|mkern|mskip|rule|phantom|hphantom|vphantom|color|raisebox|includegraphics)$/;
|
|
129
|
+
|
|
130
|
+
const mandatory = (macro) =>
|
|
131
|
+
(macro.args || []).filter((a) => a.openMark === "{");
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* unified-latex attaches arguments only to what it has a signature for. For the rest — `\def\x`,
|
|
135
|
+
* an author's `\foo[width=.48]{…}`, `\begin{subfigure}{.48\textwidth}`, the column spec of
|
|
136
|
+
* `longtable` — the arguments come back as the NEXT SIBLINGS: an optional `[…]` as bare strings,
|
|
137
|
+
* then groups. They are parameters, not prose. Returns the index of the last sibling to skip,
|
|
138
|
+
* starting after `i`; nothing is skipped past whitespace, and an unclosed `[` skips nothing.
|
|
139
|
+
*/
|
|
140
|
+
function unparsedArgsEnd(nodes, i) {
|
|
141
|
+
let j = i + 1;
|
|
142
|
+
if (nodes[j]?.type === "string" && nodes[j].content.startsWith("[")) {
|
|
143
|
+
let k = j;
|
|
144
|
+
while (
|
|
145
|
+
k < nodes.length &&
|
|
146
|
+
!(nodes[k].type === "string" && nodes[k].content.endsWith("]"))
|
|
147
|
+
)
|
|
148
|
+
k++;
|
|
149
|
+
if (k < nodes.length) j = k + 1;
|
|
150
|
+
}
|
|
151
|
+
while (nodes[j]?.type === "group") j++;
|
|
152
|
+
return j - 1;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Collect runs of visible text from a unified-latex node list. A run is a stretch of sibling
|
|
157
|
+
* `string` nodes with nothing between them: math splits `.05` into `.`, `0`, `5`, and prose
|
|
158
|
+
* keeps `2310.05736` whole — both come back as one word. Anything else ends the run.
|
|
159
|
+
*/
|
|
160
|
+
function texRuns(nodes, math, out) {
|
|
161
|
+
let run = "";
|
|
162
|
+
const flush = () => {
|
|
163
|
+
if (run) out.push(run);
|
|
164
|
+
run = "";
|
|
165
|
+
};
|
|
166
|
+
for (let i = 0; i < (nodes || []).length; i++) {
|
|
167
|
+
const n = nodes[i];
|
|
168
|
+
if (n.type === "string") {
|
|
169
|
+
run += n.content;
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
flush();
|
|
173
|
+
if (
|
|
174
|
+
n.type === "inlinemath" ||
|
|
175
|
+
n.type === "displaymath" ||
|
|
176
|
+
n.type === "mathenv"
|
|
177
|
+
)
|
|
178
|
+
texRuns(n.content, true, out);
|
|
179
|
+
else if (n.type === "environment") {
|
|
180
|
+
const env = typeof n.env === "string" ? n.env : "";
|
|
181
|
+
// The body only: an environment's own arguments are a column spec, a width, a placement —
|
|
182
|
+
// and when unified-latex has no signature for it, they lead the body as bare siblings.
|
|
183
|
+
if (!TEX_HIDDEN_ENV.test(env)) {
|
|
184
|
+
const body = n.content || [];
|
|
185
|
+
const from = (n.args || []).length ? 0 : unparsedArgsEnd(body, -1) + 1;
|
|
186
|
+
texRuns(body.slice(from), math, out);
|
|
187
|
+
}
|
|
188
|
+
} else if (n.type === "group") {
|
|
189
|
+
texRuns(n.content, math, out);
|
|
190
|
+
} else if (n.type === "macro") {
|
|
191
|
+
const args = mandatory(n);
|
|
192
|
+
if (math) {
|
|
193
|
+
if (!TEX_MATH_HIDDEN.test(n.content))
|
|
194
|
+
for (const a of args) texRuns(a.content, true, out);
|
|
195
|
+
} else if (TEX_PROSE_ARG.test(n.content)) {
|
|
196
|
+
for (const a of args) texRuns(a.content, false, out);
|
|
197
|
+
} else if (TEX_LAST_ARG.test(n.content) && args.length) {
|
|
198
|
+
texRuns(args[args.length - 1].content, false, out);
|
|
199
|
+
}
|
|
200
|
+
// In text, a macro with no parsed arguments may still have them, as siblings.
|
|
201
|
+
if (!math && !(n.args || []).length) i = unparsedArgsEnd(nodes, i);
|
|
202
|
+
}
|
|
203
|
+
// comment, verb, verbatim, whitespace, parbreak: not the reader's number, or not a word
|
|
204
|
+
}
|
|
205
|
+
flush();
|
|
206
|
+
return out;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function texVisibleRuns(raw) {
|
|
210
|
+
const root = getParser().parse(raw).content;
|
|
211
|
+
// Only the document body is typeset. A fragment with no `document` environment is all body.
|
|
212
|
+
const document = root.find(
|
|
213
|
+
(n) => n.type === "environment" && n.env === "document",
|
|
214
|
+
);
|
|
215
|
+
return texRuns(document ? document.content : root, false, []);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Markdown: the ESLint markdown language already hands over the mdast. Text nodes are what the
|
|
219
|
+
// reader sees, table cells included; these node types are not prose.
|
|
220
|
+
const MD_HIDDEN = new Set([
|
|
221
|
+
"code",
|
|
222
|
+
"inlineCode",
|
|
223
|
+
"html",
|
|
224
|
+
"yaml",
|
|
225
|
+
"toml",
|
|
226
|
+
"math",
|
|
227
|
+
"inlineMath",
|
|
228
|
+
]);
|
|
229
|
+
function mdVisibleRuns(node, out = []) {
|
|
230
|
+
if (!node || MD_HIDDEN.has(node.type)) return out;
|
|
231
|
+
if (node.type === "text") out.push(node.value);
|
|
232
|
+
for (const c of node.children || []) mdVisibleRuns(c, out);
|
|
233
|
+
return out;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/** The TexSourceCode of this package carries `raw`; anything else is read through its AST. */
|
|
237
|
+
const visibleRuns = (sourceCode) =>
|
|
238
|
+
typeof sourceCode.raw === "string"
|
|
239
|
+
? texVisibleRuns(sourceCode.raw)
|
|
240
|
+
: mdVisibleRuns(sourceCode.ast);
|
|
241
|
+
|
|
242
|
+
const LABEL = {
|
|
243
|
+
sectionSign: "`§` instead of «Section» (reviewer B)",
|
|
244
|
+
bareDecimal:
|
|
245
|
+
"a decimal without a leading zero, `.05` instead of `0.05` (IEEE / ISO 80000-1 style)",
|
|
246
|
+
figMixed: "`Fig.` and `Figure` mixed in one document",
|
|
247
|
+
unreachable:
|
|
248
|
+
"bibliography entries with no doi/url/arXiv id — a reader has nothing to follow (reviewer A)",
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
export default {
|
|
252
|
+
rules: {
|
|
253
|
+
typography: {
|
|
254
|
+
meta: {
|
|
255
|
+
type: "suggestion",
|
|
256
|
+
docs: {
|
|
257
|
+
description:
|
|
258
|
+
"mechanical conventions a reviewer already raised, counted against a declared debt: silent on what was already there, loud on what grew",
|
|
259
|
+
},
|
|
260
|
+
schema: [
|
|
261
|
+
{
|
|
262
|
+
type: "object",
|
|
263
|
+
properties: {
|
|
264
|
+
// { "<path to the paper directory from repo root>": { sectionSign: 54, unreachable: 18 } }
|
|
265
|
+
debt: { type: "object", additionalProperties: true },
|
|
266
|
+
},
|
|
267
|
+
additionalProperties: false,
|
|
268
|
+
},
|
|
269
|
+
],
|
|
270
|
+
messages: {
|
|
271
|
+
grew: "{{n}} × {{label}}{{grew}}. Paying the debt down is silent; growth is reported",
|
|
272
|
+
},
|
|
273
|
+
},
|
|
274
|
+
create(context) {
|
|
275
|
+
const debt = context.options[0]?.debt ?? {};
|
|
276
|
+
return {
|
|
277
|
+
"root:exit"(node) {
|
|
278
|
+
// `raw` on the LaTeX language, `text` everywhere else. A paper in this corpus may be
|
|
279
|
+
// written in markdown rather than LaTeX, and three of the four counts apply there
|
|
280
|
+
// unchanged; only the macro spellings never match, which is correct rather than a
|
|
281
|
+
// gap. Measured 2026-09-16: the check this rule replaces silently covered a markdown
|
|
282
|
+
// paper carrying 246 section signs, so a `.tex`-only rule would have LOST it.
|
|
283
|
+
const raw = context.sourceCode.raw ?? context.sourceCode.text;
|
|
284
|
+
if (typeof raw !== "string") return;
|
|
285
|
+
const bareDecimal = countBareDecimals(
|
|
286
|
+
visibleRuns(context.sourceCode),
|
|
287
|
+
);
|
|
288
|
+
const counts = typographyCounts(raw, bareDecimal);
|
|
289
|
+
const key = relative(context.cwd, dirname(context.filename));
|
|
290
|
+
const owed = debt[key] ?? {};
|
|
291
|
+
for (const [field, n] of Object.entries(counts)) {
|
|
292
|
+
if (n === 0) continue;
|
|
293
|
+
const before = owed[field] ?? 0;
|
|
294
|
+
if (n <= before) continue; // known debt, unchanged or paid down
|
|
295
|
+
context.report({
|
|
296
|
+
node,
|
|
297
|
+
messageId: "grew",
|
|
298
|
+
data: {
|
|
299
|
+
n: String(n),
|
|
300
|
+
label: LABEL[field],
|
|
301
|
+
grew:
|
|
302
|
+
before > 0
|
|
303
|
+
? ` (was ${String(before)}, now ${String(n)})`
|
|
304
|
+
: "",
|
|
305
|
+
},
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
},
|
|
309
|
+
};
|
|
310
|
+
},
|
|
311
|
+
},
|
|
312
|
+
},
|
|
313
|
+
};
|