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,196 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mutation battery for the LaTeX language `eslint-rules/latex-language.mjs`.
|
|
3
|
+
* Run: `node eslint-rules/latex-language.mutations.mjs` (not `vigiles test` β not a harness).
|
|
4
|
+
*
|
|
5
|
+
* π΄ EVERY MUTATION CARRIES A "THE PATCH LANDED" ASSERTION: the file is re-read from disk, the
|
|
6
|
+
* mutant must be in it, the original text must be GONE (otherwise the target was not unique),
|
|
7
|
+
* and after the rollback the contents must return byte for byte. In the repository this came
|
|
8
|
+
* from, a green mutation has three times meant not "the defence holds" but "the patch never
|
|
9
|
+
* applied".
|
|
10
|
+
*
|
|
11
|
+
* A GREEN HARNESS UNDER A MUTATION is a finding about the TEST, not a conclusion about the
|
|
12
|
+
* defence. Rank the causes:
|
|
13
|
+
* 1. the wrong test β it pins a side effect rather than the property itself;
|
|
14
|
+
* 2. the patch did not land β removed by the assertion above;
|
|
15
|
+
* 3. the set is too narrow β the property is observable in no fixture;
|
|
16
|
+
* 4. π΄ THE RUN WAS ALREADY RED BEFORE THE MUTATION β then "killed" and "was failing anyway"
|
|
17
|
+
* are indistinguishable.
|
|
18
|
+
* Cause 4 is why the baseline run comes FIRST and why its failure stops the battery.
|
|
19
|
+
*
|
|
20
|
+
* β οΈ WHAT IS NOT HERE ANY MORE. The original battery had a second group of three mutations
|
|
21
|
+
* against the `case "html"` patch in three consumer rule modules β the patch that taught them to
|
|
22
|
+
* recognise a `%` comment, not only an `<!--` one. Those modules were not extracted, so the
|
|
23
|
+
* mutations have no target. The property itself did NOT disappear with them: the language still
|
|
24
|
+
* trims both edges of a comment node, and the two mutations named
|
|
25
|
+
* `comment/RIGHT edge` and `comment/LEFT edge` below cover the language's half of it. What is
|
|
26
|
+
* genuinely gone is the evidence that a CONSUMER acts on it.
|
|
27
|
+
*/
|
|
28
|
+
import { readFileSync, writeFileSync } from "node:fs";
|
|
29
|
+
import { execFileSync } from "node:child_process";
|
|
30
|
+
|
|
31
|
+
const HARNESS = "eslint-rules/latex-language.harness.mjs";
|
|
32
|
+
|
|
33
|
+
// Coverage mode β see `scripts/run-mutations.mjs` guard 2 and the block in
|
|
34
|
+
// `lib/mutation-driver.mjs`. Says what this battery can kill, without touching a file.
|
|
35
|
+
if (process.env.MUTATIONS_REPORT_COVERAGE) {
|
|
36
|
+
console.log(`MUTATION-COVERS\t${HARNESS}`);
|
|
37
|
+
process.exit(0);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const LANG = "eslint-rules/latex-language.mjs";
|
|
41
|
+
const PRISTINE = readFileSync(LANG, "utf8");
|
|
42
|
+
|
|
43
|
+
const M = [
|
|
44
|
+
[
|
|
45
|
+
"synthesis of `## References` from `\\bibliography{}`",
|
|
46
|
+
"stop declaring the bibliography a heading",
|
|
47
|
+
' if (node.content === "bibliography" && pos) {',
|
|
48
|
+
" if (false && pos) { /* MUT */",
|
|
49
|
+
],
|
|
50
|
+
[
|
|
51
|
+
"synthesis of `## Abstract` from the environment",
|
|
52
|
+
"stop declaring the abstract a heading",
|
|
53
|
+
' if (env === "abstract" && pos) {',
|
|
54
|
+
" if (false && pos) { /* MUT */",
|
|
55
|
+
],
|
|
56
|
+
[
|
|
57
|
+
"preamble cut-off during synthesis",
|
|
58
|
+
"treat preamble constructs (e.g. `\\AtBeginDocument{\\bibliography{}}`) as nodes of the document",
|
|
59
|
+
' if (pos && pos.start.offset < preEnd) {\n for (const k of ["content", "args"]) if (node[k]) walk(node[k]);\n return;\n }',
|
|
60
|
+
" if (false) { return; } /* MUT */",
|
|
61
|
+
],
|
|
62
|
+
[
|
|
63
|
+
"preamble blanking",
|
|
64
|
+
"leave the preamble (package list, author macros) visible as prose",
|
|
65
|
+
" if (preEnd) {\n blank(0, preEnd);",
|
|
66
|
+
" if (preEnd) {\n void 0; /* MUT */",
|
|
67
|
+
],
|
|
68
|
+
[
|
|
69
|
+
"comment/RIGHT edge trim",
|
|
70
|
+
"leave the trailing `\\n` inside the comment node",
|
|
71
|
+
' while (\n ce > pos.start.offset &&\n (src[ce - 1] === "\\n" || src[ce - 1] === "\\r")\n )\n ce--;',
|
|
72
|
+
" /* MUT */",
|
|
73
|
+
],
|
|
74
|
+
[
|
|
75
|
+
"comment/LEFT edge trim",
|
|
76
|
+
"leave the preceding `\\n` inside the comment node",
|
|
77
|
+
' while (cs < ce && src[cs] !== "%") cs++;',
|
|
78
|
+
" /* MUT */",
|
|
79
|
+
],
|
|
80
|
+
[
|
|
81
|
+
"`**` written in place of `\\te`",
|
|
82
|
+
"write the `**` before the content, leaving the line to start with spaces",
|
|
83
|
+
' while (pendingStrong.length) {\n const o = pendingStrong.pop();\n chars[o] = "*";\n chars[o + 1] = "*";\n }',
|
|
84
|
+
" while (pendingStrong.length) { pendingStrong.pop(); } /* MUT */",
|
|
85
|
+
],
|
|
86
|
+
[
|
|
87
|
+
"`\\texttt{X}` β `` `X` ``",
|
|
88
|
+
"stop reproducing the one markdown markup the projection can express",
|
|
89
|
+
' if (node.content === "texttt" || node.content === "lstinline") {',
|
|
90
|
+
" if (false) { /* MUT */",
|
|
91
|
+
],
|
|
92
|
+
[
|
|
93
|
+
"heading projected to ATX",
|
|
94
|
+
"stop writing `#` where `\\section` was",
|
|
95
|
+
' for (let i = 0; i < depth; i++) chars[pos.start.offset + i] = "#";',
|
|
96
|
+
' for (let i = 0; i < 0; i++) chars[pos.start.offset + i] = "#"; /* MUT */',
|
|
97
|
+
],
|
|
98
|
+
[
|
|
99
|
+
"OPAQUE macro blanking",
|
|
100
|
+
"leave `\\cite` / `\\label` / `\\input` as prose",
|
|
101
|
+
" if (OPAQUE.test(node.content)) {\n blank(pos.start.offset, argEnd(node).offset + 1);\n return;\n }",
|
|
102
|
+
" if (false) { return; } /* MUT */",
|
|
103
|
+
],
|
|
104
|
+
[
|
|
105
|
+
"math blanking",
|
|
106
|
+
"leave formulas as prose",
|
|
107
|
+
' if (\n (node.type === "inlinemath" ||\n node.type === "displaymath" ||\n node.type === "verbatim") &&\n pos\n ) {',
|
|
108
|
+
" if (false) { /* MUT */",
|
|
109
|
+
],
|
|
110
|
+
[
|
|
111
|
+
"float prose kept (caption / footnote)",
|
|
112
|
+
"swallow the caption together with the float again β the double lock returns",
|
|
113
|
+
"const FLOAT_PROSE = /^(caption|footnote)$/;",
|
|
114
|
+
"const FLOAT_PROSE = /^(?!)$/; /* MUT */",
|
|
115
|
+
],
|
|
116
|
+
];
|
|
117
|
+
|
|
118
|
+
const runHarness = () => {
|
|
119
|
+
try {
|
|
120
|
+
execFileSync("npx", ["vigiles", "test", HARNESS], {
|
|
121
|
+
encoding: "utf8",
|
|
122
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
123
|
+
});
|
|
124
|
+
return { failed: false, out: "" };
|
|
125
|
+
} catch (e) {
|
|
126
|
+
return { failed: true, out: (e.stdout ?? "") + (e.stderr ?? "") };
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
// π΄ A BASELINE RUN BEFORE ANY MUTATION β see cause 4 in the header.
|
|
131
|
+
{
|
|
132
|
+
const base = runHarness();
|
|
133
|
+
if (base.failed) {
|
|
134
|
+
console.log(
|
|
135
|
+
"β THE HARNESS IS RED BEFORE ANY MUTATION β the battery cannot tell a killed mutation " +
|
|
136
|
+
"from that:\n" +
|
|
137
|
+
base.out.slice(-1500),
|
|
138
|
+
);
|
|
139
|
+
process.exit(1);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
let ok = 0;
|
|
144
|
+
let bad = 0;
|
|
145
|
+
const rows = [];
|
|
146
|
+
for (const [label, what, from, to] of M) {
|
|
147
|
+
const hits = PRISTINE.split(from).length - 1;
|
|
148
|
+
if (hits !== 1) {
|
|
149
|
+
console.log(
|
|
150
|
+
`β ${label}: TARGET ${hits === 0 ? "NOT FOUND" : `NOT UNIQUE (${hits})`} β ${from.slice(0, 70)}`,
|
|
151
|
+
);
|
|
152
|
+
bad++;
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
writeFileSync(LANG, PRISTINE.replace(from, to));
|
|
156
|
+
const on = readFileSync(LANG, "utf8");
|
|
157
|
+
if (!on.includes(to) || on.includes(from)) {
|
|
158
|
+
console.log(
|
|
159
|
+
`β ${label}: THE MUTATION DID NOT LAND (checked by re-reading the file)`,
|
|
160
|
+
);
|
|
161
|
+
bad++;
|
|
162
|
+
writeFileSync(LANG, PRISTINE);
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
const { failed, out } = runHarness();
|
|
166
|
+
writeFileSync(LANG, PRISTINE);
|
|
167
|
+
if (readFileSync(LANG, "utf8") !== PRISTINE) {
|
|
168
|
+
console.log(`β ${label}: THE ROLLBACK FAILED`);
|
|
169
|
+
bad++;
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
const at = (out.match(/latex-language\.harness\.mjs:(\d+)/) ?? [])[1];
|
|
173
|
+
const msg =
|
|
174
|
+
(out.match(/AssertionError[^:]*: ([^\n]+)/) ?? [])[1] ??
|
|
175
|
+
(out.match(/((?:Error|ENOENT)[^\n]*)/) ?? [])[1] ??
|
|
176
|
+
(out.match(
|
|
177
|
+
/([^\n]*(?:must|drifted|was lost|is stale|leaked|swallowed)[^\n]*)/,
|
|
178
|
+
) ?? [])[1] ??
|
|
179
|
+
"";
|
|
180
|
+
const where = (at ? `harness:${at} Β· ` : "") + msg;
|
|
181
|
+
if (failed) {
|
|
182
|
+
ok++;
|
|
183
|
+
rows.push([label, what, where.trim().slice(0, 130)]);
|
|
184
|
+
} else {
|
|
185
|
+
console.log(
|
|
186
|
+
`π΄ ${label}: THE HARNESS IS GREEN UNDER THE MUTATION β a finding about the TEST, not a conclusion about the defence`,
|
|
187
|
+
);
|
|
188
|
+
bad++;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
console.log("\n| property | mutation | what the harness died on |");
|
|
192
|
+
console.log("|---|---|---|");
|
|
193
|
+
for (const [a, b, c] of rows)
|
|
194
|
+
console.log(`| \`${a}\` | ${b} | ${c.replace(/\|/g, "\\|")} |`);
|
|
195
|
+
console.log(`\n${ok} mutation(s) killed, ${bad} problem(s)`);
|
|
196
|
+
process.exit(bad ? 1 : 0);
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Both halves for `paper/research-question`, plus two that a test forgets most often: SCOPE
|
|
3
|
+
* and LANGUAGE.
|
|
4
|
+
*
|
|
5
|
+
* The scope is load-bearing here. The rule does not ask "is there a question", it asks "is there
|
|
6
|
+
* a question FOR SOMETHING THAT HAS ALREADY SHIPPED". Remove the stage gate and the rule starts
|
|
7
|
+
* scolding every draft in the corpus, and gets turned off within a week. So "silent on a draft"
|
|
8
|
+
* is checked by its own assert, not treated as a special case of "silent".
|
|
9
|
+
*
|
|
10
|
+
* Language is the second forgotten half: a paper in this corpus can be either LaTeX or markdown.
|
|
11
|
+
* A rule pinned to `.tex` only would LOSE `compile-rules-2026`, where the paper is written in
|
|
12
|
+
* markdown and states no question β one of the two real findings on the live corpus.
|
|
13
|
+
*/
|
|
14
|
+
import assert from "node:assert/strict";
|
|
15
|
+
import { join, dirname } from "node:path";
|
|
16
|
+
import { readFileSync } from "node:fs";
|
|
17
|
+
import { fileURLToPath } from "node:url";
|
|
18
|
+
import { Linter } from "eslint";
|
|
19
|
+
import markdown from "@eslint/markdown";
|
|
20
|
+
|
|
21
|
+
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
22
|
+
const FIX = join(HERE, "..", "fixtures", "paper-research-question");
|
|
23
|
+
|
|
24
|
+
const { texLanguage } = await import(join(HERE, "latex-language.mjs"));
|
|
25
|
+
const rq = (await import(join(HERE, "paper-research-question.mjs"))).default;
|
|
26
|
+
|
|
27
|
+
let n = 0;
|
|
28
|
+
const check = (label, cond) => {
|
|
29
|
+
assert.ok(cond, label);
|
|
30
|
+
n++;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const linter = new Linter();
|
|
34
|
+
|
|
35
|
+
/** Run against one fixture. The language is picked by extension β exactly like the consumer's config. */
|
|
36
|
+
function lint(dir, file, opts = {}) {
|
|
37
|
+
const path = join(FIX, dir, file);
|
|
38
|
+
const tex = file.endsWith(".tex");
|
|
39
|
+
const cfg = tex
|
|
40
|
+
? {
|
|
41
|
+
files: ["**/*.tex"],
|
|
42
|
+
plugins: { tex: { languages: { latex: texLanguage } }, paper: rq },
|
|
43
|
+
language: "tex/latex",
|
|
44
|
+
}
|
|
45
|
+
: {
|
|
46
|
+
// `files` is required, not cosmetic: without it flat config falls back to JS
|
|
47
|
+
// extensions, and the fixture comes back as "No matching configuration found" β a
|
|
48
|
+
// message that reads exactly like a clean run.
|
|
49
|
+
files: ["**/*.md"],
|
|
50
|
+
plugins: { markdown, paper: rq },
|
|
51
|
+
language: "markdown/gfm",
|
|
52
|
+
languageOptions: { frontmatter: "yaml" },
|
|
53
|
+
};
|
|
54
|
+
const msgs = linter.verify(
|
|
55
|
+
readFileSync(path, "utf-8"),
|
|
56
|
+
[{ ...cfg, rules: { "paper/research-question": ["error", opts] } }],
|
|
57
|
+
path,
|
|
58
|
+
);
|
|
59
|
+
assert.deepEqual(
|
|
60
|
+
msgs.filter((m) => m.fatal),
|
|
61
|
+
[],
|
|
62
|
+
`${dir}/${file}: the rule threw`,
|
|
63
|
+
);
|
|
64
|
+
return msgs.map((m) => m.message);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ββ fires on a planted defect βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
68
|
+
const fires = lint("shipped-no-rq", "paper.tex");
|
|
69
|
+
check("shipped and no question β a finding", fires.length === 1);
|
|
70
|
+
// π΄ The message must name WHERE the requirement comes from. Without this it reads as the
|
|
71
|
+
// linter's taste, but the requirement came from a real venue reviewer.
|
|
72
|
+
check(
|
|
73
|
+
"and it cites the reviewer, rather than passing this off as the linter's taste",
|
|
74
|
+
/reviewer/i.test(fires[0]),
|
|
75
|
+
);
|
|
76
|
+
// The stages in the text come from the FIELD. The predecessor derived them with a regex over
|
|
77
|
+
// the scorecard's prose and on agenticdev printed `submitted` where `submitted, camera-ready`
|
|
78
|
+
// was declared.
|
|
79
|
+
check(
|
|
80
|
+
"the stage list in the message comes from the field and carries BOTH",
|
|
81
|
+
/submitted\/camera-ready/.test(fires[0]),
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
// markdown β the second half of LANGUAGE, without it one of the two real findings is lost
|
|
85
|
+
check(
|
|
86
|
+
"a paper in markdown is checked the same way",
|
|
87
|
+
lint("markdown-no-rq", "paper.md").length === 1,
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
// ββ THE CASE THE PREDECESSOR COULD NOT EXPRESS ββββββββββββββββββββββββββββββββββββββββββ
|
|
91
|
+
// π΄ The point of the 2026-09-19 redesign, in one fixture. The scorecard declares a question,
|
|
92
|
+
// the paper does not carry it β AND the paper contains the words "research question", inside
|
|
93
|
+
// "We leave the research question to future work". The old pattern matched that phrase and went
|
|
94
|
+
// SILENT on a paper stating no question at all. Matching a sentence ABOUT the thing is not
|
|
95
|
+
// finding the thing, and no list of spellings fixes that.
|
|
96
|
+
const drifted = lint("declared-not-in-paper", "paper.tex");
|
|
97
|
+
check(
|
|
98
|
+
"declared but absent from the paper β a finding, where the old pattern was silent",
|
|
99
|
+
drifted.length === 1,
|
|
100
|
+
);
|
|
101
|
+
check(
|
|
102
|
+
"and the message QUOTES what was looked for, so the author can see why it missed",
|
|
103
|
+
/does pruning reduce review cost\?/i.test(drifted[0]),
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
// ββ stays silent where it must ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
107
|
+
check(
|
|
108
|
+
"the question is stated β silent",
|
|
109
|
+
lint("shipped-with-rq", "paper.tex").length === 0,
|
|
110
|
+
);
|
|
111
|
+
// The stage gate: a draft owes nothing, because it never asked anyone to read it.
|
|
112
|
+
check(
|
|
113
|
+
"a draft (no stages) β silent, even though it has no question either",
|
|
114
|
+
lint("draft", "paper.tex").length === 0,
|
|
115
|
+
);
|
|
116
|
+
// And the distinguisher against the previous case: the same draft with a SWAPPED scorecard
|
|
117
|
+
// name is also silent β so the silence there is not because the file went unfound, it's
|
|
118
|
+
// because the stage list is empty.
|
|
119
|
+
check(
|
|
120
|
+
"and with a nonexistent scorecard a shipped paper is also silent β the stage gate is load-bearing",
|
|
121
|
+
lint("shipped-no-rq", "paper.tex", { statusFile: "NO-SUCH-FILE.md" })
|
|
122
|
+
.length === 0,
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
// ββ a named hole, pinned down by an assert, and RESHAPED by the redesign ββββββββββββββββ
|
|
126
|
+
// β οΈ The hole changed form on 2026-09-19 and did not close. Before, ANY mention of "RQ" in a
|
|
127
|
+
// LaTeX comment silenced the rule. Now the rule no longer hunts for spellings, so this fixture
|
|
128
|
+
// β a comment saying "TODO: state the research question", with no `researchQuestion` field β
|
|
129
|
+
// is a FINDING, which it never was before. What survives is narrower: if the DECLARED sentence
|
|
130
|
+
// appears only inside a comment, the raw-text comparison still accepts it. That is a smaller
|
|
131
|
+
// hole with a named owner, and closing it means walking the parsed tree.
|
|
132
|
+
check(
|
|
133
|
+
"a paper whose only mention is a comment, with nothing declared, is now a finding",
|
|
134
|
+
lint("comment-only", "paper.tex").length === 1,
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
// ββ the scorecard's name is consumer data βββββββββββββββββββββββββββββββββββββββββββββββ
|
|
138
|
+
check(
|
|
139
|
+
"the scorecard's name comes in as an option",
|
|
140
|
+
lint("shipped-no-rq", "paper.tex", { statusFile: "PIPELINE-STATUS.md" })
|
|
141
|
+
.length === 1,
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
console.log(
|
|
145
|
+
`β ${String(n)} assertions passed β paper/research-question, the debt of a shipped paper`,
|
|
146
|
+
);
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `paper/research-question` β a shipped paper states its research question EXPLICITLY.
|
|
3
|
+
*
|
|
4
|
+
* Reviewer A's verbatim point on agenticdev (#20), as it was parsed: "State the goal as a
|
|
5
|
+
* research question." This is a venue expectation, not taste: an empirical paper that never
|
|
6
|
+
* names the question it answers forces the reviewer to reconstruct it β and they say so.
|
|
7
|
+
*
|
|
8
|
+
* ββ WHY THE SCOPE IS SHIPPED PAPERS ONLY βββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
9
|
+
* A draft never asked anyone to read it. The scope deliberately matches `paper/author-list`:
|
|
10
|
+
* both are about the DEBT of a shipped paper, and an unshipped one has no debt. Measured on the
|
|
11
|
+
* live corpus before the move:
|
|
12
|
+
* agenticdev-2026 has RQ, shipped -> silent (added because of this very review)
|
|
13
|
+
* aisec-2026 no RQ, shipped -> a finding
|
|
14
|
+
* compile-rules no RQ, shipped -> a finding
|
|
15
|
+
* scored-2026 no RQ, NOT shipped -> silent
|
|
16
|
+
*
|
|
17
|
+
* ββ WHAT THE MOVE CHANGED βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
18
|
+
* The predecessor read the stage with a REGEX OVER THE SCORECARD'S PROSE. Remeasured 2026-09-17:
|
|
19
|
+
* for `agenticdev-2026` the prose reads `submitted`, while the `stages` field declares
|
|
20
|
+
* `submitted, camera-ready`. Here the stage comes from the FIELD β the same one `paper/stages`
|
|
21
|
+
* checks against the bytes in both directions.
|
|
22
|
+
*
|
|
23
|
+
* β οΈ WHAT THE MOVE DOES NOT FIX, AND THIS IS MEASURED, NOT ASSUMED. The rule reads RAW text, not
|
|
24
|
+
* the parsed tree, so a mention of RQ inside a LaTeX comment (`% add an RQ`) will put it to
|
|
25
|
+
* sleep. Measured 2026-09-17 across all four papers: ZERO such cases β not a single match inside
|
|
26
|
+
* a comment. So the hole is LATENT, not observed, and closing it with a tree walk would
|
|
27
|
+
* complicate the rule without a measured difference. The neighboring `paper/typography` is built
|
|
28
|
+
* the same way and for the same reason. Once a real case shows up, there will be something to
|
|
29
|
+
* cite here.
|
|
30
|
+
*
|
|
31
|
+
* The rule is ADVISORY by decision, not by coincidence: a position paper may legitimately have
|
|
32
|
+
* no question. Then its absence is the author's decision, and the rule says exactly that,
|
|
33
|
+
* rather than declaring the absence a defect on its own.
|
|
34
|
+
*/
|
|
35
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
36
|
+
import { join, dirname } from "node:path";
|
|
37
|
+
import { load } from "js-yaml";
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* π΄ THE REGEX IS GONE, AND THE REASON IS NOT "regexes are bad" β IT IS THAT IT ANSWERED THE
|
|
41
|
+
* WRONG QUESTION (2026-09-19).
|
|
42
|
+
*
|
|
43
|
+
* What stood here was `/\\textbf\{RQ|\bRQ[0-9]?\b|research question/i`, defended in this very
|
|
44
|
+
* file with: "a regex is LEGITIMATE here: the subject is human prose, which has no structure by
|
|
45
|
+
* definition, and no parser can tell a question's statement apart from a paragraph about it."
|
|
46
|
+
*
|
|
47
|
+
* The premise is true. The conclusion does not follow. From "the text cannot decide this" the
|
|
48
|
+
* answer is not "so pattern-match harder" β it is "so the HUMAN declares it, and the check
|
|
49
|
+
* compares the declaration against the text." The three spellings were an undeclared schema
|
|
50
|
+
* field: someone added a fact and invented a convention for how it would look in prose, which is
|
|
51
|
+
* the design running backwards.
|
|
52
|
+
*
|
|
53
|
+
* What the pattern actually decided, in both directions:
|
|
54
|
+
* β’ a paper that says "we leave the research question to future work" β MATCHED, and the rule
|
|
55
|
+
* went silent on a paper that states no question at all;
|
|
56
|
+
* β’ a paper whose question is written plainly ("Does pruning reduce review cost?") with no
|
|
57
|
+
* "RQ" anywhere β NOT matched, and the rule reported a paper that does exactly what is asked.
|
|
58
|
+
* An open list of spellings cannot be completed, so both failures are structural, not bugs.
|
|
59
|
+
*
|
|
60
|
+
* ββ WHAT REPLACES IT: ONE UNDECIDABLE QUESTION BECOMES TWO DECIDABLE ONES ββββββββββββββββββ
|
|
61
|
+
* 1. Is the question WRITTEN DOWN? β `researchQuestion` in the scorecard's front matter. A
|
|
62
|
+
* field, parsed by js-yaml, not a phrase hunted for in prose.
|
|
63
|
+
* 2. Does the paper CONTAIN what was written down? β the declared sentence, whitespace
|
|
64
|
+
* collapsed, compared against the source. Bytes, not spelling.
|
|
65
|
+
*
|
|
66
|
+
* This is the shape this package already uses twice, and for the same reason: `bytes`/
|
|
67
|
+
* `sourceBytes` (a recorded number compared against the file on disk) and `paper/author-list`
|
|
68
|
+
* (a recorded marker, because whether a cross-check ran is not visible in the paper). Neither
|
|
69
|
+
* asks the prose a question the prose cannot answer.
|
|
70
|
+
*
|
|
71
|
+
* β οΈ WHAT THIS STILL CANNOT DO, said plainly rather than implied: nothing here decides that the
|
|
72
|
+
* declared sentence IS a research question. A declaration of "banana" passes step 1 and, if the
|
|
73
|
+
* word appears in the paper, step 2. The rule guarantees that the author wrote a question down
|
|
74
|
+
* and that the paper carries it β it does not grade the question. That is a human's job, and
|
|
75
|
+
* pretending otherwise is what the regex was doing.
|
|
76
|
+
*/
|
|
77
|
+
|
|
78
|
+
/** Whitespace is the only thing normalised: a sentence wrapped across lines is the same sentence. */
|
|
79
|
+
const flatten = (t) => t.replace(/\s+/g, " ").trim().toLowerCase();
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Both facts come out of ONE parse of the scorecard's front matter: the stages (has this paper
|
|
83
|
+
* shipped?) and the declared question. Reading them separately would invite the two to be taken
|
|
84
|
+
* from different revisions of the same file.
|
|
85
|
+
*/
|
|
86
|
+
function scorecard(dir, statusName) {
|
|
87
|
+
const p = join(dir, statusName);
|
|
88
|
+
const none = { stages: [], question: "" };
|
|
89
|
+
if (!existsSync(p)) return none;
|
|
90
|
+
let text;
|
|
91
|
+
try {
|
|
92
|
+
text = readFileSync(p, "utf-8");
|
|
93
|
+
} catch {
|
|
94
|
+
return none;
|
|
95
|
+
}
|
|
96
|
+
const m = /^---\n([\s\S]*?)\n---/.exec(text);
|
|
97
|
+
if (!m) return none;
|
|
98
|
+
let data;
|
|
99
|
+
try {
|
|
100
|
+
data = load(m[1]);
|
|
101
|
+
} catch {
|
|
102
|
+
return none; // the unreadable YAML is already reported by `paper/stages`, on its own file
|
|
103
|
+
}
|
|
104
|
+
const raw = data?.stages;
|
|
105
|
+
const stages = Array.isArray(raw)
|
|
106
|
+
? raw.map((r) => r?.stage).filter(Boolean)
|
|
107
|
+
: [];
|
|
108
|
+
const q = data?.researchQuestion;
|
|
109
|
+
return { stages, question: typeof q === "string" ? q : "" };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export default {
|
|
113
|
+
rules: {
|
|
114
|
+
"research-question": {
|
|
115
|
+
meta: {
|
|
116
|
+
type: "suggestion",
|
|
117
|
+
docs: {
|
|
118
|
+
description:
|
|
119
|
+
"a paper that declares a stage states its research question explicitly β otherwise the reviewer reconstructs it themselves, and says so",
|
|
120
|
+
},
|
|
121
|
+
schema: [
|
|
122
|
+
{
|
|
123
|
+
type: "object",
|
|
124
|
+
properties: {
|
|
125
|
+
// The scorecard's name is a CONSUMER convention. The rule is declared on the
|
|
126
|
+
// paper's source, so it has to name its neighbor itself rather than get it via a
|
|
127
|
+
// config glob.
|
|
128
|
+
statusFile: { type: "string" },
|
|
129
|
+
},
|
|
130
|
+
additionalProperties: false,
|
|
131
|
+
},
|
|
132
|
+
],
|
|
133
|
+
messages: {
|
|
134
|
+
notDeclared:
|
|
135
|
+
"the paper shipped (stage Β«{{stages}}Β») and its scorecard declares no `researchQuestion`. This is reviewer A's verbatim point on agenticdev (#20), not the linter's taste. Write the question down as a field, in your own words β the check cannot infer it from the prose, and the pattern that used to try matched Β«we leave the research question to future workΒ» while missing a question stated plainly. Advisory: a position paper may legitimately have none, and then leaving the field out is a DECISION β record it as one",
|
|
136
|
+
notInPaper:
|
|
137
|
+
"the scorecard declares a research question the paper does not contain. Looked for Β«{{needle}}Β» with whitespace collapsed, and the source has no such run of text. Either the paper dropped it or the declaration drifted from what was written β and which of the two it is, only you know",
|
|
138
|
+
},
|
|
139
|
+
},
|
|
140
|
+
create(context) {
|
|
141
|
+
const statusName =
|
|
142
|
+
context.options?.[0]?.statusFile ?? "PIPELINE-STATUS.md";
|
|
143
|
+
return {
|
|
144
|
+
// `root:exit` exists for both markdown and the `tex/latex` language β the same place
|
|
145
|
+
// `paper/typography` lives, and for the same reason: a paper in this corpus can be
|
|
146
|
+
// either.
|
|
147
|
+
"root:exit"(node) {
|
|
148
|
+
const raw = context.sourceCode.raw ?? context.sourceCode.text;
|
|
149
|
+
if (typeof raw !== "string") return;
|
|
150
|
+
const { stages, question } = scorecard(
|
|
151
|
+
dirname(context.filename),
|
|
152
|
+
statusName,
|
|
153
|
+
);
|
|
154
|
+
if (stages.length === 0) return; // not shipped β owes nothing
|
|
155
|
+
|
|
156
|
+
// Step 1 β is it written down? A field, not a phrase hunted for in prose.
|
|
157
|
+
if (question.trim() === "") {
|
|
158
|
+
context.report({
|
|
159
|
+
node,
|
|
160
|
+
messageId: "notDeclared",
|
|
161
|
+
data: { stages: stages.join("/") },
|
|
162
|
+
});
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Step 2 β does the paper carry what was written down? Bytes, not spelling. Only
|
|
167
|
+
// whitespace is normalised, because a sentence wrapped across source lines is the
|
|
168
|
+
// same sentence; everything else stays the author's.
|
|
169
|
+
if (flatten(raw).includes(flatten(question))) return;
|
|
170
|
+
context.report({
|
|
171
|
+
node,
|
|
172
|
+
messageId: "notInPaper",
|
|
173
|
+
data: { needle: flatten(question) },
|
|
174
|
+
});
|
|
175
|
+
},
|
|
176
|
+
};
|
|
177
|
+
},
|
|
178
|
+
},
|
|
179
|
+
},
|
|
180
|
+
};
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Battery for `paper/research-question` β five mutations, each removes ITS OWN load-bearing
|
|
3
|
+
* property.
|
|
4
|
+
*
|
|
5
|
+
* π΄ Why the battery matters here specifically. The rule's success state is SILENCE, and on the
|
|
6
|
+
* live corpus it only produces two findings out of four papers. "It passed" and "it cannot fire"
|
|
7
|
+
* look identical from the outside, and only this tells them apart.
|
|
8
|
+
*
|
|
9
|
+
* Two mutations do not target the finding but the SCOPE and the LANGUAGE β the halves a test
|
|
10
|
+
* forgets. Without the stage gate the rule scolds every draft; without markdown it loses
|
|
11
|
+
* `compile-rules`, i.e. half of the real findings, while staying green.
|
|
12
|
+
*/
|
|
13
|
+
import { fileURLToPath } from "node:url";
|
|
14
|
+
import { dirname, join, resolve } from "node:path";
|
|
15
|
+
import { runMutations } from "../lib/mutation-driver.mjs";
|
|
16
|
+
|
|
17
|
+
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
18
|
+
const ROOT = resolve(HERE, "..");
|
|
19
|
+
const RULE = join(HERE, "paper-research-question.mjs");
|
|
20
|
+
const HARNESS = join(HERE, "paper-research-question.harness.mjs");
|
|
21
|
+
|
|
22
|
+
process.exit(
|
|
23
|
+
runMutations({
|
|
24
|
+
root: ROOT,
|
|
25
|
+
runner: "node",
|
|
26
|
+
cases: [
|
|
27
|
+
{
|
|
28
|
+
name: "the rule stops noticing that nothing was declared",
|
|
29
|
+
harness: HARNESS,
|
|
30
|
+
expect: "shipped and no question β a finding",
|
|
31
|
+
disables:
|
|
32
|
+
"the subject itself: the rule stays silent across the whole corpus, and silence is " +
|
|
33
|
+
"its success state β from the outside a disabled rule is indistinguishable from a clean paper",
|
|
34
|
+
edits: [[RULE, 'if (question.trim() === "") {', "if (false) {"]],
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
// π΄ The half that did not exist before 2026-09-19, and the reason the redesign happened.
|
|
38
|
+
// Without this case the second step could be deleted and the battery would stay green on
|
|
39
|
+
// the strength of the first β which is how a two-step check quietly becomes a one-step one.
|
|
40
|
+
name: "the CONTENT check is dropped β a declaration nobody carries passes",
|
|
41
|
+
harness: HARNESS,
|
|
42
|
+
expect:
|
|
43
|
+
"declared but absent from the paper β a finding, where the old pattern was silent",
|
|
44
|
+
disables:
|
|
45
|
+
"step two. The scorecard would only have to SAY a question exists, never to have it in " +
|
|
46
|
+
"the paper β which is the checklist the `bytes` field exists to not be. The predecessor " +
|
|
47
|
+
"failed the same way from the other side: it matched Β«we leave the research question to " +
|
|
48
|
+
"future workΒ» and reported a paper that states no question as clean",
|
|
49
|
+
edits: [
|
|
50
|
+
[
|
|
51
|
+
RULE,
|
|
52
|
+
"if (flatten(raw).includes(flatten(question))) return;",
|
|
53
|
+
"return;",
|
|
54
|
+
],
|
|
55
|
+
],
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
name: "the STAGE GATE is removed β drafts get scolded too",
|
|
59
|
+
harness: HARNESS,
|
|
60
|
+
expect:
|
|
61
|
+
"a draft (no stages) β silent, even though it has no question either",
|
|
62
|
+
disables:
|
|
63
|
+
"the scope. The rule does not ask 'is there a question', it asks 'is there a question " +
|
|
64
|
+
"FOR SOMETHING SHIPPED'. Without the gate every draft gets a finding β and a rule that " +
|
|
65
|
+
"scolds drafts gets turned off within a week, at which point both real findings disappear too",
|
|
66
|
+
edits: [
|
|
67
|
+
[
|
|
68
|
+
RULE,
|
|
69
|
+
"if (stages.length === 0) return; // not shipped β owes nothing",
|
|
70
|
+
"",
|
|
71
|
+
],
|
|
72
|
+
],
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
name: "the LANGUAGE narrows to LaTeX β a markdown paper becomes invisible",
|
|
76
|
+
harness: HARNESS,
|
|
77
|
+
expect: "a paper in markdown is checked the same way",
|
|
78
|
+
disables:
|
|
79
|
+
"the second half of the language. For the `tex/latex` language the text sits in `raw`, " +
|
|
80
|
+
"for markdown it sits in `text`. The mutation keeps only the first, and " +
|
|
81
|
+
"`compile-rules-2026` (paper written in markdown, no question) stops being found β HALF " +
|
|
82
|
+
"of the live corpus's findings disappear silently, and the run stays green",
|
|
83
|
+
edits: [
|
|
84
|
+
[
|
|
85
|
+
RULE,
|
|
86
|
+
"const raw = context.sourceCode.raw ?? context.sourceCode.text;",
|
|
87
|
+
"const raw = context.sourceCode.raw;",
|
|
88
|
+
],
|
|
89
|
+
],
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
name: "the stage list is taken from somewhere OTHER than the field again",
|
|
93
|
+
harness: HARNESS,
|
|
94
|
+
expect:
|
|
95
|
+
"the stage list in the message comes from the field and carries BOTH",
|
|
96
|
+
disables:
|
|
97
|
+
"the whole reason the move was made. The predecessor derived the stage with a regex " +
|
|
98
|
+
"over the scorecard's prose and on agenticdev printed `submitted` where `submitted, " +
|
|
99
|
+
"camera-ready` was declared. The mutation does not change the set of findings β only " +
|
|
100
|
+
"the TEXT lies, and without its own assert the regression would have passed silently",
|
|
101
|
+
edits: [
|
|
102
|
+
[
|
|
103
|
+
RULE,
|
|
104
|
+
'data: { stages: stages.join("/") }',
|
|
105
|
+
'data: { stages: "submitted" }',
|
|
106
|
+
],
|
|
107
|
+
],
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
name: "the scorecard's name stops arriving as an option",
|
|
111
|
+
harness: HARNESS,
|
|
112
|
+
expect:
|
|
113
|
+
"and with a nonexistent scorecard a shipped paper is also silent β the stage gate is load-bearing",
|
|
114
|
+
disables:
|
|
115
|
+
"the boundary 'the mechanism goes in the package, the data stays with the consumer'. " +
|
|
116
|
+
"`PIPELINE-STATUS.md` is ONE repository's convention, and hardcoding it makes the rule unusable for everyone else",
|
|
117
|
+
edits: [
|
|
118
|
+
[
|
|
119
|
+
RULE,
|
|
120
|
+
'const statusName =\n context.options?.[0]?.statusFile ?? "PIPELINE-STATUS.md";',
|
|
121
|
+
'const statusName =\n "PIPELINE-STATUS.md";',
|
|
122
|
+
],
|
|
123
|
+
],
|
|
124
|
+
},
|
|
125
|
+
],
|
|
126
|
+
}),
|
|
127
|
+
);
|