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
package/lib/markdown.mjs
ADDED
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* markdown.mjs — markup parsing for EVERYTHING in this repo that reads markdown.
|
|
3
|
+
*
|
|
4
|
+
* Three kinds of consumer: two advisory linters (`kb-lint`, `paper-lint` — through the
|
|
5
|
+
* re-export from `lint-core.mjs`) and skill scripts (`tighten-paper/structure.mjs`,
|
|
6
|
+
* `grade-paper-writing/prose-lint.mjs`, `paper-pipeline/scripts/*`). Hence the home is
|
|
7
|
+
* here and not in the linter core: importing "the linter core" from a skill script
|
|
8
|
+
* would be a lie about the direction of the dependency.
|
|
9
|
+
*
|
|
10
|
+
* CHECKS NOTHING AND PRINTS NOTHING: text in, data out. What to do with a missing
|
|
11
|
+
* parser is the caller's decision, and the decisions DIFFER:
|
|
12
|
+
* • a linter — degrades and nags LOUDLY about it (the blocking gate must keep
|
|
13
|
+
* working meanwhile: PreToolUse counts only exit code 2 as a block, and a
|
|
14
|
+
* failure on import would switch the gate off silently);
|
|
15
|
+
* • a skill script — fails through `requireMarkdown()`, because its output goes
|
|
16
|
+
* into the pipeline gates, and "0 headings" reads there as a fact about the paper.
|
|
17
|
+
*/
|
|
18
|
+
// THE RULE (`CLAUDE.md`, 2026-08-11): markdown structure is parsed with a PARSER, not
|
|
19
|
+
// with regexes. Stepped on four times, three of them the same defect: JS `\b` is
|
|
20
|
+
// defined over `[A-Za-z0-9_]`, and next to a Cyrillic letter there is NO word boundary,
|
|
21
|
+
// and the `u` flag does not change that. A level-2 heading whose text is a Cyrillic word
|
|
22
|
+
// never matched once over the whole life of the check. A heading out of the parser has a
|
|
23
|
+
// LEVEL and a TEXT — "word boundary" does not exist there in nature, and the mistake
|
|
24
|
+
// becomes IMPOSSIBLE to express.
|
|
25
|
+
//
|
|
26
|
+
// The helper moved here from `paper-lint.mjs` on 2026-08-11, when a second consumer
|
|
27
|
+
// appeared (`kb-lint.checkRejectedRowsCited` cut `topics/*.md` into sections with a
|
|
28
|
+
// regex — and did so TWO LINES ABOVE its own comment about this very bug). The
|
|
29
|
+
// condition for the move had been written down in advance, for exactly this case.
|
|
30
|
+
//
|
|
31
|
+
// 🔴 THE IMPORT IS OPTIONAL. Before 2026-08-11 the hooks had not a single dependency, and
|
|
32
|
+
// a bare top-level `import` changed the shape of the failure: no `node_modules` → exit 1
|
|
33
|
+
// → **the blocking `pre` gate in paper-lint stops blocking silently**, because
|
|
34
|
+
// PreToolUse counts only exit code 2 as a block. Measured: `ERR_MODULE_NOT_FOUND`, exit 1.
|
|
35
|
+
let md = null;
|
|
36
|
+
try {
|
|
37
|
+
const { default: MarkdownIt } = await import("markdown-it");
|
|
38
|
+
md = new MarkdownIt();
|
|
39
|
+
} catch {
|
|
40
|
+
md = null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Whether the parser resolved. A linter must check this ITSELF and say out loud that
|
|
45
|
+
* its heading checks are off: with no parser `headings()` returns an empty list, and
|
|
46
|
+
* silent emptiness reads as "the document has no headings", that is, as a clean
|
|
47
|
+
* document. The nudge lives in the linters, because by contract (see the header) this
|
|
48
|
+
* file has no right to produce a single finding.
|
|
49
|
+
*/
|
|
50
|
+
export const MD_AVAILABLE = () => md !== null;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* The frontmatter is BLANKED with spaces, not cut out: the offsets must stay the
|
|
54
|
+
* same, because the checks cut the document by offset. Blanking is necessary — inside
|
|
55
|
+
* a YAML block scalar a line indented by two spaces (` ## Foo`) is a real ATX
|
|
56
|
+
* heading to CommonMark, and without blanking the frontmatter starts supplying
|
|
57
|
+
* sections.
|
|
58
|
+
*/
|
|
59
|
+
const FRONTMATTER_RE = /^---[ \t]*\r?\n[\s\S]*?\r?\n---[ \t]*(?=\r?\n|$)/;
|
|
60
|
+
export function blankFrontmatter(text) {
|
|
61
|
+
const m = FRONTMATTER_RE.exec(text);
|
|
62
|
+
return m ? m[0].replace(/[^\n]/g, " ") + text.slice(m[0].length) : text;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* The frontmatter body (between the fences), or `null` — if the file opens without one.
|
|
67
|
+
*
|
|
68
|
+
* Added 2026-08-20, when eight places in `.claude/**` cut the frontmatter with eight
|
|
69
|
+
* slightly different regexes: some knew about `\r\n`, some did not; some required a
|
|
70
|
+
* newline after the closing fence, some did not. The difference between them is not
|
|
71
|
+
* style, it is a different answer on one and the same file.
|
|
72
|
+
*
|
|
73
|
+
* ⚠️ This is NOT a parser and does not claim to be: `---` frontmatter is not markdown,
|
|
74
|
+
* no markdown parser models it (the analysis is in the root `CLAUDE.md`, "Considered
|
|
75
|
+
* and rejected: gray-matter"). The value here is one fence instead of eight, and the
|
|
76
|
+
* parsing of VALUES is done by `js-yaml` at the caller.
|
|
77
|
+
*/
|
|
78
|
+
export function frontmatterBlock(text) {
|
|
79
|
+
const m = FRONTMATTER_RE.exec(text);
|
|
80
|
+
if (!m) return null;
|
|
81
|
+
const inner = m[0]
|
|
82
|
+
.replace(/^---[ \t]*\r?\n/, "")
|
|
83
|
+
.replace(/\r?\n---[ \t]*$/, "");
|
|
84
|
+
return inner;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* The document WITHOUT the frontmatter — the block is cut out together with the
|
|
89
|
+
* newline after it.
|
|
90
|
+
*
|
|
91
|
+
* Differs from {@link blankFrontmatter} in that the offsets SHIFT. Both forms are
|
|
92
|
+
* needed and both have been used already: blanking — where character positions are
|
|
93
|
+
* later shown to the user; cutting — where the text is merely counted. Choose by what
|
|
94
|
+
* the caller does, not by taste.
|
|
95
|
+
*/
|
|
96
|
+
export function stripFrontmatter(text) {
|
|
97
|
+
const m = FRONTMATTER_RE.exec(text);
|
|
98
|
+
return m ? text.slice(m[0].length).replace(/^\r?\n/, "") : text;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Line-start offsets — so that `token.map` becomes a position in the source string. */
|
|
102
|
+
function lineStarts(text) {
|
|
103
|
+
const out = [0];
|
|
104
|
+
for (let i = 0; i < text.length; i++) if (text[i] === "\n") out.push(i + 1);
|
|
105
|
+
return out;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* All the document's headings: `[{ depth, text, line, offset }]`, in order of occurrence.
|
|
110
|
+
*
|
|
111
|
+
* `offset` — the index of the start of the heading's line IN THE SAME text that was
|
|
112
|
+
* passed in, in `slice`/`indexOf` units (UTF-16), because those are exactly what the
|
|
113
|
+
* checks cut the document with. `line` — 0-based line number. `text` — without the
|
|
114
|
+
* hashes, trimmed at the edges.
|
|
115
|
+
*
|
|
116
|
+
* ⚠️ ATX ONLY (`## Heading`). Setext (`Heading\n---`) is deliberately NOT counted as a
|
|
117
|
+
* heading: in this knowledge base `---` is a prose separator, and a paragraph it stands
|
|
118
|
+
* under without a blank line would become a level-2 heading per CommonMark. The former
|
|
119
|
+
* regexps knew only ATX, so this preserves the contract rather than narrowing it.
|
|
120
|
+
*/
|
|
121
|
+
export function headings(text) {
|
|
122
|
+
if (md === null) return [];
|
|
123
|
+
const toks = md.parse(blankFrontmatter(text), {});
|
|
124
|
+
const starts = lineStarts(text);
|
|
125
|
+
const out = [];
|
|
126
|
+
for (let i = 0; i < toks.length; i++) {
|
|
127
|
+
const t = toks[i];
|
|
128
|
+
if (t.type !== "heading_open" || !t.map) continue;
|
|
129
|
+
if (!t.markup || !t.markup.startsWith("#")) continue; // setext — see the caveat above
|
|
130
|
+
const inline = toks[i + 1];
|
|
131
|
+
out.push({
|
|
132
|
+
depth: Number(t.tag.slice(1)),
|
|
133
|
+
text: inline?.type === "inline" ? inline.content.trim() : "",
|
|
134
|
+
line: t.map[0],
|
|
135
|
+
offset: starts[t.map[0]] ?? 0,
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
return out;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** Offsets of the `---` thematic breaks (in this knowledge base — a prose separator). */
|
|
142
|
+
export function thematicBreaks(text) {
|
|
143
|
+
if (md === null) return [];
|
|
144
|
+
const starts = lineStarts(text);
|
|
145
|
+
return md
|
|
146
|
+
.parse(blankFrontmatter(text), {})
|
|
147
|
+
.filter((t) => t.type === "hr" && t.map)
|
|
148
|
+
.map((t) => starts[t.map[0]] ?? 0);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* A section's body: from the line AFTER the heading matching `match`, up to the next
|
|
153
|
+
* heading of THE SAME OR A LOWER level. `{ heading, body }` or `null`.
|
|
154
|
+
*
|
|
155
|
+
* `match` — a regex over the heading text OR a predicate over `{depth,text,line,offset}`.
|
|
156
|
+
*
|
|
157
|
+
* Level-or-lower, not "exactly `##`": a `###` subsection inside the section does not
|
|
158
|
+
* end it — that is exactly how the former expressions with the `(?=\n## )` lookahead
|
|
159
|
+
* behaved. A regex cannot know this distinction in principle: it has no notion of level.
|
|
160
|
+
*
|
|
161
|
+
* `depth` — the CEILING on the level of the heading itself. Without it an `### Abstract`
|
|
162
|
+
* quoted in an appendix becomes the abstract — and the abstract is judged with a
|
|
163
|
+
* threshold of ZERO.
|
|
164
|
+
*
|
|
165
|
+
* `stopAtRule` additionally cuts the section off at the first `---`. That is NOT markdown
|
|
166
|
+
* structure but a paper convention, and it is preserved deliberately: the conclusion has
|
|
167
|
+
* a `---` before the appendices, and without that boundary a section with no following
|
|
168
|
+
* `##` would reach to the end of the file.
|
|
169
|
+
*
|
|
170
|
+
* 🔴 A MERGED VERSION. On 2026-08-11 this function appeared SIMULTANEOUSLY in two places:
|
|
171
|
+
* here (for `kb-lint`) and in `paper-lint.mjs` (richer — with the level ceiling, the
|
|
172
|
+
* heading in the result and `stopAtRule`). Both were written within one hour by two
|
|
173
|
+
* parallel passes, neither saw the other. Merged into the rich version: the weak one was
|
|
174
|
+
* a subset, and keeping both would have meant diverging in behaviour at the very next edit.
|
|
175
|
+
*/
|
|
176
|
+
export function sectionBody(
|
|
177
|
+
text,
|
|
178
|
+
match,
|
|
179
|
+
{ depth = 6, stopAtRule = false } = {},
|
|
180
|
+
) {
|
|
181
|
+
const test = typeof match === "function" ? match : (h) => match.test(h.text);
|
|
182
|
+
const hs = headings(text);
|
|
183
|
+
const i = hs.findIndex((h) => h.depth <= depth && test(h));
|
|
184
|
+
if (i === -1) return null;
|
|
185
|
+
const h = hs[i];
|
|
186
|
+
const next = hs.slice(i + 1).find((n) => n.depth <= h.depth);
|
|
187
|
+
let end = next ? next.offset : text.length;
|
|
188
|
+
if (stopAtRule) {
|
|
189
|
+
const hr = thematicBreaks(text).find((o) => o > h.offset && o < end);
|
|
190
|
+
if (hr !== undefined) end = hr;
|
|
191
|
+
}
|
|
192
|
+
// The body starts on the line AFTER the heading: the heading itself is not part of the
|
|
193
|
+
// section, just as the former expressions' group did not capture it.
|
|
194
|
+
const nl = text.indexOf("\n", h.offset);
|
|
195
|
+
const start = nl === -1 || nl + 1 > end ? end : nl + 1;
|
|
196
|
+
return { heading: h, body: text.slice(start, end) };
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Slicing the document into chunks "a heading + everything up to the next heading FROM
|
|
201
|
+
* THE SAME RANGE of levels". → `[{ heading, raw, body }]`.
|
|
202
|
+
*
|
|
203
|
+
* This is exactly what four scripts did as `split(/^(?=## )/m)` and
|
|
204
|
+
* `split(/^(?=#{2,3} )/m)`. The shape of the expression lied about the intent: "two or
|
|
205
|
+
* three hashes" is about CHARACTERS, while what is needed is the LEVEL, and `#{2,3}`
|
|
206
|
+
* also caught a hash inside a ```-block, that is, it cut a quotation from someone
|
|
207
|
+
* else's paper as a section of its own.
|
|
208
|
+
*
|
|
209
|
+
* `heading` — `null` on the zeroth element (the preamble before the first heading). It
|
|
210
|
+
* is ALWAYS there, including empty: the former `split` also always returned the chunk
|
|
211
|
+
* before the first separator as its zeroth element, and callers rely on that
|
|
212
|
+
* (`sub === chunk` in `artifact-coverage` meant "there are no subsections").
|
|
213
|
+
*
|
|
214
|
+
* `raw` — WITH the heading line (it is used to count `§` references and bold numbers,
|
|
215
|
+
* which occur in the heading itself too), `body` — without it (it is used to count words).
|
|
216
|
+
*
|
|
217
|
+
* 2026-08-11: moved here rather than copied into every script, because the chunk
|
|
218
|
+
* boundary is the one place where an off-by-one is easy, and such a mistake has already
|
|
219
|
+
* happened (`recordBlock` in `skill-corpus.mjs` returned a single "#" character for all
|
|
220
|
+
* 21 skills).
|
|
221
|
+
*/
|
|
222
|
+
export function splitSections(text, { min = 2, max = 6 } = {}) {
|
|
223
|
+
const hs = headings(text).filter((h) => h.depth >= min && h.depth <= max);
|
|
224
|
+
const out = [];
|
|
225
|
+
const preEnd = hs.length ? hs[0].offset : text.length;
|
|
226
|
+
out.push({
|
|
227
|
+
heading: null,
|
|
228
|
+
raw: text.slice(0, preEnd),
|
|
229
|
+
body: text.slice(0, preEnd),
|
|
230
|
+
});
|
|
231
|
+
for (let i = 0; i < hs.length; i++) {
|
|
232
|
+
const h = hs[i];
|
|
233
|
+
const end = i + 1 < hs.length ? hs[i + 1].offset : text.length;
|
|
234
|
+
const nl = text.indexOf("\n", h.offset);
|
|
235
|
+
const bodyStart = nl === -1 || nl + 1 > end ? end : nl + 1;
|
|
236
|
+
out.push({
|
|
237
|
+
heading: h,
|
|
238
|
+
raw: text.slice(h.offset, end),
|
|
239
|
+
body: text.slice(bodyStart, end),
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
return out;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* The text without the contents of ```-blocks (the fence lines themselves go too).
|
|
247
|
+
*
|
|
248
|
+
* Through the parser, not `/```[\s\S]*?```/g`: that pattern glues the end of one block
|
|
249
|
+
* to the start of the next when the number of fences is odd, knows nothing about `~~~`
|
|
250
|
+
* fences or indented fences, and silently eats the prose between blocks.
|
|
251
|
+
*
|
|
252
|
+
* `blank: true` — the block's lines are NOT deleted but REPLACED with empty ones. The
|
|
253
|
+
* difference is not cosmetic: deleting the lines GLUES the paragraph before the block to
|
|
254
|
+
* the paragraph after it, and a code block is a block boundary — on the other side a
|
|
255
|
+
* different paragraph starts. Measured 2026-08-11 on `<papers-root>/<paper>/README.md`:
|
|
256
|
+
* `prose-lint` counted 82 sentences there, and after the lines were deleted it counted
|
|
257
|
+
* 81 — the lead paragraph "**Reproduction commands:**" merged with the text AFTER the
|
|
258
|
+
* block into one sentence. That is exactly the failure against which `prose-lint` itself
|
|
259
|
+
* says "Paragraph and heading boundaries END a sentence", and it inflates the
|
|
260
|
+
* claims-per-sentence counter — the threshold at which the file emits findings. The
|
|
261
|
+
* former regex preserved the boundary (it left one newline on each side), so `blank` is
|
|
262
|
+
* also a preservation of the former behaviour where that behaviour was right.
|
|
263
|
+
*/
|
|
264
|
+
/**
|
|
265
|
+
* The CONTENTS of fenced blocks — the other side of `stripFences`.
|
|
266
|
+
*
|
|
267
|
+
* Added 2026-09-08 for the check "a path as a command argument inside a ```-block"
|
|
268
|
+
* (the sixth form of a reference). A private regex for the fence there would have been
|
|
269
|
+
* the seventh instance of the class this knowledge base has already paid for:
|
|
270
|
+
* `/^```[\s\S]*?^```/gm` knows nothing about nested fences or about `~~~`, and the
|
|
271
|
+
* parser does — the `fence` token has a `content`.
|
|
272
|
+
*
|
|
273
|
+
* No parser — returns an empty list, and the caller must survive that: for hooks
|
|
274
|
+
* degradation is acceptable (see the module comment), while a silent fallback to a
|
|
275
|
+
* regex would mean a working degraded version that nobody notices.
|
|
276
|
+
*/
|
|
277
|
+
export function fences(text) {
|
|
278
|
+
if (md === null) return [];
|
|
279
|
+
const out = [];
|
|
280
|
+
for (const t of md.parse(blankFrontmatter(text), {}))
|
|
281
|
+
if (t.type === "fence" || t.type === "code_block") out.push(t.content);
|
|
282
|
+
return out;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
export function stripFences(text, { blank = false } = {}) {
|
|
286
|
+
if (md === null) return text;
|
|
287
|
+
const lines = text.split("\n");
|
|
288
|
+
const drop = new Set();
|
|
289
|
+
for (const t of md.parse(blankFrontmatter(text), {}))
|
|
290
|
+
if ((t.type === "fence" || t.type === "code_block") && t.map)
|
|
291
|
+
for (let i = t.map[0]; i < t.map[1]; i++) drop.add(i);
|
|
292
|
+
return blank
|
|
293
|
+
? lines.map((l, i) => (drop.has(i) ? "" : l)).join("\n")
|
|
294
|
+
: lines.filter((_, i) => !drop.has(i)).join("\n");
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* For SCRIPTS, not for hooks: fails if there is no parser.
|
|
299
|
+
*
|
|
300
|
+
* For a linter degradation is acceptable — it is advisory and can say so out loud. A
|
|
301
|
+
* skill script hands numbers to a pipeline gate, and an empty list of headings there is
|
|
302
|
+
* indistinguishable from "the paper has no sections". A confident zero has already been
|
|
303
|
+
* read as a finding four times in this repo; here it is inexpressible.
|
|
304
|
+
*/
|
|
305
|
+
export function requireMarkdown() {
|
|
306
|
+
if (MD_AVAILABLE()) return;
|
|
307
|
+
// 🔴 THE CURE DEPENDS ON THE DELIVERY CHANNEL, AND NAMING THE WRONG ONE WASTES THE ERROR.
|
|
308
|
+
// Measured 2026-09-19: `npm pack` strips `package-lock.json` unconditionally, and a plugin copy
|
|
309
|
+
// gets `npm ci` only when a lockfile is present in the fetched root — so a plugin installed from
|
|
310
|
+
// npm has NO `node_modules`, with no log entry anywhere. Telling that user to run `npm i at the
|
|
311
|
+
// repo root` points at a repository they do not have. The channel is readable from this file's
|
|
312
|
+
// own location, so the message can name it instead of guessing.
|
|
313
|
+
const fromPluginCache = import.meta.url.includes("/.claude/plugins/");
|
|
314
|
+
console.error(
|
|
315
|
+
"markdown-it does not resolve — this script parses markup with a parser and without it " +
|
|
316
|
+
"would produce a confident zero instead of an error.\n" +
|
|
317
|
+
(fromPluginCache
|
|
318
|
+
? " Running from a PLUGIN copy, which is installed without dependencies. Install the npm\n" +
|
|
319
|
+
" package in the project as well (`npm i paperlint`) and call it through\n" +
|
|
320
|
+
" `npx paperlint`, which resolves from the project's own tree."
|
|
321
|
+
: " Cured by `npm i` at the repo root."),
|
|
322
|
+
);
|
|
323
|
+
process.exit(2);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* GFM tables — rows, cells and the line range, WITH THE PARSER.
|
|
328
|
+
*
|
|
329
|
+
* The only consumer as of 2026-08-20 is `crosspost-article`, which needs to re-lay a
|
|
330
|
+
* table into a monospace block for a platform without tables (Medium). It lives here
|
|
331
|
+
* and not in the skill, by the rule "markup is parsed with a parser, and in one place":
|
|
332
|
+
* with a regex a table is told apart from a line of text with pipes only by its
|
|
333
|
+
* neighbours, and that is exactly the class that has already cost this repo 27 places.
|
|
334
|
+
*
|
|
335
|
+
* Returns `{ line, endLine, rows }`, where `rows[0]` is the header; the alignment row
|
|
336
|
+
* (`| --- |`) is thrown away, because in monospace output it has nothing to express.
|
|
337
|
+
*/
|
|
338
|
+
/**
|
|
339
|
+
* The document's paragraphs — `paragraph_open` line ranges, WITH THE PARSER.
|
|
340
|
+
* `[{ line, endLine }]`.
|
|
341
|
+
*
|
|
342
|
+
* The only consumer as of 2026-08-21 is `crosspost-article`: the blog's sources are
|
|
343
|
+
* hard-wrapped at a column (an editorial habit, not markdown), and the platforms render
|
|
344
|
+
* a single `\n` inside a paragraph DIFFERENTLY — dev.to (Redcarpet) keeps it as a
|
|
345
|
+
* literal line break, and the paragraph arrives torn into short lines exactly at the
|
|
346
|
+
* source file's boundaries. Medium has no such defect not because the platform is more
|
|
347
|
+
* honest, but because the paste goes through "Import a story" — the importer takes the
|
|
348
|
+
* ALREADY RENDERED page of the site (where the breaks were collapsed by the browser long
|
|
349
|
+
* ago), not the raw markdown.
|
|
350
|
+
*
|
|
351
|
+
* The range is EXACTLY the one `t.map` gives on `paragraph_open` — that is, a code
|
|
352
|
+
* fence, a heading, a table, a list (in a TIGHT list the item is not wrapped in a
|
|
353
|
+
* `paragraph_open` at all, it has no paragraph of its own) fall outside this function
|
|
354
|
+
* automatically, by the construction of the parser, and not by a separate line-type
|
|
355
|
+
* check.
|
|
356
|
+
*/
|
|
357
|
+
export function paragraphs(text) {
|
|
358
|
+
if (md === null) return [];
|
|
359
|
+
return md
|
|
360
|
+
.parse(blankFrontmatter(text), {})
|
|
361
|
+
.filter((t) => t.type === "paragraph_open" && t.map)
|
|
362
|
+
.map((t) => ({ line: t.map[0], endLine: t.map[1] }));
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
export function tables(text) {
|
|
366
|
+
if (md === null) return [];
|
|
367
|
+
const lines = blankFrontmatter(text).split("\n");
|
|
368
|
+
const out = [];
|
|
369
|
+
for (const t of md.parse(blankFrontmatter(text), {})) {
|
|
370
|
+
if (t.type !== "table_open" || !t.map) continue;
|
|
371
|
+
const rows = [];
|
|
372
|
+
for (let i = t.map[0]; i < t.map[1]; i++) {
|
|
373
|
+
const raw = lines[i];
|
|
374
|
+
if (/^\s*\|?[\s:|-]+\|?\s*$/.test(raw) && raw.includes("-")) continue; // alignment
|
|
375
|
+
rows.push(
|
|
376
|
+
raw
|
|
377
|
+
.replace(/^\s*\|/, "")
|
|
378
|
+
.replace(/\|\s*$/, "")
|
|
379
|
+
.split("|")
|
|
380
|
+
.map((c) => c.trim()),
|
|
381
|
+
);
|
|
382
|
+
}
|
|
383
|
+
out.push({ line: t.map[0], endLine: t.map[1], rows });
|
|
384
|
+
}
|
|
385
|
+
return out;
|
|
386
|
+
}
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `runMutations` — three harness outcomes, not two.
|
|
3
|
+
*
|
|
4
|
+
* ── WHY THIS FILE SHOWED UP LAST, NOT FIRST ────────────────────────────
|
|
5
|
+
* The engine that renders a verdict for EVERY battery in the repository was, until 2026-09-15,
|
|
6
|
+
* checked by nothing. Every one of its defenses (a harness path pointing at nothing · a no-op
|
|
7
|
+
* mutation · retrying a non-kill · a baseline of already-red harnesses) was written AFTER it
|
|
8
|
+
* caught a real defect, and none of them had a test. So the next defect of the same class
|
|
9
|
+
* arrived in CI and was read as "two mutations survived".
|
|
10
|
+
*
|
|
11
|
+
* The third outcome — SKIP — is pinned down here. The fixtures are real: the skipping harness
|
|
12
|
+
* calls `skip()` FROM vigiles rather than faking it with exit code 77 by hand, because the
|
|
13
|
+
* latter would test my mental model of the protocol instead of the protocol itself.
|
|
14
|
+
*/
|
|
15
|
+
import assert from "node:assert/strict";
|
|
16
|
+
import {
|
|
17
|
+
mkdtempSync,
|
|
18
|
+
writeFileSync,
|
|
19
|
+
readFileSync,
|
|
20
|
+
rmSync,
|
|
21
|
+
realpathSync,
|
|
22
|
+
} from "node:fs";
|
|
23
|
+
import { tmpdir } from "node:os";
|
|
24
|
+
import { join } from "node:path";
|
|
25
|
+
import { createRequire } from "node:module";
|
|
26
|
+
import { pathToFileURL } from "node:url";
|
|
27
|
+
import { recordCheck } from "vigiles";
|
|
28
|
+
import { runMutations } from "./mutation-driver.mjs";
|
|
29
|
+
|
|
30
|
+
// The fixture lives in a temp directory where `node` will not find `vigiles` by walking up
|
|
31
|
+
// the tree. The specifier is resolved HERE, by node's own resolver, and substituted in
|
|
32
|
+
// absolute — that way the skip is done by a real `skip()`, not an imitation via exit code 77.
|
|
33
|
+
// An imitation would test my mental model of the protocol instead of the protocol itself.
|
|
34
|
+
const VIGILES = pathToFileURL(
|
|
35
|
+
createRequire(import.meta.url).resolve("vigiles"),
|
|
36
|
+
).href;
|
|
37
|
+
|
|
38
|
+
const SUBJECT = "export const MARKER = 'keep-me';\n";
|
|
39
|
+
const cases = [];
|
|
40
|
+
|
|
41
|
+
/** Temp root: a fixture subject + the harness that judges it. */
|
|
42
|
+
function fixture(harnessBody) {
|
|
43
|
+
const root = realpathSync(mkdtempSync(join(tmpdir(), "mutdrv-")));
|
|
44
|
+
const subject = join(root, "subject.mjs");
|
|
45
|
+
const harness = join(root, "probe.harness.mjs");
|
|
46
|
+
writeFileSync(subject, SUBJECT);
|
|
47
|
+
writeFileSync(harness, harnessBody(subject));
|
|
48
|
+
return { root, subject, harness };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const quiet = { log: console.log, error: console.error };
|
|
52
|
+
/** The verdict is read from the exit code and output, so the driver's output is intercepted. */
|
|
53
|
+
function silently(fn) {
|
|
54
|
+
const lines = [];
|
|
55
|
+
console.log = console.error = (...a) => lines.push(a.join(" "));
|
|
56
|
+
try {
|
|
57
|
+
return { code: fn(), out: lines.join("\n") };
|
|
58
|
+
} finally {
|
|
59
|
+
console.log = quiet.log;
|
|
60
|
+
console.error = quiet.error;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ── 1. THE HARNESS SKIPS ⇒ the battery REFUSES, rather than declaring the mutation survived.
|
|
65
|
+
// Observed failure (run 34966606186): CI had no `claude` binary, the harness skipped,
|
|
66
|
+
// `vigiles test` returned 0, and the driver printed "🔴 SURVIVED" — a claim about the TEST
|
|
67
|
+
// that was really a claim about the ENVIRONMENT.
|
|
68
|
+
{
|
|
69
|
+
const { root, subject, harness } = fixture(
|
|
70
|
+
() =>
|
|
71
|
+
`import { skip } from ${JSON.stringify(VIGILES)};\nskip("fixture: the capability this tier observes is absent");\n`,
|
|
72
|
+
);
|
|
73
|
+
try {
|
|
74
|
+
const before = readFileSync(subject, "utf8");
|
|
75
|
+
const { code, out } = silently(() =>
|
|
76
|
+
runMutations({
|
|
77
|
+
root,
|
|
78
|
+
runner: "node",
|
|
79
|
+
cases: [
|
|
80
|
+
{
|
|
81
|
+
name: "marker removed",
|
|
82
|
+
harness,
|
|
83
|
+
expect: "MARKER",
|
|
84
|
+
disables: "fixture",
|
|
85
|
+
edits: [[subject, "keep-me", "gone"]],
|
|
86
|
+
},
|
|
87
|
+
],
|
|
88
|
+
}),
|
|
89
|
+
);
|
|
90
|
+
assert.equal(code, 2, `a skip must REFUSE, not judge; got ${code}\n${out}`);
|
|
91
|
+
assert.doesNotMatch(
|
|
92
|
+
out,
|
|
93
|
+
/SURVIVED/,
|
|
94
|
+
"and not claim a survived mutation — that's a false diagnosis",
|
|
95
|
+
);
|
|
96
|
+
assert.match(
|
|
97
|
+
out,
|
|
98
|
+
/SKIP on clean source/,
|
|
99
|
+
"the refusal must name the cause",
|
|
100
|
+
);
|
|
101
|
+
assert.match(
|
|
102
|
+
out,
|
|
103
|
+
/probe\.harness\.mjs/,
|
|
104
|
+
"and name WHICH harness is skipping",
|
|
105
|
+
);
|
|
106
|
+
assert.equal(
|
|
107
|
+
readFileSync(subject, "utf8"),
|
|
108
|
+
before,
|
|
109
|
+
"the refusal must happen BEFORE any file is edited — the message arrives with a clean tree",
|
|
110
|
+
);
|
|
111
|
+
cases.push(
|
|
112
|
+
"harness skips → refusal before editing files, no verdict about the mutation",
|
|
113
|
+
);
|
|
114
|
+
} finally {
|
|
115
|
+
rmSync(root, { recursive: true, force: true });
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// ── 2. THE HARNESS WORKS ⇒ the prior behavior is intact. Without this half, case 1 is
|
|
120
|
+
// indistinguishable from an engine that just always refuses.
|
|
121
|
+
{
|
|
122
|
+
const { root, subject, harness } = fixture(
|
|
123
|
+
(s) =>
|
|
124
|
+
`import assert from "node:assert/strict";\nimport { readFileSync } from "node:fs";\nassert.match(readFileSync(${JSON.stringify(s)}, "utf8"), /keep-me/, "MARKER is gone");\n`,
|
|
125
|
+
);
|
|
126
|
+
try {
|
|
127
|
+
const { code, out } = silently(() =>
|
|
128
|
+
runMutations({
|
|
129
|
+
root,
|
|
130
|
+
runner: "node",
|
|
131
|
+
cases: [
|
|
132
|
+
{
|
|
133
|
+
name: "marker removed",
|
|
134
|
+
harness,
|
|
135
|
+
expect: "MARKER is gone",
|
|
136
|
+
disables: "fixture",
|
|
137
|
+
edits: [[subject, "keep-me", "gone"]],
|
|
138
|
+
},
|
|
139
|
+
],
|
|
140
|
+
}),
|
|
141
|
+
);
|
|
142
|
+
assert.equal(
|
|
143
|
+
code,
|
|
144
|
+
0,
|
|
145
|
+
`a working harness must kill the mutation; got ${code}\n${out}`,
|
|
146
|
+
);
|
|
147
|
+
assert.match(
|
|
148
|
+
out,
|
|
149
|
+
/RED \(expected case\)/,
|
|
150
|
+
"and kill it with ITS OWN message, not just any red",
|
|
151
|
+
);
|
|
152
|
+
assert.equal(
|
|
153
|
+
readFileSync(subject, "utf8"),
|
|
154
|
+
SUBJECT,
|
|
155
|
+
"the source must be restored",
|
|
156
|
+
);
|
|
157
|
+
cases.push(
|
|
158
|
+
"harness works → mutation killed by its own assertion, source restored",
|
|
159
|
+
);
|
|
160
|
+
} finally {
|
|
161
|
+
rmSync(root, { recursive: true, force: true });
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// ── 3. THE PATCH NEVER LANDED ⇒ that is NOT a survived mutation. The third outcome, and until
|
|
166
|
+
// 09-17 the engine dumped it into the second one: anything that was not `RED (expected` counted
|
|
167
|
+
// as survived — including NOT FOUND, AMBIGUOUS and NO-OP, where the file was never edited at all.
|
|
168
|
+
//
|
|
169
|
+
// 🔴 The two verdicts send the reader in OPPOSITE directions: survived means STRENGTHEN THE
|
|
170
|
+
// TEST, unusable means FIX THE MUTATION. Observed the same day: a `find` string went stale
|
|
171
|
+
// after the rule was rewritten, the engine reported "1 mutation survived", and the next step
|
|
172
|
+
// would have been rewriting a harness that was fine.
|
|
173
|
+
{
|
|
174
|
+
const { root, subject, harness } = fixture(
|
|
175
|
+
(s) =>
|
|
176
|
+
`import assert from "node:assert/strict";\nimport { readFileSync } from "node:fs";\nassert.match(readFileSync(${JSON.stringify(s)}, "utf8"), /keep-me/, "MARKER is gone");\n`,
|
|
177
|
+
);
|
|
178
|
+
try {
|
|
179
|
+
const { code, out } = silently(() =>
|
|
180
|
+
runMutations({
|
|
181
|
+
root,
|
|
182
|
+
runner: "node",
|
|
183
|
+
cases: [
|
|
184
|
+
{
|
|
185
|
+
name: "stale target",
|
|
186
|
+
harness,
|
|
187
|
+
expect: "MARKER is gone",
|
|
188
|
+
disables: "fixture",
|
|
189
|
+
// The string is not in the file — exactly the case of a stale target.
|
|
190
|
+
edits: [[subject, "no-such-string-anywhere", "x"]],
|
|
191
|
+
},
|
|
192
|
+
],
|
|
193
|
+
}),
|
|
194
|
+
);
|
|
195
|
+
assert.equal(
|
|
196
|
+
code,
|
|
197
|
+
1,
|
|
198
|
+
`an unusable mutation must REFUSE the run; got ${code}\n${out}`,
|
|
199
|
+
);
|
|
200
|
+
assert.match(
|
|
201
|
+
out,
|
|
202
|
+
/UNUSABLE/,
|
|
203
|
+
"the verdict must be named unusable, not survived",
|
|
204
|
+
);
|
|
205
|
+
assert.match(
|
|
206
|
+
out,
|
|
207
|
+
/NOT FOUND/,
|
|
208
|
+
"and name the CAUSE — what exactly was not found",
|
|
209
|
+
);
|
|
210
|
+
assert.doesNotMatch(
|
|
211
|
+
out,
|
|
212
|
+
/mutation\(s\) survived/,
|
|
213
|
+
"and must NOT claim a survived mutation: that would send someone to fix a working test",
|
|
214
|
+
);
|
|
215
|
+
cases.push(
|
|
216
|
+
'patch never landed → UNUSABLE with a cause, not "mutation survived"',
|
|
217
|
+
);
|
|
218
|
+
} finally {
|
|
219
|
+
rmSync(root, { recursive: true, force: true });
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Report to the tool, or the surface counts as UNCOVERED despite a working test —
|
|
224
|
+
// "0 checks" and "no test" look identical in the ledger.
|
|
225
|
+
recordCheck(cases.length);
|
|
226
|
+
console.log(`mutation-driver: ${cases.length} outcome(s) pinned down:`);
|
|
227
|
+
for (const c of cases) console.log(` ok ${c}`);
|