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,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Battery for `paper/typography` — mutations over DIFFERENT properties: the `§` count, the
|
|
3
|
+
* ratchet and its direction, and what the bare-decimal count is allowed to read.
|
|
4
|
+
*
|
|
5
|
+
* 🔴 Why the ratchet needs TWO mutations, not one. It has two halves, and they're opposite:
|
|
6
|
+
* declared debt must STAY SILENT (otherwise a legacy paper drowns out a new finding and the
|
|
7
|
+
* rule gets turned off), while growth must SPEAK (otherwise debt turns into a permit). A
|
|
8
|
+
* mutation removing the first leaves the second green and vice versa — so both are needed, and
|
|
9
|
+
* they must die on different asserts.
|
|
10
|
+
*
|
|
11
|
+
* ⚠️ What the battery does NOT check, and why: the accuracy of the counting regexes
|
|
12
|
+
* themselves. The harness's own fixtures hold those (an arXiv identifier is not a decimal
|
|
13
|
+
* fraction, consecutive `Figure`s are not a defect), and a mutation here would prove the
|
|
14
|
+
* fixture exists, not that the count is correct.
|
|
15
|
+
*
|
|
16
|
+
* The bare-decimal count is the exception, because what it gets wrong is not the lexeme but the
|
|
17
|
+
* INPUT: rpp#44 was a correct regex run over the wrong text. So two cases below change what it
|
|
18
|
+
* reads — back to the raw source (the SILENT half must die), and with math dropped from the walk
|
|
19
|
+
* (the CAUGHT half must die, since math is where p-values live).
|
|
20
|
+
*/
|
|
21
|
+
import { fileURLToPath } from "node:url";
|
|
22
|
+
import { dirname, join, resolve } from "node:path";
|
|
23
|
+
import { runMutations } from "../lib/mutation-driver.mjs";
|
|
24
|
+
|
|
25
|
+
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
26
|
+
const ROOT = resolve(HERE, "..");
|
|
27
|
+
const RULE = join(HERE, "paper-typography.mjs");
|
|
28
|
+
const HARNESS = join(HERE, "paper-typography.harness.mjs");
|
|
29
|
+
|
|
30
|
+
process.exit(
|
|
31
|
+
runMutations({
|
|
32
|
+
root: ROOT,
|
|
33
|
+
runner: "node",
|
|
34
|
+
cases: [
|
|
35
|
+
{
|
|
36
|
+
name: "the ratchet stops LETTING declared debt THROUGH",
|
|
37
|
+
harness: HARNESS,
|
|
38
|
+
expect: "known debt, unchanged, is silent",
|
|
39
|
+
disables:
|
|
40
|
+
"the 'debt stays silent' half — a legacy paper with 246 § characters starts going " +
|
|
41
|
+
"red on every run, and the rule gets turned off within a day, taking new findings with it",
|
|
42
|
+
edits: [
|
|
43
|
+
[
|
|
44
|
+
RULE,
|
|
45
|
+
"if (n <= before) continue; // known debt, unchanged or paid down",
|
|
46
|
+
"if (false) continue;",
|
|
47
|
+
],
|
|
48
|
+
],
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
name: "the ratchet stops CATCHING growth",
|
|
52
|
+
harness: HARNESS,
|
|
53
|
+
expect: "growth over known debt is reported",
|
|
54
|
+
disables:
|
|
55
|
+
"the second half — the one debt is declared for in the first place: without it a " +
|
|
56
|
+
"record in the debt file turns into a permanent permit rather than a mark of today's state",
|
|
57
|
+
// 🔴 NOT an unconditional `continue;`: that kills the rule entirely, and the harness dies
|
|
58
|
+
// on the very first "fires on §" assert — that is, a finding about the MUTATION, not
|
|
59
|
+
// about the protection. `before > 0` keeps the rule alive where there is no debt, and
|
|
60
|
+
// mutes EXACTLY growth over declared debt.
|
|
61
|
+
edits: [
|
|
62
|
+
[
|
|
63
|
+
RULE,
|
|
64
|
+
"if (n <= before) continue; // known debt, unchanged or paid down",
|
|
65
|
+
"if (before > 0) continue;",
|
|
66
|
+
],
|
|
67
|
+
],
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
name: "the `§` counter stops seeing the macro form",
|
|
71
|
+
harness: HARNESS,
|
|
72
|
+
expect:
|
|
73
|
+
"the section sign count includes the macro form, not only the glyph",
|
|
74
|
+
disables:
|
|
75
|
+
"exactly the case the counter was written for: in LaTeX the section sign is typeset " +
|
|
76
|
+
"as `\\S\\ref{…}`, not as the glyph, and a check that only counts the glyph reports " +
|
|
77
|
+
"clean on the very defect it was written to catch",
|
|
78
|
+
edits: [
|
|
79
|
+
[
|
|
80
|
+
RULE,
|
|
81
|
+
"(body.match(/§/g) || []).length +\n (body.match(/\\\\S(?=\\s*\\\\ref|~\\\\ref|\\d)/g) || []).length",
|
|
82
|
+
"(body.match(/§/g) || []).length",
|
|
83
|
+
],
|
|
84
|
+
],
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
name: "bareDecimal reads the RAW source again (the #44 defect)",
|
|
88
|
+
harness: HARNESS,
|
|
89
|
+
expect: "LaTeX SILENT: a figure width option",
|
|
90
|
+
disables:
|
|
91
|
+
"the reason the count walks the tree: over raw source the regex fires on option values, " +
|
|
92
|
+
"column specs, comments, listings and tikz coordinates — 22 findings, 0 real, on the " +
|
|
93
|
+
"corpus that prompted #44",
|
|
94
|
+
edits: [[RULE, "visibleRuns(context.sourceCode),", "[raw],"]],
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
name: "arguments unified-latex did not attach are read as prose again",
|
|
98
|
+
harness: HARNESS,
|
|
99
|
+
expect: "LaTeX SILENT: a macro definition body",
|
|
100
|
+
disables:
|
|
101
|
+
"the second path to the #44 defect: for `\\def`, an author's macro, `subfigure` or " +
|
|
102
|
+
"`longtable` the parser returns the arguments as sibling nodes, and a tree walk that " +
|
|
103
|
+
"trusts the tree reads `{.48\\textwidth}` as prose",
|
|
104
|
+
edits: [
|
|
105
|
+
[
|
|
106
|
+
RULE,
|
|
107
|
+
' while (nodes[j]?.type === "group") j++;\n return j - 1;',
|
|
108
|
+
" return i;",
|
|
109
|
+
],
|
|
110
|
+
],
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
name: "the tree walk stops entering math",
|
|
114
|
+
harness: HARNESS,
|
|
115
|
+
// The messy fixture's two decimals are both in math, so the first assertion to die is the
|
|
116
|
+
// plain "fires" one — which is the point: without math there is no bare decimal left.
|
|
117
|
+
expect: "fires on the bare decimal",
|
|
118
|
+
disables:
|
|
119
|
+
"the half the projection could never give: math is blanked there, and math is where " +
|
|
120
|
+
"`$p < .05$` lives — the exact form the reviewer flagged",
|
|
121
|
+
edits: [
|
|
122
|
+
[
|
|
123
|
+
RULE,
|
|
124
|
+
'n.type === "mathenv"\n )\n texRuns(n.content, true, out);',
|
|
125
|
+
'n.type === "mathenv"\n )\n void 0;',
|
|
126
|
+
],
|
|
127
|
+
],
|
|
128
|
+
},
|
|
129
|
+
],
|
|
130
|
+
}),
|
|
131
|
+
);
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Colocated test for `eslint-rules/papers.mjs`.
|
|
3
|
+
* Run: `npx vigiles test eslint-rules/papers.harness.mjs`
|
|
4
|
+
*
|
|
5
|
+
* FOUR PARTS:
|
|
6
|
+
* I. THE DEFAULT AND THE DECLARATION — the two ways a root is chosen.
|
|
7
|
+
* II. THE REFUSALS — every input this module must NOT quietly accept, each checked for the
|
|
8
|
+
* message a person will actually read, not only for the fact that something threw.
|
|
9
|
+
* III. PROPERTIES NOBODY SEES, WITHOUT WHICH THE GLOBS SILENTLY CHANGE MEANING:
|
|
10
|
+
* the returned root is RELATIVE, and `baseDir` — not `process.cwd()` — decides where it
|
|
11
|
+
* is looked for.
|
|
12
|
+
* IV. THE ACCEPTANCE CRITERION OF THE WHOLE EXTRACTION: no consumer-specific path anywhere
|
|
13
|
+
* in this package. This is the assertion that makes the package reusable, and it is the
|
|
14
|
+
* one that will fail first when someone "just hard-codes it for now".
|
|
15
|
+
*
|
|
16
|
+
* 🔴 Assertions live at MODULE TOP LEVEL: `vigiles test` imports the file and treats "it did
|
|
17
|
+
* not throw" as success; an exported `tests` object is run by nothing.
|
|
18
|
+
*/
|
|
19
|
+
import assert from "node:assert/strict";
|
|
20
|
+
import { PAPERS_DIR_FIELD } from "../lib/paper-config.mjs";
|
|
21
|
+
import {
|
|
22
|
+
mkdirSync,
|
|
23
|
+
mkdtempSync,
|
|
24
|
+
readFileSync,
|
|
25
|
+
readdirSync,
|
|
26
|
+
rmSync,
|
|
27
|
+
statSync,
|
|
28
|
+
realpathSync,
|
|
29
|
+
} from "node:fs";
|
|
30
|
+
import { tmpdir } from "node:os";
|
|
31
|
+
import { dirname, isAbsolute, join } from "node:path";
|
|
32
|
+
import { fileURLToPath } from "node:url";
|
|
33
|
+
|
|
34
|
+
import { DEFAULT_PAPERS_ROOT, paperFiles, papersRoot } from "./papers.mjs";
|
|
35
|
+
|
|
36
|
+
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
37
|
+
const ROOT = join(HERE, "..");
|
|
38
|
+
|
|
39
|
+
const TMP = realpathSync(mkdtempSync(join(tmpdir(), "papers-root-")));
|
|
40
|
+
// Registered immediately, not at the end: assertions throw, and cleanup at the bottom of a
|
|
41
|
+
// file never runs in exactly the runs that are red.
|
|
42
|
+
process.on("exit", () => rmSync(TMP, { recursive: true, force: true }));
|
|
43
|
+
|
|
44
|
+
/** A throwaway repository with the given directories already on disk. */
|
|
45
|
+
const repoWith = (...dirs) => {
|
|
46
|
+
const base = mkdtempSync(join(TMP, "repo-"));
|
|
47
|
+
for (const d of dirs) mkdirSync(join(base, d), { recursive: true });
|
|
48
|
+
return base;
|
|
49
|
+
};
|
|
50
|
+
const declaring = (papers) => ({
|
|
51
|
+
paperlint: { [PAPERS_DIR_FIELD]: papers },
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// ═════════════════════════════════════════════════════════════════════════════
|
|
55
|
+
// I. THE DEFAULT AND THE DECLARATION
|
|
56
|
+
// ═════════════════════════════════════════════════════════════════════════════
|
|
57
|
+
{
|
|
58
|
+
const repo = repoWith(DEFAULT_PAPERS_ROOT);
|
|
59
|
+
for (const [label, pkg] of [
|
|
60
|
+
["no package.json content at all", undefined],
|
|
61
|
+
["an empty package.json", {}],
|
|
62
|
+
["a package.json with an unrelated key", { name: "x", dependencies: {} }],
|
|
63
|
+
["our key present but empty", { paperlint: {} }],
|
|
64
|
+
])
|
|
65
|
+
assert.equal(
|
|
66
|
+
papersRoot(pkg, repo),
|
|
67
|
+
DEFAULT_PAPERS_ROOT,
|
|
68
|
+
`${label}: a consumer that declares nothing must get the default "${DEFAULT_PAPERS_ROOT}"`,
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// A declared root wins, including a nested one — the first consumer's is two levels deep, and
|
|
73
|
+
// a helper that only handled a single path segment would work on every fixture and fail there.
|
|
74
|
+
for (const declared of ["papers", "docs/papers", "writing/drafts", "a/b/c/d"]) {
|
|
75
|
+
const repo = repoWith(declared);
|
|
76
|
+
assert.equal(
|
|
77
|
+
papersRoot(declaring(declared), repo),
|
|
78
|
+
declared,
|
|
79
|
+
`a declared root must be returned as declared: ${declared}`,
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// ═════════════════════════════════════════════════════════════════════════════
|
|
84
|
+
// II. THE REFUSALS — and the message each one carries
|
|
85
|
+
// ═════════════════════════════════════════════════════════════════════════════
|
|
86
|
+
// 🔴 THE CENTRAL ONE. A root that is not on disk must STOP the config from loading, because
|
|
87
|
+
// the alternative is not "wrong findings" but NO findings — and a run with no findings is
|
|
88
|
+
// byte-identical to a run that checked everything and passed.
|
|
89
|
+
{
|
|
90
|
+
const repo = repoWith("somewhere-else");
|
|
91
|
+
assert.throws(
|
|
92
|
+
() => papersRoot(declaring("writing/drafts"), repo),
|
|
93
|
+
(e) =>
|
|
94
|
+
/does not exist/.test(e.message) &&
|
|
95
|
+
// names WHAT was declared, so the reader does not have to guess which of several
|
|
96
|
+
// candidate paths the tool was looking at,
|
|
97
|
+
e.message.includes('"writing/drafts"') &&
|
|
98
|
+
// names WHERE it looked,
|
|
99
|
+
e.message.includes(repo) &&
|
|
100
|
+
// tells the reader it came from package.json rather than from a default,
|
|
101
|
+
/declared in package\.json/.test(e.message) &&
|
|
102
|
+
// and says WHY this is fatal instead of ignorable.
|
|
103
|
+
/indistinguishable/.test(e.message),
|
|
104
|
+
"a declared-but-missing root must throw, and the message must name the value, the place " +
|
|
105
|
+
"it looked, its source, and why silence was not an option",
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
// The SAME failure reached by defaulting carries a DIFFERENT message, and that difference is
|
|
109
|
+
// the whole usefulness of it: "you declared the wrong thing" and "you declared nothing and
|
|
110
|
+
// the default does not fit you" have different fixes, and a shared message would hide which.
|
|
111
|
+
assert.throws(
|
|
112
|
+
() => papersRoot({}, repo),
|
|
113
|
+
(e) =>
|
|
114
|
+
/does not exist/.test(e.message) &&
|
|
115
|
+
/Nothing was declared/.test(e.message) &&
|
|
116
|
+
e.message.includes(
|
|
117
|
+
`"paperlint": { "${PAPERS_DIR_FIELD}": "path/to/papers" }`,
|
|
118
|
+
),
|
|
119
|
+
"a missing root reached by DEFAULTING must say so and show the declaration to add",
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Shapes that are present but unusable. Left unchecked, `""` is the worst of them: it makes
|
|
124
|
+
// every glob `"/*/paper.tex"` — rooted at the filesystem, matching nothing, silently.
|
|
125
|
+
for (const bad of ["", null, 0, false, [], {}, 42])
|
|
126
|
+
assert.throws(
|
|
127
|
+
() => papersRoot(declaring(bad), TMP),
|
|
128
|
+
(e) => e instanceof TypeError && /non-empty string/.test(e.message),
|
|
129
|
+
`a "papers" of ${JSON.stringify(bad)} must be refused as a type error, not coerced`,
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
// ═════════════════════════════════════════════════════════════════════════════
|
|
133
|
+
// III. PROPERTIES NOBODY SEES
|
|
134
|
+
// ═════════════════════════════════════════════════════════════════════════════
|
|
135
|
+
// 🔴 THE RETURNED ROOT MUST STAY RELATIVE. ESLint resolves `files:` globs against the config's
|
|
136
|
+
// directory; an absolute path there is a different pattern with different behaviour, and the
|
|
137
|
+
// breakage would show up as "the rule stopped matching", i.e. as silence.
|
|
138
|
+
{
|
|
139
|
+
const repo = repoWith("writing/drafts");
|
|
140
|
+
const got = papersRoot(declaring("writing/drafts"), repo);
|
|
141
|
+
assert.equal(
|
|
142
|
+
isAbsolute(got),
|
|
143
|
+
false,
|
|
144
|
+
"the root must be returned relative, not resolved",
|
|
145
|
+
);
|
|
146
|
+
assert.equal(
|
|
147
|
+
got.includes(repo),
|
|
148
|
+
false,
|
|
149
|
+
"the root must not carry the base directory",
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// 🔴 `baseDir` DECIDES, NOT `process.cwd()`. This is the assertion that pins the reason the
|
|
154
|
+
// parameter exists: cwd is wherever the editor, the CI step or the hook runtime happened to
|
|
155
|
+
// start, and in a git worktree it can be an entirely different checkout. A helper that quietly
|
|
156
|
+
// used cwd would pass every test run from the repository root and fail in exactly the setups
|
|
157
|
+
// nobody runs tests in.
|
|
158
|
+
{
|
|
159
|
+
const real = repoWith("writing/drafts");
|
|
160
|
+
const empty = repoWith("unrelated");
|
|
161
|
+
assert.equal(papersRoot(declaring("writing/drafts"), real), "writing/drafts");
|
|
162
|
+
assert.throws(
|
|
163
|
+
() => papersRoot(declaring("writing/drafts"), empty),
|
|
164
|
+
/does not exist/,
|
|
165
|
+
"baseDir must be what is consulted — the same declaration must fail against a base that " +
|
|
166
|
+
"does not contain the root",
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Every glob set is derived from the root, and NONE of them is spelled out independently: a
|
|
171
|
+
// set that forgot to interpolate would keep matching the previous consumer's layout.
|
|
172
|
+
{
|
|
173
|
+
const sets = paperFiles("R");
|
|
174
|
+
const names = Object.keys(sets).sort();
|
|
175
|
+
assert.deepEqual(
|
|
176
|
+
names,
|
|
177
|
+
["all", "md", "pdfFacts", "refFacts", "status", "tex", "venue"],
|
|
178
|
+
"the glob set changed — a consumer's config enumerates these by name, and a renamed or " +
|
|
179
|
+
"dropped key is a block that silently lints nothing",
|
|
180
|
+
);
|
|
181
|
+
for (const [name, globs] of Object.entries(sets)) {
|
|
182
|
+
assert.ok(
|
|
183
|
+
Array.isArray(globs) && globs.length > 0,
|
|
184
|
+
`${name}: must be a non-empty array`,
|
|
185
|
+
);
|
|
186
|
+
for (const g of globs)
|
|
187
|
+
assert.ok(
|
|
188
|
+
g.startsWith("R/"),
|
|
189
|
+
`${name}: glob "${g}" does not start at the given root`,
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
// Two roots must produce two disjoint sets — proof the root is interpolated everywhere and
|
|
193
|
+
// not merely present in the first glob of each list.
|
|
194
|
+
const other = paperFiles("Q");
|
|
195
|
+
for (const name of names)
|
|
196
|
+
assert.deepEqual(
|
|
197
|
+
sets[name].map((g) => g.replace(/^R\//, "")),
|
|
198
|
+
other[name].map((g) => g.replace(/^Q\//, "")),
|
|
199
|
+
`${name}: the two roots did not produce the same shapes — something is hard-coded`,
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// ═════════════════════════════════════════════════════════════════════════════
|
|
204
|
+
// IV. THE ACCEPTANCE CRITERION OF THE EXTRACTION
|
|
205
|
+
// ═════════════════════════════════════════════════════════════════════════════
|
|
206
|
+
// 🔴 NO CONSUMER-SPECIFIC PATH ANYWHERE IN THIS PACKAGE. This is the assertion the whole
|
|
207
|
+
// extraction exists for, and the reason it is here rather than in a plan document: a plan is
|
|
208
|
+
// read once, a test runs every time. The forbidden word is the first consumer's own directory
|
|
209
|
+
// name; it may appear ONLY in this file (as the value under test) and in prose that documents
|
|
210
|
+
// the decision.
|
|
211
|
+
{
|
|
212
|
+
// ⚠️ THE ONLY LEGITIMATE OCCURRENCE OF THIS WORD IN THE PACKAGE, and it is here out of
|
|
213
|
+
// necessity: it is the SUBJECT of the assert. The fixtures above deliberately use neutral
|
|
214
|
+
// paths (`writing/drafts`) — until 2026-09-12 they carried the consumer's real directory name,
|
|
215
|
+
// and the pre-publication audit found eleven occurrences where the meaning called for one.
|
|
216
|
+
// ⚠️ THIS LIST HOLDS PATH TOKENS ONLY, AND THE PERSONAL NAMES ARE DELIBERATELY ABSENT.
|
|
217
|
+
// Extending it to the owner's name was tried on 2026-09-12 and reverted the same hour by the
|
|
218
|
+
// consumer-side audit that reads this repository before it is published: to GUARD against a
|
|
219
|
+
// string a checker must SPELL it, so the extension put a private personal name into a public
|
|
220
|
+
// source file and into a public commit message — a leak created by the leak detector. A
|
|
221
|
+
// directory name is a layout fact and is safe to spell here; a person is not.
|
|
222
|
+
//
|
|
223
|
+
// The personal half therefore lives in the CONSUMER, where those strings already are, and runs
|
|
224
|
+
// over `node_modules/<this package>` from there. That is rule T3 of the extraction plan, and
|
|
225
|
+
// this is the measurement that made it non-negotiable rather than merely tidy.
|
|
226
|
+
const FORBIDDEN = ["migratsiya"];
|
|
227
|
+
const SELF = fileURLToPath(import.meta.url);
|
|
228
|
+
const skipDirs = new Set(["node_modules", ".git", "fixtures"]);
|
|
229
|
+
const offenders = [];
|
|
230
|
+
const walk = (dir) => {
|
|
231
|
+
for (const entry of readdirSync(dir)) {
|
|
232
|
+
if (skipDirs.has(entry)) continue;
|
|
233
|
+
const p = join(dir, entry);
|
|
234
|
+
if (statSync(p).isDirectory()) {
|
|
235
|
+
walk(p);
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
if (p === SELF) continue;
|
|
239
|
+
if (!/\.(mjs|js|ts|json|tex|sh)$/.test(entry)) continue;
|
|
240
|
+
let text;
|
|
241
|
+
try {
|
|
242
|
+
text = readFileSync(p, "utf8");
|
|
243
|
+
} catch {
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
const hit = FORBIDDEN.find((w) => text.includes(w));
|
|
247
|
+
if (hit) offenders.push(`${p.slice(ROOT.length + 1)} (${hit})`);
|
|
248
|
+
}
|
|
249
|
+
};
|
|
250
|
+
walk(ROOT);
|
|
251
|
+
assert.deepEqual(
|
|
252
|
+
offenders,
|
|
253
|
+
[],
|
|
254
|
+
`this package names the first consumer's own directory ("${FORBIDDEN.join(", ")}") in: ` +
|
|
255
|
+
`${offenders.join(", ")}. A package that hard-codes one consumer's layout has exactly ` +
|
|
256
|
+
`one possible user, which is the thing the extraction exists to undo. The root belongs ` +
|
|
257
|
+
`in the consumer's package.json — see eslint-rules/papers.mjs`,
|
|
258
|
+
);
|
|
259
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* papers.mjs — WHERE THE CONSUMER'S PAPERS LIVE. One declaration, three readers.
|
|
3
|
+
*
|
|
4
|
+
* ── THE PROBLEM THIS SOLVES ─────────────────────────────────────────────────
|
|
5
|
+
* This package lints papers. It must not know where any particular consumer keeps them: the
|
|
6
|
+
* first consumer keeps them two levels deep under a directory named in its own language, and
|
|
7
|
+
* a package that hard-codes that word is a package with exactly one possible user. But the
|
|
8
|
+
* path cannot simply be "passed in" either, because it is needed by FOUR carriers with four
|
|
9
|
+
* different mechanics — an ESLint config (JavaScript), a compiled hook (a closed vocabulary
|
|
10
|
+
* that forbids imports), the PROSE of a skill (read by a model, not resolved by code), and
|
|
11
|
+
* LaTeX (`TEXINPUTS`, kpathsea).
|
|
12
|
+
*
|
|
13
|
+
* ── THE DECISION (2026-09-11) ───────────────────────────────────────────────
|
|
14
|
+
* ONE declaration, in the consumer's `package.json`, with a default:
|
|
15
|
+
*
|
|
16
|
+
* "paperlint": { "papersDir": "docs/papers" }
|
|
17
|
+
*
|
|
18
|
+
* (The field was called `papers` until 2026-09-24; the old name is now refused.)
|
|
19
|
+
* No key → `papers`. Every carrier then reads that one value with its OWN standard mechanism:
|
|
20
|
+
*
|
|
21
|
+
* ESLint → `import pkg from "./package.json" with { type: "json" }` + this module
|
|
22
|
+
* hook → `needs: [provide("pkg", "cat package.json")]`, JSON.parse inside `decide`
|
|
23
|
+
* prose → the skill names a COMMAND, not a path:
|
|
24
|
+
* `node -p "require('./package.json')['paperlint']?.papersDir ?? 'papers'"`
|
|
25
|
+
* LaTeX → `TEXINPUTS` built FROM THE SCRIPT (`$(dirname "$0")/../tex//:`), so the
|
|
26
|
+
* consumer declares nothing at all for this carrier
|
|
27
|
+
*
|
|
28
|
+
* `package.json` is the right home rather than a new dotfile for one measured reason: it is
|
|
29
|
+
* the file Node already opens to resolve this package, so it cannot be absent, cannot be
|
|
30
|
+
* gitignored, and needs no discovery rules of its own.
|
|
31
|
+
*
|
|
32
|
+
* ── WHAT WAS MEASURED AND KILLED, so it is not proposed again ───────────────
|
|
33
|
+
* (full measurements: consumer repo, `idei/paper-pipeline-extraction/`, 2026-09-11)
|
|
34
|
+
*
|
|
35
|
+
* A SYMLINK (`papers -> <the real directory>`) plus a hard-coded default, no config at all.
|
|
36
|
+
* ESLint does NOT descend into a symlinked directory when walking `.`: the run reported
|
|
37
|
+
* ZERO files and exit 0 — the green zero this package has a dedicated guard against.
|
|
38
|
+
*
|
|
39
|
+
* AN ENVIRONMENT VARIABLE (`$PAPER_ROOT`).
|
|
40
|
+
* A compiled hook cannot read `process.env` — the vocabulary check rejects every import
|
|
41
|
+
* except `vigiles/hook`. The only way in is `provide("root", "echo $PAPER_ROOT")`, and
|
|
42
|
+
* then THREE different places must set the same value (the agent harness that spawns the
|
|
43
|
+
* hook, CI, the editor running ESLint). Unset, it expands to the empty string and every
|
|
44
|
+
* prefix test silently passes.
|
|
45
|
+
*
|
|
46
|
+
* A FILENAME MARKER, no root at all (`**\/paper.tex`).
|
|
47
|
+
* Measured on the first consumer: `paper.tex` ×4 with one outside the corpus,
|
|
48
|
+
* `PIPELINE-STATUS.md` ×6 with two of them test fixtures, `paper.md` ×3 with two outside.
|
|
49
|
+
* The guard that would consume this is a `deny` at `error` severity; a denial on writing
|
|
50
|
+
* a fixture gets the hook switched off the same day.
|
|
51
|
+
*
|
|
52
|
+
* `context.settings`.
|
|
53
|
+
* It delivers data INTO a rule; it does not choose which files a rule sees. `files:` is
|
|
54
|
+
* the only mechanism that does. Useful for a plugin's `structureRoot`, useless here.
|
|
55
|
+
*
|
|
56
|
+
* ── WHY THIS THROWS INSTEAD OF FALLING BACK ─────────────────────────────────
|
|
57
|
+
* 🔴 A wrong root does not produce wrong findings — it produces NO findings, and a run with no
|
|
58
|
+
* findings is byte-identical to a run that examined everything and passed. That is the one
|
|
59
|
+
* failure mode of this whole design, and it belongs to the ESLint carrier alone: the other
|
|
60
|
+
* three fail loudly on their own (a hook throws on `join(undefined)`, `ls` exits non-zero,
|
|
61
|
+
* LaTeX stops with `! Emergency stop.`). So this module refuses to return a root it cannot
|
|
62
|
+
* see on disk, and the refusal happens while the CONFIG is loading — before any rule runs,
|
|
63
|
+
* where it cannot be mistaken for a clean lint.
|
|
64
|
+
*
|
|
65
|
+
* ⚠️ THAT IS NOT ENOUGH ON ITS OWN, and saying so is the point. `existsSync` proves the
|
|
66
|
+
* directory exists, not that any rule was handed a file from it: a root that exists but holds
|
|
67
|
+
* no papers, or a glob shape that stopped matching, still yields the green zero. The second
|
|
68
|
+
* net is `scripts/rules-see-files.mjs`, which asks ESLint what the effective config of every
|
|
69
|
+
* linted file is and names any rule that saw none. A consumer using this module without
|
|
70
|
+
* running that script has one net, not two.
|
|
71
|
+
*
|
|
72
|
+
* ⚠️ THE EXAMPLES IN THIS FILE ARE DELIBERATELY GENERIC (`docs/papers`), and that is ENFORCED
|
|
73
|
+
* rather than a style preference: `papers.harness.mjs` part IV fails if the first consumer's own
|
|
74
|
+
* directory name appears anywhere in this package outside the test itself. It caught this very
|
|
75
|
+
* docblock on 2026-09-11, where the example used the real value. An exemption list is what turns
|
|
76
|
+
* such a check into decoration, so there is exactly one exemption: the file asserting it.
|
|
77
|
+
*/
|
|
78
|
+
import { existsSync } from "node:fs";
|
|
79
|
+
import { resolve } from "node:path";
|
|
80
|
+
|
|
81
|
+
// Re-exported, not re-declared. This file is not a hook, so nothing stops it importing the one
|
|
82
|
+
// source; it had its own copy for no reason, and that copy was outside the agreement check that
|
|
83
|
+
// compares the three hooks — it could have drifted in silence.
|
|
84
|
+
import {
|
|
85
|
+
CONFIG_KEY,
|
|
86
|
+
DEFAULT_PAPERS_ROOT,
|
|
87
|
+
PAPERS_DIR_FIELD,
|
|
88
|
+
declaredSettings,
|
|
89
|
+
renamedFieldMessage,
|
|
90
|
+
settingsOf,
|
|
91
|
+
} from "../lib/paper-config.mjs";
|
|
92
|
+
export { DEFAULT_PAPERS_ROOT };
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* The declared papers root, verified to be on disk.
|
|
96
|
+
*
|
|
97
|
+
* @param pkg the consumer's parsed `package.json`
|
|
98
|
+
* @param baseDir the directory the root is relative to. PASS `import.meta.dirname` FROM THE
|
|
99
|
+
* CONFIG rather than relying on the default: `process.cwd()` is wherever the
|
|
100
|
+
* editor, the hook runtime or the CI step happened to start, and in a git
|
|
101
|
+
* worktree it can be a different checkout entirely. The config file sits at
|
|
102
|
+
* the repository root, so its own directory is the stable answer.
|
|
103
|
+
* @returns the root as DECLARED (relative), not resolved — it goes straight into `files:`
|
|
104
|
+
* globs, which ESLint interprets relative to the config, and an absolute path there
|
|
105
|
+
* would change their meaning.
|
|
106
|
+
*/
|
|
107
|
+
export function papersRoot(pkg, baseDir = process.cwd()) {
|
|
108
|
+
const found = declaredSettings(pkg);
|
|
109
|
+
if (found.conflict !== null) throw new TypeError(found.conflict);
|
|
110
|
+
const renamed = renamedFieldMessage(found.settings);
|
|
111
|
+
if (renamed) throw new TypeError(renamed);
|
|
112
|
+
const declared = settingsOf(pkg)?.[PAPERS_DIR_FIELD];
|
|
113
|
+
// 🔴 `declared === undefined`, NOT `declared ?? DEFAULT`. The two differ on exactly one
|
|
114
|
+
// input — `"papersDir": null` — and the difference is the whole point: `??` reads an explicit
|
|
115
|
+
// `null` as "nothing was declared" and silently uses the default, which is a typed keystroke
|
|
116
|
+
// being treated as an absence. Caught by the harness on 2026-09-11, where the first draft of
|
|
117
|
+
// this line used `??` and let `null` through. Absence means default; anything written down
|
|
118
|
+
// means it must be usable.
|
|
119
|
+
const root = declared === undefined ? DEFAULT_PAPERS_ROOT : declared;
|
|
120
|
+
if (typeof root !== "string" || root.length === 0)
|
|
121
|
+
throw new TypeError(
|
|
122
|
+
`${CONFIG_KEY}: "${PAPERS_DIR_FIELD}" must be a non-empty string, got ${JSON.stringify(root)}`,
|
|
123
|
+
);
|
|
124
|
+
if (!existsSync(resolve(baseDir, root)))
|
|
125
|
+
throw new Error(
|
|
126
|
+
`${CONFIG_KEY}: the papers root "${root}" does not exist under ${baseDir}.\n` +
|
|
127
|
+
(declared === undefined
|
|
128
|
+
? `Nothing was declared, so the default "${DEFAULT_PAPERS_ROOT}" was used. Declare the ` +
|
|
129
|
+
`real location in package.json:\n` +
|
|
130
|
+
` "${CONFIG_KEY}": { "${PAPERS_DIR_FIELD}": "path/to/papers" }`
|
|
131
|
+
: `It is declared in package.json as "${declared}". Fix it there, or create the ` +
|
|
132
|
+
`directory.`) +
|
|
133
|
+
`\nThis is thrown rather than ignored on purpose: a papers root that matches nothing ` +
|
|
134
|
+
`makes every rule lint zero files, and a run with zero findings is indistinguishable ` +
|
|
135
|
+
`from a run that checked everything and passed.`,
|
|
136
|
+
);
|
|
137
|
+
return root;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* The glob sets a consumer declares its blocks on, derived from one root.
|
|
142
|
+
*
|
|
143
|
+
* 🔴 THE SHAPES LIVE HERE, NOT IN THE CONSUMER, and that is the half that keeps a rule from
|
|
144
|
+
* going blind later. `paper.md` + `draft.md`, `_build/*.facts.json`, `PIPELINE-STATUS.md` are
|
|
145
|
+
* this pipeline's CONVENTION; if a rule starts reading a new file, the glob for it must arrive
|
|
146
|
+
* with the rule, in the same package, rather than as a line every consumer has to add by hand
|
|
147
|
+
* to keep up. A hand-maintained list of paths is the thing that rots, and rots silently.
|
|
148
|
+
*/
|
|
149
|
+
export function paperFiles(root) {
|
|
150
|
+
return {
|
|
151
|
+
/** Markdown drafts — the two names a paper's prose is allowed to have. */
|
|
152
|
+
md: [`${root}/*/paper.md`, `${root}/*/draft.md`],
|
|
153
|
+
/** The LaTeX source of a paper (not its figures, not its vendored drafts). */
|
|
154
|
+
tex: [`${root}/*/paper.tex`],
|
|
155
|
+
/** The per-paper stage ledger. */
|
|
156
|
+
status: [`${root}/*/PIPELINE-STATUS.md`],
|
|
157
|
+
/** The venue data card. */
|
|
158
|
+
venue: [`${root}/*/venue.json`],
|
|
159
|
+
/** Facts extracted from the built PDF. */
|
|
160
|
+
pdfFacts: [`${root}/*/_build/paper.facts.json`],
|
|
161
|
+
/** Facts extracted from the bibliography. */
|
|
162
|
+
refFacts: [`${root}/*/_build/refs.facts.json`],
|
|
163
|
+
/** Everything under the root — for blocks that turn other tooling OFF rather than on. */
|
|
164
|
+
all: [`${root}/**/*.*`],
|
|
165
|
+
};
|
|
166
|
+
}
|