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,623 @@
|
|
|
1
|
+
// ledger.mjs ā the pipeline's memory. One append-only file, and everything else is derived.
|
|
2
|
+
//
|
|
3
|
+
// WHY THIS EXISTS. Until 2026-08-07 the pipeline's state lived in a hand-maintained markdown
|
|
4
|
+
// table (`PIPELINE-STATUS.md`) whose rows a human ticked. It lied constantly and in one
|
|
5
|
+
// direction: a gate marked ā had been run against text that was later rewritten, and nothing
|
|
6
|
+
// noticed. On the day paper #3 shipped, the author found defects that every green gate had
|
|
7
|
+
// passed over ā because the green was about a document that no longer existed.
|
|
8
|
+
//
|
|
9
|
+
// Occupancy research (2026-08-06, `paper-pipeline/references/occupancy-2026-08-06-*`) said this
|
|
10
|
+
// is a solved problem and named the solution: content-addressed build systems. `make` is not
|
|
11
|
+
// enough ā it keys on timestamps and has NO notion of the recipe changing, which is exactly our
|
|
12
|
+
// bug, because editing a checker leaves its old verdict looking fresh. Nix and Bazel both fold
|
|
13
|
+
// the builder's own source into the cache key. So do we.
|
|
14
|
+
//
|
|
15
|
+
// THREE PROPERTIES, and each one is a defect we actually shipped:
|
|
16
|
+
//
|
|
17
|
+
// 1. STALENESS IS COMPUTED, NEVER DECLARED. A run is fresh only if the paper's bytes hash to
|
|
18
|
+
// what they hashed when the gate ran. No human ticks a box.
|
|
19
|
+
// 2. THE CHECKER'S OWN SOURCE IS PART OF THE KEY. Improve a skill and its old verdicts go
|
|
20
|
+
// stale, because they were verdicts of the old skill.
|
|
21
|
+
// 3. A GATE THAT HAS NEVER FOUND ANYTHING IS REPORTED AS SUSPECT. Borrowed from mutation
|
|
22
|
+
// testing: a test that kills no mutant is not a test. We shipped a tightening pass that
|
|
23
|
+
// returned KEEP on 80 of 81 sections and a justification pass that never once recommended
|
|
24
|
+
// deletion. Both "ran". Neither could have said no.
|
|
25
|
+
//
|
|
26
|
+
// NOT A BUILD SYSTEM. It does not run anything or decide what to run next. It records what ran,
|
|
27
|
+
// against what, and what it said ā the one thing nobody was doing.
|
|
28
|
+
//
|
|
29
|
+
// āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
30
|
+
// 2026-08-10 ā THERE IS NO `PASS` CONSTRUCTOR, AND THE ROW KEY IS THE CHECK
|
|
31
|
+
// āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
32
|
+
//
|
|
33
|
+
// FIVE FAILURES were observed on this pipeline. Three of them were defects of the CHECKING
|
|
34
|
+
// APPARATUS rather than of any manuscript, and all three are the same defect wearing three coats:
|
|
35
|
+
//
|
|
36
|
+
// 1. a gate was recorded as PASS while the input it needs did not exist;
|
|
37
|
+
// 2. a check that had never once returned a negative counted as a working check;
|
|
38
|
+
// 3. five model reviewers from one vendor agreeing was recorded as an acquittal.
|
|
39
|
+
//
|
|
40
|
+
// Each is a stored value meaning "nothing was wrong" ā written by a process that had no evidence
|
|
41
|
+
// for it, and thereafter indistinguishable from one that did. So the value is gone. A run may
|
|
42
|
+
// record exactly two things:
|
|
43
|
+
//
|
|
44
|
+
// FINDING ā something is wrong. MUST carry a path to the evidence. Refusing to record it
|
|
45
|
+
// would destroy the finding, so the row is written first and the command fails
|
|
46
|
+
// afterwards (see the CLI at the bottom; that ordering is load-bearing).
|
|
47
|
+
// ABSTAINED ā no judgement was produced. MUST carry a reason from a closed vocabulary, so
|
|
48
|
+
// "I ran and found nothing" and "I could not run" are different rows, not
|
|
49
|
+
// different sentences in a note nobody parses.
|
|
50
|
+
//
|
|
51
|
+
// "Nothing was wrong" is not a value that can be written down. It is the ABSENCE of findings,
|
|
52
|
+
// derived at read time by whoever is asking, who then owns the inference.
|
|
53
|
+
//
|
|
54
|
+
// āā PRIOR ART, stated honestly because both halves of it cut against us āāāāāāāāāāāāāāāāāāāāāā
|
|
55
|
+
//
|
|
56
|
+
// SARIF (OASIS Standard 2.1.0 §3.27.9) defines `kind: "pass"` verbatim as "The rule ⦠was
|
|
57
|
+
// evaluated, and no problem was found", and the only REQUIRED property of a `result` object is
|
|
58
|
+
// `message` ā `locations` is optional. So a conformant SARIF acquittal can name no place, no
|
|
59
|
+
// span and no artifact at all. That is the stored justification we delete.
|
|
60
|
+
//
|
|
61
|
+
// š“ What must NOT be said, here or in any paper: that industry lacks abstention. It does not.
|
|
62
|
+
// `kind: "open"` sits in the same paragraph ā "the tool concluded that there was insufficient
|
|
63
|
+
// information to decide whether a problem exists" ā and its NOTE 1 draws exactly the trichotomy
|
|
64
|
+
// we build on. Our position is therefore "DELETE PASS, leaving abstain as the only unwitnessed
|
|
65
|
+
// outcome", never "add abstain". A reviewer opens §3.27.9 and finds `open` in ten seconds.
|
|
66
|
+
// (Established [A] against the OASIS text and its JSON schema:
|
|
67
|
+
// `the author's private research notes` §5.)
|
|
68
|
+
//
|
|
69
|
+
// The ancestor is `Certifying algorithms` ā McConnell, Mehlhorn, NƤher & Schweitzer, Computer
|
|
70
|
+
// Science Review 5(2):119ā161, 2011 ā where BOTH answers carry a witness: "bipartite" ships a
|
|
71
|
+
// 2-colouring, "not bipartite" ships an odd cycle. š“ Our discipline is STRICTLY WEAKER than
|
|
72
|
+
// theirs and the code must say so rather than borrow their credit: we witness only the POSITIVE
|
|
73
|
+
// answer. We have no witness for "nothing is wrong", which is precisely why we do not offer a
|
|
74
|
+
// constructor that asserts it ā the honest name for a completed clean run is `no-witness`, an
|
|
75
|
+
// abstention, and it is not a pass with better manners.
|
|
76
|
+
//
|
|
77
|
+
// āā THE SECOND DEFECT, FIXED IN THE SAME PASS: the row key āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
78
|
+
//
|
|
79
|
+
// `status()` reduces a key's rows with `runs[runs.length - 1]` ā last row wins. That is correct
|
|
80
|
+
// for repeated runs of ONE check and catastrophic across two. The unit that produces a verdict
|
|
81
|
+
// is the CHECK, not the skill: two mechanical checks filed under one skill name took turns being
|
|
82
|
+
// the answer, so a clean run of the second erased a finding of the first from every derived view
|
|
83
|
+
// while the ledger file itself still honestly held both lines.
|
|
84
|
+
//
|
|
85
|
+
// The key is now `skill` + `check`, rendered `skill/check` (or just `skill` when a skill has a
|
|
86
|
+
// single check). `check-provenance`, `arm-permutation` and `delivered-pdf` all belong to
|
|
87
|
+
// `build-benchmark` and now hold three separate rows that cannot overwrite one another.
|
|
88
|
+
|
|
89
|
+
import { createHash } from "node:crypto";
|
|
90
|
+
import {
|
|
91
|
+
readFileSync,
|
|
92
|
+
appendFileSync,
|
|
93
|
+
existsSync,
|
|
94
|
+
mkdirSync,
|
|
95
|
+
statSync,
|
|
96
|
+
readdirSync,
|
|
97
|
+
} from "node:fs";
|
|
98
|
+
import { join, dirname, resolve, basename } from "node:path";
|
|
99
|
+
import { fileURLToPath } from "node:url";
|
|
100
|
+
import { consumerSkillsDir, isMain, ledgerPath } from "./consumer.mjs";
|
|
101
|
+
|
|
102
|
+
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
103
|
+
|
|
104
|
+
// š“ Overridable so a test never touches real history. The first version of the selftest wrote to
|
|
105
|
+
// the production ledger and restored it afterwards ā which works right up until the run crashes
|
|
106
|
+
// between the two, and then fixture rows are in the record permanently. That is not hypothetical:
|
|
107
|
+
// two `my-paper` rows survived into the working tree on 2026-08-07, hours after this shipped.
|
|
108
|
+
// Save-and-restore is not isolation. A separate file is.
|
|
109
|
+
//
|
|
110
|
+
// š“ THREE RUNGS SINCE 2026-09-12, AND THE THIRD ONE THROWS. `join(HERE, 'runs.jsonl')` was
|
|
111
|
+
// the whole resolution while this file lived in the repository it serves. Once the mechanism moved
|
|
112
|
+
// into a package, `HERE` is inside `node_modules` ā a directory `npm ci` deletes. Appending there
|
|
113
|
+
// SUCCEEDS, so rows would look recorded right up until the next install removed them, with no
|
|
114
|
+
// error at any point: the exact stored-untruth this ledger exists to abolish, arriving through its
|
|
115
|
+
// own front door. `ledgerPath()` refuses that path and names the key to declare instead; the
|
|
116
|
+
// environment variable above is unchanged and still outranks everything.
|
|
117
|
+
export const LEDGER = ledgerPath(HERE);
|
|
118
|
+
// š“ `resolve(HERE, '..', 'skills')` until 2026-08-15, correct while this lived in
|
|
119
|
+
// `.claude/pipeline/` and silently wrong the moment it moved into the skill it serves: from
|
|
120
|
+
// `.claude/skills/paper-pipeline/scripts/` that resolves to `ā¦/paper-pipeline/skills`, which does
|
|
121
|
+
// not exist. And the failure is SILENT in the worst direction ā `skillHash()` over a missing
|
|
122
|
+
// directory is stable, so every recorded verdict reads FRESH forever and staleness detection is
|
|
123
|
+
// dead without a single error. Caught only because `ledger.selftest.mjs` asserts the transition.
|
|
124
|
+
//
|
|
125
|
+
// š“ AND IT BROKE A SECOND TIME, THE SAME WAY, ON 2026-09-12 ā which is why it is no longer a
|
|
126
|
+
// walk at all. The skills being hashed are the CONSUMER's: `skillHash()` answers "has the checker
|
|
127
|
+
// changed since it rendered this verdict", and the checker is a SKILL.md in the repository under
|
|
128
|
+
// test. From inside `node_modules` two `..` land on the package's own skills directory, which
|
|
129
|
+
// holds no consumer skill ā and a hash over a missing directory is stable, i.e. every verdict
|
|
130
|
+
// reads FRESH forever. Same silent direction as last time, so the anchor is now the consumer root
|
|
131
|
+
// rather than a distance from this file.
|
|
132
|
+
const SKILLS = consumerSkillsDir();
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* The two things a run may record. There is no third, and in particular there is no positive.
|
|
136
|
+
*
|
|
137
|
+
* A gate that can only ABSTAIN is not a gate ā that property is asserted against every SKILL.md
|
|
138
|
+
* in `skill-checks.mjs` (assertion 10), because it cannot be observed at runtime until the gate
|
|
139
|
+
* has run, and by then a paper has shipped.
|
|
140
|
+
*/
|
|
141
|
+
export const KINDS = ["FINDING", "ABSTAINED"];
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Why a run produced no finding. CLOSED, because the whole point of deleting `PASS` is to stop
|
|
145
|
+
* one word covering several unrelated states ā and a free-text reason is that same word with
|
|
146
|
+
* more characters.
|
|
147
|
+
*
|
|
148
|
+
* š“ `no-witness` IS NOT A PASS. It says: this check ran to completion and produced no finding,
|
|
149
|
+
* and ā unlike a certifying algorithm (McConnell et al. 2011) ā it has no witness for the
|
|
150
|
+
* negative answer and therefore does not assert one. Everything downstream must render it as an
|
|
151
|
+
* abstention. `status.mjs` asserts, in its own harness, that its output contains no word meaning
|
|
152
|
+
* "passed".
|
|
153
|
+
*/
|
|
154
|
+
export const ABSTENTIONS = new Map([
|
|
155
|
+
[
|
|
156
|
+
"started",
|
|
157
|
+
"the run has begun and has not reported yet (written by announce.mjs)",
|
|
158
|
+
],
|
|
159
|
+
[
|
|
160
|
+
"no-witness",
|
|
161
|
+
"ran to completion, produced no finding ā NOT a claim that nothing is wrong",
|
|
162
|
+
],
|
|
163
|
+
["input-missing", "could not run: the thing it reads is not there"],
|
|
164
|
+
["blocked", "a required upstream gate has not produced its input"],
|
|
165
|
+
["crashed", "the check itself errored out"],
|
|
166
|
+
]);
|
|
167
|
+
|
|
168
|
+
/** The retired vocabulary, kept BY NAME so a caller reaching for it gets an explanation. */
|
|
169
|
+
export const RETIRED = new Map([
|
|
170
|
+
[
|
|
171
|
+
"PASS",
|
|
172
|
+
'deleted: it stored "nothing was wrong" as a value. Record ABSTAINED `no-witness`, and let the reader derive cleanliness from the absence of findings.',
|
|
173
|
+
],
|
|
174
|
+
[
|
|
175
|
+
"FINDINGS",
|
|
176
|
+
"renamed FINDING (one row is one check, and it either found something or it did not).",
|
|
177
|
+
],
|
|
178
|
+
[
|
|
179
|
+
"FAIL",
|
|
180
|
+
"is a FINDING. Blockingness is a property of the finding ā pass `blocking: true` ā not a second constructor.",
|
|
181
|
+
],
|
|
182
|
+
["ABSENT", "is ABSTAINED with reason `input-missing`."],
|
|
183
|
+
[
|
|
184
|
+
"ERROR",
|
|
185
|
+
"is ABSTAINED with reason `crashed` (or `started`, for an announce row).",
|
|
186
|
+
],
|
|
187
|
+
]);
|
|
188
|
+
|
|
189
|
+
const sha = (buf) =>
|
|
190
|
+
createHash("sha256").update(buf).digest("hex").slice(0, 16);
|
|
191
|
+
|
|
192
|
+
/** Hash of the document under test. Missing file is not an error ā it is a distinct state. */
|
|
193
|
+
export function inputHash(paperDir) {
|
|
194
|
+
const p = join(paperDir, "paper.md");
|
|
195
|
+
if (!existsSync(p)) return null;
|
|
196
|
+
return sha(readFileSync(p));
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Hash of the CHECKER ITSELF ā its SKILL.md plus any scripts it ships. This is the property
|
|
201
|
+
* `make` lacks and the reason its staleness answers cannot be trusted. Rewrite a skill's
|
|
202
|
+
* instructions and every verdict it ever gave becomes a verdict about a tool that is gone.
|
|
203
|
+
*/
|
|
204
|
+
export function skillHash(skill) {
|
|
205
|
+
const dir = join(SKILLS, skill);
|
|
206
|
+
if (!existsSync(dir)) return null;
|
|
207
|
+
const parts = [];
|
|
208
|
+
const walk = (d) => {
|
|
209
|
+
for (const e of readdirSorted(d)) {
|
|
210
|
+
const full = join(d, e);
|
|
211
|
+
const st = statSync(full);
|
|
212
|
+
if (st.isDirectory()) {
|
|
213
|
+
if (e !== "fixtures" && e !== "node_modules") walk(full);
|
|
214
|
+
} else if (/\.(md|mjs|js|sh|py|ts)$/.test(e))
|
|
215
|
+
parts.push(e + ":" + sha(readFileSync(full)));
|
|
216
|
+
}
|
|
217
|
+
};
|
|
218
|
+
walk(dir);
|
|
219
|
+
return sha(parts.join("\n"));
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function readdirSorted(d) {
|
|
223
|
+
// Sorted so the hash is stable across filesystems. An unstable key would make every run look
|
|
224
|
+
// stale on another machine, and a check that cries wolf is a check that gets muted.
|
|
225
|
+
return readdirSync(d).sort();
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// āā the key āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
229
|
+
|
|
230
|
+
/** The row key of a ledger row: `skill` for a single-check skill, `skill/check` otherwise. */
|
|
231
|
+
export const rowKey = (r) =>
|
|
232
|
+
r.check && r.check !== r.skill ? `${r.skill}/${r.check}` : r.skill;
|
|
233
|
+
|
|
234
|
+
/** Split a gate identifier (`skill` or `skill/check`) into its parts. Inverse of `rowKey`. */
|
|
235
|
+
export function parseGate(g) {
|
|
236
|
+
const [skill, check] = String(g).split("/");
|
|
237
|
+
return { key: String(g), skill, check: check || skill };
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// āā writing āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Record one run. Called by the skill itself, at the END, with what it produced.
|
|
244
|
+
*
|
|
245
|
+
* Throws BEFORE writing when the vocabulary is wrong (nothing is lost ā there is no evidence in
|
|
246
|
+
* a malformed call), and AFTER writing when a FINDING's evidence is missing (the finding must
|
|
247
|
+
* survive; see the CLI comment). An error thrown after the write carries `err.recorded`.
|
|
248
|
+
*/
|
|
249
|
+
export function record({
|
|
250
|
+
skill,
|
|
251
|
+
check = skill,
|
|
252
|
+
paper,
|
|
253
|
+
kind,
|
|
254
|
+
findings = 0,
|
|
255
|
+
report = null,
|
|
256
|
+
reason = null,
|
|
257
|
+
note = null,
|
|
258
|
+
blocking = false,
|
|
259
|
+
}) {
|
|
260
|
+
if (RETIRED.has(kind)) {
|
|
261
|
+
throw new Error(
|
|
262
|
+
`"${kind}" is not a verdict any more ā ${RETIRED.get(kind)}\n` +
|
|
263
|
+
` Record one of ${KINDS.join("|")}. Reasons for ABSTAINED: ${[...ABSTENTIONS.keys()].join("|")}.`,
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
if (!KINDS.includes(kind)) {
|
|
267
|
+
throw new Error(
|
|
268
|
+
`kind must be one of ${KINDS.join("|")}, got ${JSON.stringify(kind)}`,
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
if (kind === "ABSTAINED" && !ABSTENTIONS.has(reason)) {
|
|
272
|
+
throw new Error(
|
|
273
|
+
`ABSTAINED needs a reason from ${[...ABSTENTIONS.keys()].join("|")}, got ${JSON.stringify(reason)}.\n` +
|
|
274
|
+
` A free-text reason is "PASS" with more characters: the point of the closed set is that\n` +
|
|
275
|
+
` "ran and found nothing" and "could not run" stop being the same row.`,
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
if (kind === "FINDING" && !(Number(findings) >= 1)) {
|
|
279
|
+
throw new Error(
|
|
280
|
+
`FINDING with findings=${JSON.stringify(findings)}. A finding of nothing is an abstention:\n` +
|
|
281
|
+
` record ABSTAINED no-witness instead. Nothing is lost by refusing here ā there is no\n` +
|
|
282
|
+
` evidence in a count of zero.`,
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const paperDir = resolve(paper);
|
|
287
|
+
const row = {
|
|
288
|
+
ts: new Date().toISOString(),
|
|
289
|
+
skill,
|
|
290
|
+
check,
|
|
291
|
+
paper: basename(paperDir),
|
|
292
|
+
inputSha: inputHash(paperDir),
|
|
293
|
+
skillSha: skillHash(skill),
|
|
294
|
+
kind,
|
|
295
|
+
findings: kind === "FINDING" ? Number(findings) : 0,
|
|
296
|
+
blocking: kind === "FINDING" ? Boolean(blocking) : false,
|
|
297
|
+
reason,
|
|
298
|
+
report,
|
|
299
|
+
note,
|
|
300
|
+
};
|
|
301
|
+
mkdirSync(dirname(LEDGER), { recursive: true });
|
|
302
|
+
appendFileSync(LEDGER, JSON.stringify(row) + "\n");
|
|
303
|
+
|
|
304
|
+
if (kind === "FINDING") {
|
|
305
|
+
const problem = evidenceProblem(row, paperDir);
|
|
306
|
+
if (problem) {
|
|
307
|
+
const e = new Error(problem);
|
|
308
|
+
e.recorded = row; // the row IS on disk; only the run failed
|
|
309
|
+
throw e;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
return row;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* š“ THE EVIDENCE CONTRACT. A FINDING must point at something a reader can open, and if that
|
|
317
|
+
* something states its own count the two must agree.
|
|
318
|
+
*
|
|
319
|
+
* This is where an output contract can actually be enforced, and the reason is structural: a
|
|
320
|
+
* skill is prose and returns nothing to a runtime, so there is no `return skill({schema})` to
|
|
321
|
+
* typecheck. What there IS, is a choke point ā every skill must call `record` or its row never
|
|
322
|
+
* reaches the status table and the gate reads NEVER-RUN, which is loud. Anything checked here
|
|
323
|
+
* cannot be skipped, and nothing checked anywhere else has that property.
|
|
324
|
+
*
|
|
325
|
+
* Graduated on purpose, the gradual-typing shape rather than a flag day:
|
|
326
|
+
* no path at all -> HARD. The row survives (see `record`); the RUN fails.
|
|
327
|
+
* path points at nothing -> HARD. Worse than no path: it reads as evidence and sends the
|
|
328
|
+
* next reader into a 404.
|
|
329
|
+
* report states no count -> nudge only. Reports written before this convention are fine;
|
|
330
|
+
* demanding a field they lack would get the check disabled on
|
|
331
|
+
* day one.
|
|
332
|
+
* report states a DIFFERENT -> HARD. Two numbers that disagree mean one of them is already
|
|
333
|
+
* count wrong, and silence picks the wrong one at random.
|
|
334
|
+
*/
|
|
335
|
+
function evidenceProblem(row, paperDir) {
|
|
336
|
+
if (!row.report) {
|
|
337
|
+
return (
|
|
338
|
+
`${row.skill}: recorded ${row.findings} finding(s) with NO report path.\n` +
|
|
339
|
+
` The row is saved, so nothing is lost ā but a count with no location is a rumour, and\n` +
|
|
340
|
+
` next session will re-derive it from scratch. Re-run with the report path last:\n` +
|
|
341
|
+
` node .claude/skills/paper-pipeline/scripts/ledger.mjs record ${row.skill} <paper-dir> FINDING ${row.findings} <report-path>`
|
|
342
|
+
);
|
|
343
|
+
}
|
|
344
|
+
const reportPath = resolve(paperDir, row.report);
|
|
345
|
+
if (!existsSync(reportPath)) {
|
|
346
|
+
return (
|
|
347
|
+
`${row.skill}: recorded report path "${row.report}" ā nothing is there.\n` +
|
|
348
|
+
` Resolved to ${reportPath}\n` +
|
|
349
|
+
` A path that points at nothing is worse than no path: the status table shows it as\n` +
|
|
350
|
+
` evidence and the next reader follows it into a 404. Write the report, then record.`
|
|
351
|
+
);
|
|
352
|
+
}
|
|
353
|
+
const stated = /^findings:\s*(\d+)\s*$/m.exec(
|
|
354
|
+
readFileSync(reportPath, "utf8"),
|
|
355
|
+
);
|
|
356
|
+
if (!stated) {
|
|
357
|
+
console.error(
|
|
358
|
+
`\nā ļø ${row.skill}: ${row.report} does not state its own finding count.\n` +
|
|
359
|
+
` Add \`findings: ${row.findings}\` to its frontmatter and the ledger and the report can\n` +
|
|
360
|
+
` be checked against each other. Without it, the count here is unverifiable prose.`,
|
|
361
|
+
);
|
|
362
|
+
return null;
|
|
363
|
+
}
|
|
364
|
+
if (Number(stated[1]) !== Number(row.findings)) {
|
|
365
|
+
return (
|
|
366
|
+
`${row.skill}: the ledger says ${row.findings} finding(s), ${row.report} says ${stated[1]}.\n` +
|
|
367
|
+
` One of the two is already wrong and nothing here can tell which. Fix the report or the\n` +
|
|
368
|
+
` recorded count so they agree, then record again.`
|
|
369
|
+
);
|
|
370
|
+
}
|
|
371
|
+
return null;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// āā reading āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* š“ HOW ROWS FROM THE OLD VOCABULARY ARE TREATED, decided deliberately and loudly.
|
|
378
|
+
*
|
|
379
|
+
* `runs.jsonl` is append-only and holds real rows carrying `PASS`, `FINDINGS`, `FAIL`, `ABSENT`
|
|
380
|
+
* and `ERROR`. The file is NOT rewritten ā rewriting history to make it agree with today's
|
|
381
|
+
* design is the same move as ticking a box in the old status table.
|
|
382
|
+
*
|
|
383
|
+
* Nor are those rows TRANSLATED into the new constructors. A silent reinterpretation is exactly
|
|
384
|
+
* the class of defect this refactor exists to remove: were an old `PASS` mapped onto
|
|
385
|
+
* `ABSTAINED no-witness`, a row asserting "nothing was wrong" would come back out of the reader
|
|
386
|
+
* wearing the new vocabulary's honesty without having earned it.
|
|
387
|
+
*
|
|
388
|
+
* So a legacy row keeps its own kind, `LEGACY`, and its original word verbatim in
|
|
389
|
+
* `legacyVerdict`. `LEGACY` is deliberately NOT in `KINDS`: it can be read and can never be
|
|
390
|
+
* written. Downstream:
|
|
391
|
+
*
|
|
392
|
+
* ⢠it counts as a RUN (someone did run something, and pretending otherwise is its own lie);
|
|
393
|
+
* ⢠a legacy `FINDINGS`/`FAIL` row still surfaces its count and report path, because those
|
|
394
|
+
* rows carry evidence and discarding evidence is the destructive direction;
|
|
395
|
+
* ⢠a legacy `PASS`/`ABSENT`/`ERROR` row carries nothing forward. It is displayed as what it
|
|
396
|
+
* is ā a word from a vocabulary we retired ā and satisfies nothing.
|
|
397
|
+
*/
|
|
398
|
+
export const LEGACY_KIND = "LEGACY";
|
|
399
|
+
|
|
400
|
+
function normalise(row) {
|
|
401
|
+
if (row.kind) return { ...row, check: row.check || row.skill };
|
|
402
|
+
return {
|
|
403
|
+
...row,
|
|
404
|
+
check: row.check || row.skill,
|
|
405
|
+
kind: LEGACY_KIND,
|
|
406
|
+
legacyVerdict: row.verdict ?? null,
|
|
407
|
+
// Only the two negatives carried anything. Everything else is a word, not a fact.
|
|
408
|
+
findings:
|
|
409
|
+
row.verdict === "FINDINGS" || row.verdict === "FAIL"
|
|
410
|
+
? Number(row.findings ?? 0)
|
|
411
|
+
: 0,
|
|
412
|
+
blocking: row.verdict === "FAIL",
|
|
413
|
+
reason: null,
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/** Does this row assert that something is wrong? Legacy negatives count; legacy `PASS` does not. */
|
|
418
|
+
export const isFinding = (r) =>
|
|
419
|
+
r.kind === "FINDING" ||
|
|
420
|
+
(r.kind === LEGACY_KIND &&
|
|
421
|
+
(r.legacyVerdict === "FINDINGS" || r.legacyVerdict === "FAIL"));
|
|
422
|
+
|
|
423
|
+
export function readLedger() {
|
|
424
|
+
if (!existsSync(LEDGER)) return [];
|
|
425
|
+
return readFileSync(LEDGER, "utf8")
|
|
426
|
+
.split("\n")
|
|
427
|
+
.filter(Boolean)
|
|
428
|
+
.map((l) => {
|
|
429
|
+
try {
|
|
430
|
+
return normalise(JSON.parse(l));
|
|
431
|
+
} catch {
|
|
432
|
+
return null;
|
|
433
|
+
}
|
|
434
|
+
})
|
|
435
|
+
.filter(Boolean);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* The most recent run of one key ā by TIMESTAMP, not by position in the file.
|
|
440
|
+
*
|
|
441
|
+
* š“ WHY THIS IS NOT `runs[runs.length - 1]`, WHICH IS WHAT IT USED TO BE. The comment at the
|
|
442
|
+
* top of this file works out that "last row wins" is right for repeated runs of one check, and
|
|
443
|
+
* that is still the rule here. What it assumed without saying is that FILE ORDER EQUALS TIME
|
|
444
|
+
* ORDER. That holds for an append-only file with one writer, and it stops holding the moment two
|
|
445
|
+
* branches both append and git merges them: `.gitattributes` in the consumer declares
|
|
446
|
+
* `merge=union`, which concatenates "ours, then theirs" and orders nothing.
|
|
447
|
+
*
|
|
448
|
+
* Measured 2026-09-15 (external review, P1), two rows of one check:
|
|
449
|
+
*
|
|
450
|
+
* by position in file ABSTAINED findings=0 2026-09-14
|
|
451
|
+
* by timestamp FINDING findings=3 2026-09-15
|
|
452
|
+
*
|
|
453
|
+
* So a merge that lands an older row after a newer one HIDES the finding and reports a clean
|
|
454
|
+
* state ā a substituted verdict, not a cosmetic reordering of a log.
|
|
455
|
+
*
|
|
456
|
+
* ā ļø THE FIX BELONGS TO THE READER, AND THAT IS THE ARCHITECTURAL POINT. A merge driver that
|
|
457
|
+
* sorts by `ts` would close exactly one source of disorder and would have to be installed on
|
|
458
|
+
* every clone (`git config merge.<name>.driver`), which is the cost `union` exists to avoid.
|
|
459
|
+
* Disorder has other doors: a hand-edited ledger, two processes appending at once, a restored
|
|
460
|
+
* backup. Making the READER independent of order closes all of them at once, and after it the
|
|
461
|
+
* merge driver's behaviour stops mattering.
|
|
462
|
+
*
|
|
463
|
+
* Ties and missing stamps keep the OLD behaviour on purpose, so this change is a strict
|
|
464
|
+
* refinement rather than a new policy:
|
|
465
|
+
* - equal `ts` -> the later row in the file wins (two runs inside one second: file order is
|
|
466
|
+
* the only information there is, and it is the true order for one writer);
|
|
467
|
+
* - one row has no parsable `ts` -> the stamped row wins (a stamp is evidence, its absence is not);
|
|
468
|
+
* - neither has one -> file order, exactly as before.
|
|
469
|
+
*/
|
|
470
|
+
function latestRun(runs) {
|
|
471
|
+
let best = null;
|
|
472
|
+
let bestTime = null;
|
|
473
|
+
for (const r of runs) {
|
|
474
|
+
const t = Date.parse(r.ts ?? "");
|
|
475
|
+
const time = Number.isFinite(t) ? t : null;
|
|
476
|
+
if (best === null) {
|
|
477
|
+
best = r;
|
|
478
|
+
bestTime = time;
|
|
479
|
+
continue;
|
|
480
|
+
}
|
|
481
|
+
if (time === null && bestTime === null) {
|
|
482
|
+
best = r;
|
|
483
|
+
bestTime = time;
|
|
484
|
+
continue;
|
|
485
|
+
}
|
|
486
|
+
if (time === null) continue;
|
|
487
|
+
if (bestTime === null || time >= bestTime) {
|
|
488
|
+
best = r;
|
|
489
|
+
bestTime = time;
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
return best;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
/**
|
|
496
|
+
* The computed state of every CHECK for one paper. This REPLACES the hand-maintained table ā
|
|
497
|
+
* it is not a second copy of it. Nothing here is declared; every field is derived from the
|
|
498
|
+
* ledger plus the current bytes on disk.
|
|
499
|
+
*
|
|
500
|
+
* š“ There is no `verdict` field, and that is the interface change. A caller that wants to know
|
|
501
|
+
* whether a check is clean must read `findings === 0` and own the inference, which is a sentence
|
|
502
|
+
* it has to write down rather than a value it can copy.
|
|
503
|
+
*/
|
|
504
|
+
export function status(paperDir, { gates } = {}) {
|
|
505
|
+
const dir = resolve(paperDir);
|
|
506
|
+
const paper = basename(dir);
|
|
507
|
+
const nowInput = inputHash(dir);
|
|
508
|
+
const rows = readLedger().filter((r) => r.paper === paper);
|
|
509
|
+
|
|
510
|
+
const known = gates || [...new Set(rows.map(rowKey))];
|
|
511
|
+
return known.map((g) => {
|
|
512
|
+
const { key, skill, check } = parseGate(g);
|
|
513
|
+
const runs = rows.filter((r) => rowKey(r) === key);
|
|
514
|
+
const last = latestRun(runs);
|
|
515
|
+
const nowSkill = skillHash(skill);
|
|
516
|
+
|
|
517
|
+
let state;
|
|
518
|
+
if (!last) state = "NEVER-RUN";
|
|
519
|
+
else if (last.inputSha !== nowInput) state = "STALE-PAPER";
|
|
520
|
+
else if (last.skillSha !== nowSkill) state = "STALE-SKILL";
|
|
521
|
+
else state = "FRESH";
|
|
522
|
+
|
|
523
|
+
const found = last && isFinding(last);
|
|
524
|
+
return {
|
|
525
|
+
key,
|
|
526
|
+
skill,
|
|
527
|
+
check,
|
|
528
|
+
state,
|
|
529
|
+
runs: runs.length,
|
|
530
|
+
// The last run's finding, if it made one. Zero here means NO FINDING WAS RECORDED ā it is
|
|
531
|
+
// not, and must never be rendered as, a claim that the paper is clean.
|
|
532
|
+
findings: found ? Number(last.findings ?? 0) : 0,
|
|
533
|
+
blocking: found ? Boolean(last.blocking) : false,
|
|
534
|
+
report: found ? (last.report ?? null) : null,
|
|
535
|
+
abstained:
|
|
536
|
+
last && last.kind === "ABSTAINED"
|
|
537
|
+
? { reason: last.reason, note: last.note }
|
|
538
|
+
: null,
|
|
539
|
+
legacy: last && last.kind === LEGACY_KIND ? last.legacyVerdict : null,
|
|
540
|
+
ts: last?.ts ?? null,
|
|
541
|
+
// Mutation-testing borrow: a check with runs and no finding has never demonstrated it is
|
|
542
|
+
// able to produce one. Not proof of a broken check ā a reason to go plant a defect.
|
|
543
|
+
everFound: runs.some(isFinding),
|
|
544
|
+
};
|
|
545
|
+
});
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
// āā CLI āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
549
|
+
|
|
550
|
+
const USAGE = [
|
|
551
|
+
"usage: ledger.mjs record <skill> <paperDir> FINDING <count> <report-path> [--check=<id>] [--blocking]",
|
|
552
|
+
" ledger.mjs record <skill> <paperDir> ABSTAINED <reason> [note] [--check=<id>]",
|
|
553
|
+
" ledger.mjs status <paperDir>",
|
|
554
|
+
"",
|
|
555
|
+
` reasons: ${[...ABSTENTIONS.keys()].join(" | ")}`,
|
|
556
|
+
" --blocking marks a finding that must stop the pass (a fact is wrong, a required predecessor",
|
|
557
|
+
" does not exist). It is a PROPERTY of the finding, not a second constructor: the old `FAIL`.",
|
|
558
|
+
" there is no PASS: a clean run is `ABSTAINED no-witness`, and cleanliness is derived by the",
|
|
559
|
+
" reader from the absence of findings, never stored as a value.",
|
|
560
|
+
].join("\n");
|
|
561
|
+
|
|
562
|
+
if (isMain(import.meta.url)) {
|
|
563
|
+
// š“ EVERY FLAG THE USAGE TEXT ADVERTISES IS PARSED HERE, and that sentence is load-bearing
|
|
564
|
+
// because it was false for the first hour of this file's life: `--blocking` was written into the
|
|
565
|
+
// usage block and into all 22 SKILL.md files while nothing read it, so every skill instructing
|
|
566
|
+
// `--blocking` for its desk-reject case would have recorded an ordinary advisory finding and the
|
|
567
|
+
// status view would have shown a bullet where it should show š“. A documented flag that does
|
|
568
|
+
// nothing is this repository's oldest recurring defect ā three dead hooks, a twelve-day-dead
|
|
569
|
+
// nudge ā arriving inside the refactor written to end it. `gates.harness.mjs` block 14 asserts
|
|
570
|
+
// the flag from the outside, which is the only form of this that cannot rot.
|
|
571
|
+
const argv = process.argv.slice(2);
|
|
572
|
+
const flag = (name) => argv.some((a) => a === `--${name}`);
|
|
573
|
+
const valued = (name) =>
|
|
574
|
+
argv.find((a) => a.startsWith(`--${name}=`))?.slice(name.length + 3);
|
|
575
|
+
const [cmd, ...rest] = argv.filter((a) => !a.startsWith("--"));
|
|
576
|
+
|
|
577
|
+
if (cmd === "record") {
|
|
578
|
+
const [skill, paper, kind, a4, a5] = rest;
|
|
579
|
+
const check = valued("check") || skill;
|
|
580
|
+
const args =
|
|
581
|
+
kind === "FINDING"
|
|
582
|
+
? {
|
|
583
|
+
kind,
|
|
584
|
+
findings: Number(a4 || 0),
|
|
585
|
+
report: a5 || null,
|
|
586
|
+
blocking: flag("blocking"),
|
|
587
|
+
}
|
|
588
|
+
: { kind, reason: a4 || null, note: a5 || null };
|
|
589
|
+
|
|
590
|
+
// š“ A FINDING WITH BROKEN EVIDENCE IS RECORDED, AND THEN THE COMMAND FAILS.
|
|
591
|
+
//
|
|
592
|
+
// Every SKILL.md instructs `record <skill> <dir> FINDING <count> <report-path>`. On
|
|
593
|
+
// 2026-08-07, surfacing open findings for the first time showed that four of six real runs
|
|
594
|
+
// had passed the count and omitted the path: build-benchmark 13, tighten-paper 9,
|
|
595
|
+
// grade-paper-writing 11, draft-paper 3 ā thirty-six findings that are a number and nothing
|
|
596
|
+
// else, unrecoverable without rerunning the gate. The instruction was written, read, and not
|
|
597
|
+
// followed, which is this pipeline's own thesis landing on itself.
|
|
598
|
+
//
|
|
599
|
+
// The row is written FIRST and deliberately: refusing outright would destroy the finding,
|
|
600
|
+
// which is worse than storing it badly. What must not happen is the run reading as DONE. So
|
|
601
|
+
// the data survives and the command exits non-zero, which the author sees in the session
|
|
602
|
+
// rather than a week later in a status table. `record()` throws AFTER appending, and this
|
|
603
|
+
// catch is the half that turns that into an exit code.
|
|
604
|
+
let row;
|
|
605
|
+
try {
|
|
606
|
+
row = record({ skill, check, paper, ...args });
|
|
607
|
+
} catch (e) {
|
|
608
|
+
if (e.recorded) {
|
|
609
|
+
console.log(JSON.stringify(e.recorded));
|
|
610
|
+
console.error(`\nš“ ${e.message}`);
|
|
611
|
+
process.exit(1);
|
|
612
|
+
}
|
|
613
|
+
console.error(`\nš“ ${e.message}\n\n${USAGE}`);
|
|
614
|
+
process.exit(2);
|
|
615
|
+
}
|
|
616
|
+
console.log(JSON.stringify(row));
|
|
617
|
+
} else if (cmd === "status") {
|
|
618
|
+
console.log(JSON.stringify(status(rest[0] || "."), null, 2));
|
|
619
|
+
} else {
|
|
620
|
+
console.error(USAGE);
|
|
621
|
+
process.exit(2);
|
|
622
|
+
}
|
|
623
|
+
}
|