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,338 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* generated-code.mjs โ lint the analysis scripts BEFORE they produce a number.
|
|
4
|
+
*
|
|
5
|
+
* node generated-code.mjs <paper-dir> report
|
|
6
|
+
* node generated-code.mjs <paper-dir> --flags-only for hooks and pre-commit
|
|
7
|
+
*
|
|
8
|
+
* Ported from `check_generated_code.py` in MedSci Skills (Nam, Jeong & Kim, arXiv:2606.09500v4,
|
|
9
|
+
* tag v3.8.0), one of the twenty-one deterministic detectors that paper ships. It asks three
|
|
10
|
+
* questions of a script a model wrote for you:
|
|
11
|
+
*
|
|
12
|
+
* NO_SEED it draws randomness and never seeds it, so its result is not the same twice;
|
|
13
|
+
* ABS_PATH it hard-codes an absolute path, so it runs on exactly one machine;
|
|
14
|
+
* IN_PLACE it opens a file for reading and later overwrites that same file, so the input to a
|
|
15
|
+
* re-run is the output of the last one and the original is gone.
|
|
16
|
+
*
|
|
17
|
+
* โ ๏ธ ON THE SOURCE. That paper's headline (27/27 deterministic versus 11/27 for an LLM reviewer)
|
|
18
|
+
* does not survive its own artifact โ seven of the twenty-seven defects were in files the model was
|
|
19
|
+
* never shown, and one of its eleven hits is a substring match on `random` in a sentence about
|
|
20
|
+
* random forests. Honest: 10/27, and 10/20 on what the model could see. See
|
|
21
|
+
* `the author's private research notes` ยง2.4. None of that
|
|
22
|
+
* touches whether these three questions are worth asking; they are, and nothing here asked them.
|
|
23
|
+
*
|
|
24
|
+
* โโ ๐ด THE BOUNDARY WITH check-anon.sh, WHICH IS NOT A DUPLICATE โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
25
|
+
*
|
|
26
|
+
* `repro/check-anon.sh` category 5 greps for `/home/`, `/Users/`, `/workspace/`, `/root/` too, and
|
|
27
|
+
* the two look identical until you ask WHAT each one reads and WHEN:
|
|
28
|
+
*
|
|
29
|
+
* check-anon.sh cat. 5 โ reads the SHIPPED BUNDLE (`repro/artifact-anon/`) at release time, and
|
|
30
|
+
* its question is DISCLOSURE: does the reviewer's copy leak the author's
|
|
31
|
+
* machine layout and therefore their identity? A hard-coded path that is
|
|
32
|
+
* never shipped is none of its business.
|
|
33
|
+
* this check โ reads the WORKING TREE's generated scripts, before they run, and its
|
|
34
|
+
* question is REPRODUCIBILITY: will this script do anything at all on
|
|
35
|
+
* somebody else's disk? A path scrubbed out of the bundle by hand is
|
|
36
|
+
* still a script only one machine can execute.
|
|
37
|
+
*
|
|
38
|
+
* They therefore scan DISJOINT trees on purpose: the bundle is excluded here, and this exclusion is
|
|
39
|
+
* printed in the ignore ledger below rather than being a silent line in a glob. If they overlapped,
|
|
40
|
+
* one of them should be deleted.
|
|
41
|
+
*
|
|
42
|
+
* ๐ด AND THE SAME FALSE-POSITIVE DISCIPLINE AS THE REVERSE COVERAGE LEG. `repro/` is mostly not
|
|
43
|
+
* analysis code, so an ignore set is unavoidable โ and an ignore set applied in silence is how a
|
|
44
|
+
* check becomes a green light. Every structural rule prints the names it swallowed, and
|
|
45
|
+
* `repro/generated-code-grandfathered.txt` prints every row and every reason, on every run,
|
|
46
|
+
* findings or none. Same reasoning as `check_grandfather` in `repro/paper_numbers.py`.
|
|
47
|
+
*/
|
|
48
|
+
import { readFileSync, existsSync, readdirSync, statSync } from "node:fs";
|
|
49
|
+
import { join, relative } from "node:path";
|
|
50
|
+
|
|
51
|
+
const args = process.argv.slice(2);
|
|
52
|
+
const dir = args.find((a) => !a.startsWith("--"));
|
|
53
|
+
const flagsOnly = args.includes("--flags-only");
|
|
54
|
+
if (!dir) {
|
|
55
|
+
console.error("usage: node generated-code.mjs <paper-dir> [--flags-only]");
|
|
56
|
+
process.exit(0);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// โโ WHERE repro/ IS โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
60
|
+
// ๐ด `repro/` is NOT always directly under the paper directory. `<paper-e>` keeps it at
|
|
61
|
+
// `typed-shell/repro/`, and the first version of this line was `join(dir, 'repro')` followed by a
|
|
62
|
+
// bare `process.exit(0)`. Measured 2026-08-26: run against that paper's own directory this check
|
|
63
|
+
// printed NOTHING and exited 0 โ indistinguishable from a clean paper โ while EIGHTEEN ABS_PATH
|
|
64
|
+
// findings sat one level below, among them `<papers-root>/<paper-c>/โฆ`
|
|
65
|
+
// hard-coded inside a reproduction script. Same class the `numbers` job documents about itself
|
|
66
|
+
// (`for d in <papers-root>/*/repro`) and the same class as the `artifact/reproduce.py` skip:
|
|
67
|
+
// A GUARD THAT TESTS FOR A FILENAME RATHER THAN FOR THE JOB.
|
|
68
|
+
//
|
|
69
|
+
// Two changes, and the second matters more than the first: the root is now also looked for one
|
|
70
|
+
// level down, and "no repro/ anywhere" SAYS SO instead of exiting in silence. Silence has to mean
|
|
71
|
+
// "scanned and clean", never "did not look".
|
|
72
|
+
const nested = existsSync(dir)
|
|
73
|
+
? readdirSync(dir, { withFileTypes: true })
|
|
74
|
+
.filter(
|
|
75
|
+
(e) =>
|
|
76
|
+
e.isDirectory() &&
|
|
77
|
+
!e.name.startsWith(".") &&
|
|
78
|
+
e.name !== "node_modules",
|
|
79
|
+
)
|
|
80
|
+
.map((e) => join(dir, e.name, "repro"))
|
|
81
|
+
.filter(existsSync)
|
|
82
|
+
: [];
|
|
83
|
+
const roots = [join(dir, "repro"), ...nested].filter(existsSync);
|
|
84
|
+
if (!roots.length) {
|
|
85
|
+
if (!flagsOnly)
|
|
86
|
+
console.log(
|
|
87
|
+
`๐งช generated-code โ 0 finding(s) in ${dir}\n NOTHING WAS SCANNED: no repro/ here or one level below. ` +
|
|
88
|
+
"That is not the same as clean, and this line exists because the silent exit that used to stand here hid 18 findings",
|
|
89
|
+
);
|
|
90
|
+
process.exit(0);
|
|
91
|
+
}
|
|
92
|
+
const repro = roots[0];
|
|
93
|
+
|
|
94
|
+
// โโ scope โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
95
|
+
// Depth 3 under repro/ is where this project's analysis code lives: `repro/<experiment>/script.mjs`
|
|
96
|
+
// and `repro/<experiment>/lib/โฆ`, plus `repro/<experiment>/generated/โฆ`, which holds the checkers a
|
|
97
|
+
// model synthesised and is the most literal reading of "generated code" there is. Below that are
|
|
98
|
+
// fixtures, working clones and raw transcripts; going deeper buys tens of thousands of files and no
|
|
99
|
+
// analysis. This is a SCOPE and therefore a blind spot, and it is printed as one.
|
|
100
|
+
const EXT = /\.(py|mjs|cjs|js)$/;
|
|
101
|
+
const MAX_DEPTH = 3;
|
|
102
|
+
const swallowed = new Map([
|
|
103
|
+
[
|
|
104
|
+
"the released bundle โ `check-anon.sh` cat. 5 owns it, and it asks a different question there " +
|
|
105
|
+
"(does it leak an identity, not will it run elsewhere)",
|
|
106
|
+
[],
|
|
107
|
+
],
|
|
108
|
+
[
|
|
109
|
+
"dependency, VCS and build trees (node_modules, .git, __pycache__, dot-directories)",
|
|
110
|
+
[],
|
|
111
|
+
],
|
|
112
|
+
[
|
|
113
|
+
`nested deeper than ${MAX_DEPTH} levels under repro/ โ fixtures, working clones, raw transcripts`,
|
|
114
|
+
[],
|
|
115
|
+
],
|
|
116
|
+
// The allow file is per repro root, so this check reads ONE. A paper carrying two would have the
|
|
117
|
+
// second read by nobody, and an unread tree has to be a name you can count, not an absence.
|
|
118
|
+
[
|
|
119
|
+
"a SECOND repro/ root under this paper directory โ this check reads one, and the allow file is " +
|
|
120
|
+
"scoped to it; run the check against that directory to cover the other",
|
|
121
|
+
[],
|
|
122
|
+
],
|
|
123
|
+
]);
|
|
124
|
+
const RULE = [...swallowed.keys()];
|
|
125
|
+
for (const other of roots.slice(1))
|
|
126
|
+
swallowed.get(RULE[3]).push(relative(dir, other));
|
|
127
|
+
const bundleName = ["artifact-anon", "artifact"].find((b) =>
|
|
128
|
+
existsSync(join(repro, b)),
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
const files = [];
|
|
132
|
+
(function walk(abs, depth) {
|
|
133
|
+
for (const name of readdirSync(abs)) {
|
|
134
|
+
const p = join(abs, name);
|
|
135
|
+
let st;
|
|
136
|
+
try {
|
|
137
|
+
st = statSync(p);
|
|
138
|
+
} catch {
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
if (st.isDirectory()) {
|
|
142
|
+
if (depth === 1 && name === bundleName) {
|
|
143
|
+
swallowed.get(RULE[0]).push(name);
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
if (
|
|
147
|
+
name.startsWith(".") ||
|
|
148
|
+
name === "node_modules" ||
|
|
149
|
+
name === "__pycache__"
|
|
150
|
+
) {
|
|
151
|
+
swallowed.get(RULE[1]).push(relative(repro, p));
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
if (depth >= MAX_DEPTH) {
|
|
155
|
+
swallowed.get(RULE[2]).push(relative(repro, p));
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
walk(p, depth + 1);
|
|
159
|
+
} else if (EXT.test(name)) files.push(p);
|
|
160
|
+
}
|
|
161
|
+
})(repro, 1);
|
|
162
|
+
|
|
163
|
+
// โโ the allow file โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
164
|
+
const allowPath = join(repro, "generated-code-grandfathered.txt");
|
|
165
|
+
const allowed = new Map();
|
|
166
|
+
if (existsSync(allowPath)) {
|
|
167
|
+
for (const line of readFileSync(allowPath, "utf8").split("\n")) {
|
|
168
|
+
const row = line.split("#")[0].trimEnd();
|
|
169
|
+
if (!row.trim()) continue;
|
|
170
|
+
// The key is TWO fields, path and kind. The first draft took only the first field, so every
|
|
171
|
+
// row silenced nothing and the two real allowances came back as findings AND as stale rows โ
|
|
172
|
+
// caught because the run printed both at once, which is the argument for printing the ledger.
|
|
173
|
+
const [path, kind, ...rest] = row.split("\t").map((c) => c.trim());
|
|
174
|
+
if (path && kind)
|
|
175
|
+
allowed.set(
|
|
176
|
+
`${path}\t${kind}`,
|
|
177
|
+
rest.filter(Boolean).join(" โ ") || "(NO REASON GIVEN)",
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// โโ the three rules โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
183
|
+
// Whole-line comments are stripped first. Not inline ones: `p = "/home/me/x" # fixme` is still a
|
|
184
|
+
// hard-coded path, and a rule that lets a trailing comment disarm it is worse than no rule.
|
|
185
|
+
const strip = (s) => s.replace(/^\s*#.*$/gm, "").replace(/^\s*\/\/.*$/gm, "");
|
|
186
|
+
|
|
187
|
+
const DRAWS =
|
|
188
|
+
/(?:\brandom\.(?:random|sample|shuffle|choice|choices|randint|randrange|uniform)|\bnp\.random\.|\bnumpy\.random\.|\bMath\.random\s*\(|\brandom_state\b|\bcrypto\.randomBytes\s*\()/;
|
|
189
|
+
// ๐ด NO LEADING \b ON THIS ALTERNATION. The first draft opened with `\b(?:โฆ|--seed|โฆ)`, and `\b`
|
|
190
|
+
// before a hyphen never matches after a quote โ so `"--seed"` in an argparse call was invisible and
|
|
191
|
+
// `spotcheck-census.py`, which seeds properly and takes `--seed` on the command line, was reported.
|
|
192
|
+
// Found by reading the one file the rule accused, which is the only way this class is ever found.
|
|
193
|
+
const SEEDS =
|
|
194
|
+
/(?:random\.seed|np\.random\.seed|numpy\.random\.seed|default_rng\s*\(\s*\d|\bseed\s*=\s*\d|--seed|\bSEED\b|Random\s*\(\s*[\w.]+\s*\)|seedrandom|mulberry32|splitmix)/;
|
|
195
|
+
// Anchored and case-sensitive, for the reasons check-anon.sh cat. 5 documents at length: without
|
|
196
|
+
// the anchor a third party's relative `src/workspace/x` matches, and without case-sensitivity an
|
|
197
|
+
// Express route `/users/export` matches `/Users/`.
|
|
198
|
+
const ABS =
|
|
199
|
+
/(?<![\w./-])(?:\/home\/|\/Users\/|\/workspace\/|\/root\/|[A-Z]:\\\\?[A-Za-z])/;
|
|
200
|
+
|
|
201
|
+
// IN_PLACE is deliberately LITERAL โ the same quoted path, or the same argv element, read and then
|
|
202
|
+
// written. It is a textual check and not dataflow: a path held in a variable is invisible to it.
|
|
203
|
+
// The first draft matched the OPERAND EXPRESSION instead (`open(p)` โฆ `open(p, "w")`) and produced
|
|
204
|
+
// seventeen hits on this repository, essentially all of them a variable named `file` or `p` reused
|
|
205
|
+
// across two unrelated loops, plus `path.join(HERE` truncated at the comma by the regex. A checker
|
|
206
|
+
// whose findings are mostly noise is muted within a day; precision is the only affordable choice
|
|
207
|
+
// here, and the recall it costs is written down in the blind-spot list at the bottom.
|
|
208
|
+
const READS = [
|
|
209
|
+
/open\(\s*(['"])([^'"]+)\1\s*(?:,\s*(['"])r[b+]*\3\s*)?\)/g,
|
|
210
|
+
/readFileSync\(\s*(['"])([^'"]+)\1/g,
|
|
211
|
+
/Path\(\s*(['"])([^'"]+)\1\s*\)\s*\.read/g,
|
|
212
|
+
/open\(\s*((?:sys\.argv|process\.argv)\[\d\])\s*\)/g,
|
|
213
|
+
];
|
|
214
|
+
const WRITES = [
|
|
215
|
+
/open\(\s*(['"])([^'"]+)\1\s*,\s*['"][wa]/g,
|
|
216
|
+
/writeFileSync\(\s*(['"])([^'"]+)\1/g,
|
|
217
|
+
/Path\(\s*(['"])([^'"]+)\1\s*\)\s*\.write/g,
|
|
218
|
+
/open\(\s*((?:sys\.argv|process\.argv)\[\d\])\s*,\s*['"][wa]/g,
|
|
219
|
+
];
|
|
220
|
+
const operands = (code, res) => {
|
|
221
|
+
const s = new Set();
|
|
222
|
+
for (const re of res) for (const m of code.matchAll(re)) s.add(m[2] ?? m[1]);
|
|
223
|
+
return s;
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
const findings = [];
|
|
227
|
+
const ignored = [];
|
|
228
|
+
const hitFiles = new Set();
|
|
229
|
+
for (const f of files.sort()) {
|
|
230
|
+
const rel = relative(dir, f);
|
|
231
|
+
const src = readFileSync(f, "utf8");
|
|
232
|
+
const code = strip(src);
|
|
233
|
+
const raise = (kind, detail) => {
|
|
234
|
+
const key = `${relative(repro, f)}\t${kind}`;
|
|
235
|
+
// An allow row is per FILE AND KIND: silencing `ABS_PATH` in a script must not also silence a
|
|
236
|
+
// missing seed in it. A bare filename row silences the file entirely and is spelled that way.
|
|
237
|
+
const reason = allowed.get(key) ?? allowed.get(`${relative(repro, f)}\t*`);
|
|
238
|
+
if (reason !== undefined) {
|
|
239
|
+
ignored.push([`${relative(repro, f)} ยท ${kind}`, reason]);
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
hitFiles.add(rel);
|
|
243
|
+
findings.push(`${kind.padEnd(9)} ${rel} โ ${detail}`);
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
if (DRAWS.test(code) && !SEEDS.test(src))
|
|
247
|
+
raise(
|
|
248
|
+
"NO_SEED",
|
|
249
|
+
`draws randomness (\`${code.match(DRAWS)[0]}\`) and never seeds it: two runs, two answers, ` +
|
|
250
|
+
"and the number in the paper came from one of them",
|
|
251
|
+
);
|
|
252
|
+
|
|
253
|
+
// ๐ด `ABS.flags`, not a literal 'g'. The first version passed 'g' alone, which silently DROPPED
|
|
254
|
+
// the case-sensitivity that keeps an Express route `/users/export` from matching `/Users/` โ
|
|
255
|
+
// so the property the harness asserts lived nowhere the harness could reach it. Found by the
|
|
256
|
+
// mutations file: adding an `i` to ABS changed no verdict, i.e. a mutation that did not mutate.
|
|
257
|
+
const abs = code.match(
|
|
258
|
+
new RegExp(ABS.source + "[^\\s'\"`)\\],]*", ABS.flags + "g"),
|
|
259
|
+
);
|
|
260
|
+
if (abs)
|
|
261
|
+
raise(
|
|
262
|
+
"ABS_PATH",
|
|
263
|
+
`hard-codes ${abs.length} absolute path(s), e.g. \`${abs[0]}\` โ it runs on one machine. ` +
|
|
264
|
+
"Distinct from check-anon cat. 5, which asks whether the SHIPPED bundle leaks a layout",
|
|
265
|
+
);
|
|
266
|
+
|
|
267
|
+
const both = [...operands(code, READS)].filter((p) =>
|
|
268
|
+
operands(code, WRITES).has(p),
|
|
269
|
+
);
|
|
270
|
+
if (both.length)
|
|
271
|
+
raise(
|
|
272
|
+
"IN_PLACE",
|
|
273
|
+
`reads and then overwrites \`${both.join("`, `")}\` โ after one run the input is gone and ` +
|
|
274
|
+
"a re-run is computing on its own output",
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// stale allowances, the same property `check_grandfather` keeps: a list that never shrinks is a list
|
|
279
|
+
// nobody reads.
|
|
280
|
+
const live = new Set();
|
|
281
|
+
for (const f of files)
|
|
282
|
+
for (const k of ["NO_SEED", "ABS_PATH", "IN_PLACE", "*"])
|
|
283
|
+
live.add(`${relative(repro, f)}\t${k}`);
|
|
284
|
+
for (const key of allowed.keys())
|
|
285
|
+
if (!live.has(key))
|
|
286
|
+
findings.push(
|
|
287
|
+
`STALE_ALLOW the allow row \`${key.replace("\t", " ยท ")}\` names no script under repro/ โ ` +
|
|
288
|
+
"retire it rather than leave it to rot",
|
|
289
|
+
);
|
|
290
|
+
|
|
291
|
+
// โโ output โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
292
|
+
const ledger = [
|
|
293
|
+
"",
|
|
294
|
+
` โโ what this check ignored, in full (${files.length} scripts scanned under repro/) โโ`,
|
|
295
|
+
" Printed on every run, findings or none. An ignore set nobody sees turns a check into a green",
|
|
296
|
+
" light; the uncovered set has to be a list you can count. Same discipline as check_grandfather",
|
|
297
|
+
" in repro/paper_numbers.py.",
|
|
298
|
+
];
|
|
299
|
+
for (const [rule, names] of swallowed) {
|
|
300
|
+
ledger.push(` โข ${names.length} tree(s) not entered โ ${rule}`);
|
|
301
|
+
if (names.length)
|
|
302
|
+
ledger.push(
|
|
303
|
+
` ${names.slice(0, 8).join(", ")}${names.length > 8 ? ` โฆ and ${names.length - 8} more` : ""}`,
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
ledger.push(
|
|
307
|
+
` โข ${ignored.length} row(s) in repro/generated-code-grandfathered.txt โ every row, every reason:`,
|
|
308
|
+
);
|
|
309
|
+
for (const [key, reason] of ignored) ledger.push(` ${key} โ ${reason}`);
|
|
310
|
+
if (!ignored.length) ledger.push(" (nothing is being waved through)");
|
|
311
|
+
ledger.push(
|
|
312
|
+
` โข ${hitFiles.size} script(s) reported above.`,
|
|
313
|
+
"",
|
|
314
|
+
" ๐ด WHAT THIS CHECK IS SILENT ABOUT:",
|
|
315
|
+
' โ a path held in a VARIABLE. IN_PLACE matches literals and argv elements only; `p = f"{d}/x";',
|
|
316
|
+
' open(p); open(p, "w")` passes. Dataflow would catch it and would also have produced seventeen',
|
|
317
|
+
" false positives on this repository, which is why it is not here.",
|
|
318
|
+
" โ a seed that is set but not RECORDED. A script can seed from the clock and satisfy NO_SEED.",
|
|
319
|
+
" โ anything below the depth limit, and any language that is not Python or JavaScript.",
|
|
320
|
+
" โ WHETHER THE SCRIPT IS CORRECT. Every rule here is about whether it runs the same way twice,",
|
|
321
|
+
" which is a different and much weaker question.",
|
|
322
|
+
);
|
|
323
|
+
|
|
324
|
+
const header = `๐งช generated-code โ ${findings.length} finding(s) in ${dir}`;
|
|
325
|
+
if (findings.length) {
|
|
326
|
+
const out = flagsOnly ? console.error : console.log;
|
|
327
|
+
out(header);
|
|
328
|
+
findings.forEach((f) => out(" " + f));
|
|
329
|
+
ledger.forEach((l) => out(l));
|
|
330
|
+
process.exit(1);
|
|
331
|
+
}
|
|
332
|
+
if (!flagsOnly) {
|
|
333
|
+
console.log(header);
|
|
334
|
+
console.log(
|
|
335
|
+
" every analysis script seeds its randomness, names only relative paths, and does not clobber its input",
|
|
336
|
+
);
|
|
337
|
+
ledger.forEach((l) => console.log(l));
|
|
338
|
+
}
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* generated-code.mutations.mjs โ proof that every rule and every guard in `generated-code.mjs` is
|
|
3
|
+
* watched FAILING. Run: `node .claude/skills/paper-pipeline/scripts/generated-code.mutations.mjs`
|
|
4
|
+
*
|
|
5
|
+
* This is not a test. It is the evidence behind the claim that the tests test something.
|
|
6
|
+
*
|
|
7
|
+
* WHY IT MATTERS MORE HERE THAN FOR A NORMAL CHECKER. Half of what this file asserts is that the
|
|
8
|
+
* checker STAYS QUIET โ on a seeded script, on a lowercase route, on a reused variable name. A quiet
|
|
9
|
+
* assertion is the easiest kind to write vacuously: delete the rule it is guarding and the fixture
|
|
10
|
+
* still passes, because a checker that does nothing is quiet about everything. Only a mutation can
|
|
11
|
+
* tell those apart, and the ignore set has the same shape: an over-wide allow rule and a working one
|
|
12
|
+
* produce identical output.
|
|
13
|
+
*
|
|
14
|
+
* WHAT IT DOES. For each rule, it neuters exactly that rule in the source, runs the harness, and
|
|
15
|
+
* requires the harness to go RED at the named assertion. Then it restores the file.
|
|
16
|
+
*
|
|
17
|
+
* ๐ด The quiet cases need INVERTED mutations. Disabling a rule cannot make a "stays quiet" case go
|
|
18
|
+
* red โ it makes it quieter. So those rows LOOSEN the guard instead (drop the anchor, add the `i`
|
|
19
|
+
* flag, accept any file mode), which is the mistake a future editor would actually make.
|
|
20
|
+
*
|
|
21
|
+
* The source is restored in a `finally`, and the run refuses to start on a dirty working tree.
|
|
22
|
+
*/
|
|
23
|
+
import { runMutations } from "../../../lib/mutation-driver.mjs";
|
|
24
|
+
import { dirname, join } from "node:path";
|
|
25
|
+
import { fileURLToPath } from "node:url";
|
|
26
|
+
import { consumerRoot } from "./consumer.mjs";
|
|
27
|
+
|
|
28
|
+
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
29
|
+
const ROOT = consumerRoot();
|
|
30
|
+
const SRC = join(HERE, "generated-code.mjs");
|
|
31
|
+
const HARNESS = join(HERE, "generated-code.harness.mjs");
|
|
32
|
+
|
|
33
|
+
/** [name, what it disables or loosens, exact source substring, replacement, expected red assertion] */
|
|
34
|
+
const MUTATIONS = [
|
|
35
|
+
// โโ the three rules fire โโ
|
|
36
|
+
[
|
|
37
|
+
"no-seed/rule",
|
|
38
|
+
"the unseeded-randomness rule",
|
|
39
|
+
" if (DRAWS.test(code) && !SEEDS.test(src))",
|
|
40
|
+
" if (false)",
|
|
41
|
+
"a sampling script with no seed passed",
|
|
42
|
+
],
|
|
43
|
+
[
|
|
44
|
+
"no-seed/python-draws",
|
|
45
|
+
"recognising `random.sample` as a draw",
|
|
46
|
+
"\\brandom\\.(?:random|sample|",
|
|
47
|
+
"\\brandom\\.(?:random|nope|",
|
|
48
|
+
"a sampling script with no seed passed",
|
|
49
|
+
],
|
|
50
|
+
[
|
|
51
|
+
"abs/rule",
|
|
52
|
+
"the hard-coded-absolute-path rule",
|
|
53
|
+
" const abs = code.match(\n new RegExp(ABS.source",
|
|
54
|
+
" const abs = false && code.match(\n new RegExp(ABS.source",
|
|
55
|
+
"a script that only runs on its author's laptop",
|
|
56
|
+
],
|
|
57
|
+
[
|
|
58
|
+
"abs/home",
|
|
59
|
+
"recognising /home/ as an absolute path",
|
|
60
|
+
"(?:\\/home\\/|\\/Users\\/",
|
|
61
|
+
"(?:\\/nope\\/|\\/Users\\/",
|
|
62
|
+
"a script that only runs on its author's laptop",
|
|
63
|
+
],
|
|
64
|
+
[
|
|
65
|
+
"inplace/rule",
|
|
66
|
+
"the in-place-overwrite rule",
|
|
67
|
+
" const both = [...operands(code, READS)].filter((p) =>\n operands(code, WRITES).has(p),\n );",
|
|
68
|
+
" const both = [];",
|
|
69
|
+
"a script that clobbers its own input passed",
|
|
70
|
+
],
|
|
71
|
+
[
|
|
72
|
+
"inplace/argv",
|
|
73
|
+
"the argv form of an in-place rewrite",
|
|
74
|
+
" /open\\(\\s*((?:sys\\.argv|process\\.argv)\\[\\d\\])\\s*\\)/g,",
|
|
75
|
+
" /never-matches-anything-x/g,",
|
|
76
|
+
"argv form of an in-place rewrite was not caught",
|
|
77
|
+
],
|
|
78
|
+
|
|
79
|
+
// โโ the guards that keep it quiet on correct code (LOOSENED, not disabled) โโ
|
|
80
|
+
[
|
|
81
|
+
"quiet/seed-recognised",
|
|
82
|
+
"recognising that a seed was set at all",
|
|
83
|
+
" if (DRAWS.test(code) && !SEEDS.test(src))",
|
|
84
|
+
" if (DRAWS.test(code))",
|
|
85
|
+
"a properly seeded script (explicit) was reported as unseeded",
|
|
86
|
+
],
|
|
87
|
+
// Disables BOTH alternations that could credit a seed threaded through argparse. Naming only
|
|
88
|
+
// `--seed` left the row alive on `\bSEED\b`, and a partial disable that another alternation
|
|
89
|
+
// covers is the same as no disable at all.
|
|
90
|
+
[
|
|
91
|
+
"quiet/seed-argparse",
|
|
92
|
+
"recognising a seed threaded through argparse (the leading-\\b regression)",
|
|
93
|
+
"--seed|\\bSEED\\b|Random",
|
|
94
|
+
"\\b--seed|\\bNOSUCHSEED\\b|Random",
|
|
95
|
+
"a properly seeded script (argparse) was reported as unseeded",
|
|
96
|
+
],
|
|
97
|
+
[
|
|
98
|
+
"quiet/abs-anchor",
|
|
99
|
+
"the left anchor that keeps a RELATIVE third-party path out",
|
|
100
|
+
"const ABS =\n /(?<![\\w./-])(?:",
|
|
101
|
+
"const ABS =\n /(?:",
|
|
102
|
+
"reported as absolute local paths",
|
|
103
|
+
],
|
|
104
|
+
[
|
|
105
|
+
"quiet/abs-case",
|
|
106
|
+
"case sensitivity, so an Express route /users/ stops matching /Users/",
|
|
107
|
+
"|[A-Z]:\\\\\\\\?[A-Za-z])/;",
|
|
108
|
+
"|[A-Z]:\\\\\\\\?[A-Za-z])/i;", // โ only bites since ABS.flags is threaded through
|
|
109
|
+
"reported as absolute local paths",
|
|
110
|
+
],
|
|
111
|
+
[
|
|
112
|
+
"quiet/read-mode",
|
|
113
|
+
"requiring a READ to not be a write (the prototype's vacuity bug)",
|
|
114
|
+
" /open\\(\\s*(['\"])([^'\"]+)\\1\\s*(?:,\\s*(['\"])r[b+]*\\3\\s*)?\\)/g,",
|
|
115
|
+
" /open\\(\\s*(['\"])([^'\"]+)\\1\\s*(?:,[^)]*)?\\)/g,",
|
|
116
|
+
"was reported as clobbering its input",
|
|
117
|
+
],
|
|
118
|
+
// ๐ด SPANS BOTH ARRAYS ON PURPOSE. Loosening only the READ side survived: the WRITE side still
|
|
119
|
+
// demanded a quoted literal, so no operand could ever pair and the verdict did not move. A
|
|
120
|
+
// mutation that cannot change the answer is not a mutation, and this is the second one this file
|
|
121
|
+
// caught in itself.
|
|
122
|
+
[
|
|
123
|
+
"quiet/literal-only",
|
|
124
|
+
"matching literals rather than operand expressions (17 false positives)",
|
|
125
|
+
" /readFileSync\\(\\s*(['\"])([^'\"]+)\\1/g,\n /Path\\(\\s*(['\"])([^'\"]+)\\1\\s*\\)\\s*\\.read/g,\n /open\\(\\s*((?:sys\\.argv|process\\.argv)\\[\\d\\])\\s*\\)/g,\n];\nconst WRITES = [\n /open\\(\\s*(['\"])([^'\"]+)\\1\\s*,\\s*['\"][wa]/g,\n /writeFileSync\\(\\s*(['\"])([^'\"]+)\\1/g,",
|
|
126
|
+
" /readFileSync\\(\\s*([^,)]+?)\\s*[,)]/g,\n /Path\\(\\s*(['\"])([^'\"]+)\\1\\s*\\)\\s*\\.read/g,\n /open\\(\\s*((?:sys\\.argv|process\\.argv)\\[\\d\\])\\s*\\)/g,\n];\nconst WRITES = [\n /open\\(\\s*(['\"])([^'\"]+)\\1\\s*,\\s*['\"][wa]/g,\n /writeFileSync\\(\\s*([^,)]+?)\\s*,/g,",
|
|
127
|
+
"reused across two loops was mistaken for dataflow",
|
|
128
|
+
],
|
|
129
|
+
|
|
130
|
+
// โโ the boundary with check-anon.sh โโ
|
|
131
|
+
[
|
|
132
|
+
"bundle/excluded",
|
|
133
|
+
"excluding the released bundle, which check-anon.sh cat. 5 owns",
|
|
134
|
+
" if (depth === 1 && name === bundleName) {\n swallowed.get(RULE[0]).push(name);\n continue;\n }",
|
|
135
|
+
" if (false) {\n swallowed.get(RULE[0]).push(name);\n continue;\n }",
|
|
136
|
+
"reported an absolute path inside the RELEASED bundle",
|
|
137
|
+
], // case 8, its own assertion
|
|
138
|
+
[
|
|
139
|
+
"bundle/named-in-ledger",
|
|
140
|
+
"naming that exclusion in the ledger",
|
|
141
|
+
' [\n "the released bundle โ `check-anon.sh` cat. 5 owns it, and it asks a different question there " +\n "(does it leak an identity, not will it run elsewhere)",\n [],\n ],',
|
|
142
|
+
' [\n "a tree we do not enter",\n [],\n ],',
|
|
143
|
+
"the boundary invisible",
|
|
144
|
+
],
|
|
145
|
+
|
|
146
|
+
// โโ the ignore set โโ
|
|
147
|
+
[
|
|
148
|
+
"allow/applied",
|
|
149
|
+
"applying an allow row at all",
|
|
150
|
+
" if (reason !== undefined) {\n ignored.push(",
|
|
151
|
+
" if (false) {\n ignored.push(",
|
|
152
|
+
"the allowed kind still fired",
|
|
153
|
+
],
|
|
154
|
+
[
|
|
155
|
+
"allow/per-kind",
|
|
156
|
+
"scoping an allowance to ONE KIND rather than to the whole file",
|
|
157
|
+
" const reason = allowed.get(key) ?? allowed.get(`${relative(repro, f)}\\t*`);",
|
|
158
|
+
" const reason = allowed.get(key) ?? [...allowed.entries()].find(([k]) => k.startsWith(relative(repro, f)))?.[1];",
|
|
159
|
+
"also silenced its unseeded sampling",
|
|
160
|
+
],
|
|
161
|
+
[
|
|
162
|
+
"allow/reason-printed",
|
|
163
|
+
"printing each allowance's reason",
|
|
164
|
+
"for (const [key, reason] of ignored) ledger.push(` ${key} โ ${reason}`);",
|
|
165
|
+
"for (const [key, reason] of ignored) ledger.push(` ${key}`);",
|
|
166
|
+
"its reason was not printed",
|
|
167
|
+
],
|
|
168
|
+
[
|
|
169
|
+
"allow/stale",
|
|
170
|
+
"reporting a row that names no script",
|
|
171
|
+
" if (!live.has(key))",
|
|
172
|
+
" if (false)",
|
|
173
|
+
"sat unreported, so the list never shrinks",
|
|
174
|
+
],
|
|
175
|
+
[
|
|
176
|
+
"allow/empty-said",
|
|
177
|
+
"saying so when nothing is being waved through",
|
|
178
|
+
'if (!ignored.length) ledger.push(" (nothing is being waved through)");',
|
|
179
|
+
'if (false) ledger.push(" (nothing is being waved through)");',
|
|
180
|
+
"an empty allow file printed nothing",
|
|
181
|
+
],
|
|
182
|
+
|
|
183
|
+
// โโ WHERE repro/ IS, and what silence is allowed to mean โโ
|
|
184
|
+
// ๐ด These three guard a defect that was LIVE until 2026-08-26 and produced no symptom: pointed
|
|
185
|
+
// at `<paper-e>`, the check exited 0 with no output while 18 findings sat in
|
|
186
|
+
// `typed-shell/repro/`. A missed directory is worse than a missed finding, because it is
|
|
187
|
+
// reported in exactly the words of a clean paper.
|
|
188
|
+
[
|
|
189
|
+
"scope/nested-root",
|
|
190
|
+
"looking for repro/ one level below the paper directory",
|
|
191
|
+
' .map((e) => join(dir, e.name, "repro"))',
|
|
192
|
+
' .map((e) => join(dir, e.name, "nope"))',
|
|
193
|
+
"a repro/ one level below the paper directory was not scanned",
|
|
194
|
+
],
|
|
195
|
+
[
|
|
196
|
+
"scope/loud-empty",
|
|
197
|
+
"saying so when there is no repro/ to scan (silence would read as clean)",
|
|
198
|
+
" if (!flagsOnly)\n console.log(\n `๐งช generated-code โ 0 finding(s) in ${dir}\\n NOTHING WAS SCANNED:",
|
|
199
|
+
" if (false)\n console.log(\n `๐งช generated-code โ 0 finding(s) in ${dir}\\n NOTHING WAS SCANNED:",
|
|
200
|
+
"produced no output at all",
|
|
201
|
+
],
|
|
202
|
+
[
|
|
203
|
+
"scope/second-root-named",
|
|
204
|
+
"naming a SECOND repro/ root in the ledger instead of dropping it",
|
|
205
|
+
"for (const other of roots.slice(1))\n swallowed.get(RULE[3]).push(relative(dir, other));",
|
|
206
|
+
"for (const other of [])\n swallowed.get(RULE[3]).push(relative(dir, other));",
|
|
207
|
+
"was dropped without being named",
|
|
208
|
+
],
|
|
209
|
+
|
|
210
|
+
// โโ the machine-readable contract with run-mechanical.mjs โโ
|
|
211
|
+
[
|
|
212
|
+
"out/stated-count",
|
|
213
|
+
"the stated finding count in the header",
|
|
214
|
+
"const header = `๐งช generated-code โ ${findings.length} finding(s) in ${dir}`;",
|
|
215
|
+
"const header = `๐งช generated-code โ ${dir}`;",
|
|
216
|
+
"does not state `โ 0 finding(s)`",
|
|
217
|
+
],
|
|
218
|
+
[
|
|
219
|
+
"out/ledger-on-clean",
|
|
220
|
+
"printing the ignore ledger when there are no findings",
|
|
221
|
+
' console.log(\n " every analysis script seeds its randomness, names only relative paths, and does not clobber its input",\n );\n ledger.forEach((l) => console.log(l));',
|
|
222
|
+
' console.log(\n " every analysis script seeds its randomness, names only relative paths, and does not clobber its input",\n );',
|
|
223
|
+
// Killed by case 8's ledger assertion rather than case 11's: case 8 is also a 0-finding
|
|
224
|
+
// report-mode run and it runs first. Both assert the property; the row records WHICH one caught
|
|
225
|
+
// it so this is not misread as an off-target kill.
|
|
226
|
+
"the boundary invisible",
|
|
227
|
+
],
|
|
228
|
+
[
|
|
229
|
+
"out/flags-quiet",
|
|
230
|
+
"the silence of --flags-only on a clean tree",
|
|
231
|
+
"if (!flagsOnly) {\n console.log(header);",
|
|
232
|
+
"if (true) {\n console.log(header);",
|
|
233
|
+
"--flags-only printed something about a clean tree",
|
|
234
|
+
],
|
|
235
|
+
];
|
|
236
|
+
|
|
237
|
+
// The table above is DATA about this checker; the engine below is shared with the other mutation
|
|
238
|
+
// files (`mutation-driver.mjs`). The split gave this file two protections it never had: the no-op
|
|
239
|
+
// guard (a replacement equal to the original leaves a green harness proving nothing) and one retry
|
|
240
|
+
// on a non-kill (a flake reported as a survivor sends the next reader to rewrite a working
|
|
241
|
+
// assertion). It also refuses to start when a named harness does not exist โ the defect that made
|
|
242
|
+
// three files report SURVIVED for every case after the 2026-08-11 colocation.
|
|
243
|
+
process.exit(
|
|
244
|
+
runMutations({
|
|
245
|
+
root: ROOT,
|
|
246
|
+
cases: MUTATIONS.map(([name, disables, find, replace, expect]) => ({
|
|
247
|
+
name,
|
|
248
|
+
disables,
|
|
249
|
+
edits: [[SRC, find, replace]],
|
|
250
|
+
harness: HARNESS,
|
|
251
|
+
expect,
|
|
252
|
+
})),
|
|
253
|
+
}),
|
|
254
|
+
);
|