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,74 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
3
|
+
import {
|
|
4
|
+
existsSync,
|
|
5
|
+
mkdirSync,
|
|
6
|
+
mkdtempSync,
|
|
7
|
+
realpathSync,
|
|
8
|
+
rmSync,
|
|
9
|
+
} from "node:fs";
|
|
10
|
+
import { tmpdir } from "node:os";
|
|
11
|
+
import { join } from "node:path";
|
|
12
|
+
import { afterAll as after, test } from "vitest";
|
|
13
|
+
import { nodeAdapters } from "../node/index.ts";
|
|
14
|
+
import type { AbsolutePath } from "../../domain/paths.ts";
|
|
15
|
+
import {
|
|
16
|
+
banalCommand,
|
|
17
|
+
shQuote,
|
|
18
|
+
stage,
|
|
19
|
+
stageBanalInput,
|
|
20
|
+
type StagedInput,
|
|
21
|
+
} from "./invocation.ts";
|
|
22
|
+
import type { LocatedBanal } from "./locate.ts";
|
|
23
|
+
import { XML_DIALECT } from "./xml.ts";
|
|
24
|
+
|
|
25
|
+
test("the staging holds BOTH files: the XML and the -v stub answering the dialect rpp writes", () => {
|
|
26
|
+
const [xml, stub] = stageBanalInput([]).files;
|
|
27
|
+
assert.equal(xml.name, "paper.xml");
|
|
28
|
+
assert.match(xml.content, /^<\?xml/);
|
|
29
|
+
assert.equal(stub.mode, "exec");
|
|
30
|
+
// Guards: the version the stub answers — below 0.85 banal moves every font size.
|
|
31
|
+
assert.ok(stub.content.includes(`pdftohtml version ${XML_DIALECT.version}`));
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test("banalCommand: perl runs banal on the staged .xml, with $PDFTOHTML quoted", () => {
|
|
35
|
+
const banal = {
|
|
36
|
+
path: "/b/banal" as AbsolutePath,
|
|
37
|
+
provenance: { kind: "cache" },
|
|
38
|
+
} as LocatedBanal;
|
|
39
|
+
const staged = { xml: "/s/paper.xml", stub: "/s d/pdftohtml" } as StagedInput;
|
|
40
|
+
const c = banalCommand(banal, staged, { PATH: "/bin" });
|
|
41
|
+
assert.deepEqual(
|
|
42
|
+
[c.file, ...c.args],
|
|
43
|
+
["perl", "/b/banal", "-no-time", "-json", "/s/paper.xml"],
|
|
44
|
+
);
|
|
45
|
+
// Guards: banal interpolates $PDFTOHTML into /bin/sh unquoted; a space in $TMPDIR must survive.
|
|
46
|
+
assert.deepEqual(c.env, { PATH: "/bin", PDFTOHTML: "'/s d/pdftohtml'" });
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("shQuote survives a single quote", () => {
|
|
50
|
+
assert.equal(shQuote("it's"), `'it'"'"'s'`);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const root = realpathSync(mkdtempSync(join(tmpdir(), "rpp-stage-test-")));
|
|
54
|
+
after(() => rmSync(root, { recursive: true, force: true }));
|
|
55
|
+
|
|
56
|
+
test("🔴 stage: on a real scratch directory the stub answers `-v` through the shell, from a path with a space and a quote", () => {
|
|
57
|
+
const awkward = join(root, "it's a dir");
|
|
58
|
+
mkdirSync(awkward);
|
|
59
|
+
const { workspace } = nodeAdapters({ tmpDir: awkward });
|
|
60
|
+
workspace.within("rpp-banal-", (s) => {
|
|
61
|
+
const staged = stage(s, stageBanalInput([]));
|
|
62
|
+
assert.ok(existsSync(staged.xml));
|
|
63
|
+
// Guards: banal runs `$PDFTOHTML -v 2>&1 |` through /bin/sh, unquoted.
|
|
64
|
+
const v = spawnSync("/bin/sh", ["-c", `${shQuote(staged.stub)} -v 2>&1`], {
|
|
65
|
+
encoding: "utf8",
|
|
66
|
+
});
|
|
67
|
+
assert.equal(v.stdout.trim(), "pdftohtml version 24.02.0");
|
|
68
|
+
const convert = spawnSync(staged.stub, ["-xml", "paper.pdf", "out"], {
|
|
69
|
+
encoding: "utf8",
|
|
70
|
+
});
|
|
71
|
+
assert.equal(convert.status, 1);
|
|
72
|
+
assert.match(convert.stderr, /only answers -v/);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A banal run, as DATA. What must be on disk before banal starts — the banal input XML and the
|
|
3
|
+
* `pdftohtml -v` stub, always together — and the command that runs it.
|
|
4
|
+
*
|
|
5
|
+
* Why the stub: banal asks `$pdftohtml -v` once, before it reads any input (the XML included), to
|
|
6
|
+
* pick the zoom and a font-size correction (banal 1.2, lines 1828-1868). With no pdftohtml it stops
|
|
7
|
+
* (`Error: Failed to run pdftohtml`, measured); told a version below 0.85 it applies a larger
|
|
8
|
+
* correction and every font size moves. So `$PDFTOHTML` (banal's own override, line 166) points at a
|
|
9
|
+
* stub that answers `-v` with `XML_DIALECT.version` — the pdftohtml whose XML `xml.ts` writes — and
|
|
10
|
+
* refuses anything else. `test/e2e/banal.mjs` shows both failures with the real banal.
|
|
11
|
+
*/
|
|
12
|
+
import type { Opaque } from "ts-essentials";
|
|
13
|
+
import type { AbsolutePath } from "../../domain/paths.ts";
|
|
14
|
+
import type { Command } from "../../ports/process.ts";
|
|
15
|
+
import type { Scratch } from "../../ports/workspace.ts";
|
|
16
|
+
import type { LocatedBanal } from "./locate.ts";
|
|
17
|
+
import type { PageLayout } from "../../domain/page-layout.ts";
|
|
18
|
+
import { pdf2xml, XML_DIALECT } from "./xml.ts";
|
|
19
|
+
|
|
20
|
+
/** banal takes well under a second on a paper; a hang still has to end. */
|
|
21
|
+
export const BANAL_RUN_MS = 120_000;
|
|
22
|
+
|
|
23
|
+
/** A path quoted for `/bin/sh`. Minted by `shQuote` only. */
|
|
24
|
+
export type ShQuoted = Opaque<string, "ShQuoted">;
|
|
25
|
+
|
|
26
|
+
/** banal interpolates `$PDFTOHTML` into a shell command unquoted — so the value is quoted here. */
|
|
27
|
+
export const shQuote = (s: string): ShQuoted =>
|
|
28
|
+
`'${s.replace(/'/g, `'"'"'`)}'` as ShQuoted;
|
|
29
|
+
|
|
30
|
+
/** The stub standing in for `pdftohtml`: it answers `-v` and refuses to convert anything. */
|
|
31
|
+
export const PDFTOHTML_STUB = [
|
|
32
|
+
"#!/bin/sh",
|
|
33
|
+
"# research-paper-pipeline: banal reads the banal input XML rpp writes, never a PDF. banal still",
|
|
34
|
+
"# asks `pdftohtml -v` which dialect to expect; this answers with the one rpp writes.",
|
|
35
|
+
'if [ "$1" = "-v" ]; then',
|
|
36
|
+
` echo "pdftohtml version ${XML_DIALECT.version}"`,
|
|
37
|
+
" exit 0",
|
|
38
|
+
"fi",
|
|
39
|
+
'echo "rpp: this pdftohtml only answers -v; banal was given a PDF instead of the banal input XML" >&2',
|
|
40
|
+
"exit 1",
|
|
41
|
+
"",
|
|
42
|
+
].join("\n");
|
|
43
|
+
|
|
44
|
+
export interface StagedFile<N extends string, M extends "read" | "exec"> {
|
|
45
|
+
readonly name: N;
|
|
46
|
+
readonly content: string;
|
|
47
|
+
readonly mode: M;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** What must exist on disk before banal runs. Both files, always: there is no way to build one alone. */
|
|
51
|
+
export interface BanalStaging {
|
|
52
|
+
readonly files: readonly [
|
|
53
|
+
StagedFile<"paper.xml", "read">,
|
|
54
|
+
StagedFile<"pdftohtml", "exec">,
|
|
55
|
+
];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export const stageBanalInput = (
|
|
59
|
+
pages: readonly PageLayout[],
|
|
60
|
+
): BanalStaging => ({
|
|
61
|
+
files: [
|
|
62
|
+
{ name: "paper.xml", content: pdf2xml(pages), mode: "read" },
|
|
63
|
+
{ name: "pdftohtml", content: PDFTOHTML_STUB, mode: "exec" },
|
|
64
|
+
],
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
/** The staging written to disk. Minted by `stage` only: both files written, or nothing returned. */
|
|
68
|
+
export type StagedInput = Opaque<
|
|
69
|
+
{ readonly xml: AbsolutePath; readonly stub: AbsolutePath },
|
|
70
|
+
"StagedInput"
|
|
71
|
+
>;
|
|
72
|
+
|
|
73
|
+
/** Write a staging into a scratch directory, the stub executable. The only minter of `StagedInput`. */
|
|
74
|
+
export function stage(s: Scratch, staging: BanalStaging): StagedInput {
|
|
75
|
+
const [xml, stub] = staging.files;
|
|
76
|
+
return {
|
|
77
|
+
xml: s.write(xml.name, xml.content, xml.mode),
|
|
78
|
+
stub: s.write(stub.name, stub.content, stub.mode),
|
|
79
|
+
} as StagedInput;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** `perl banal -no-time -json <xml>`, with `$PDFTOHTML` quoted by construction. */
|
|
83
|
+
export function banalCommand(
|
|
84
|
+
banal: LocatedBanal,
|
|
85
|
+
staged: StagedInput,
|
|
86
|
+
baseEnv: Readonly<Record<string, string>>,
|
|
87
|
+
): Command {
|
|
88
|
+
return {
|
|
89
|
+
file: "perl",
|
|
90
|
+
args: [banal.path, "-no-time", "-json", staged.xml],
|
|
91
|
+
env: { ...baseEnv, PDFTOHTML: shQuote(staged.stub) },
|
|
92
|
+
timeoutMs: BANAL_RUN_MS,
|
|
93
|
+
maxOutputBytes: 64 * 1024 * 1024,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { test } from "vitest";
|
|
3
|
+
import type { AbsolutePath } from "../../domain/paths.ts";
|
|
4
|
+
import { err } from "../../domain/result.ts";
|
|
5
|
+
import { installedBanal, lookupOrder, pickBanal } from "./locate.ts";
|
|
6
|
+
import { BANAL_PIN } from "./pin.ts";
|
|
7
|
+
import { parseBanalSettings } from "./settings.ts";
|
|
8
|
+
|
|
9
|
+
const dirs = { home: "/h", tmp: "/t", cwd: "/w" };
|
|
10
|
+
const project = "/p" as AbsolutePath;
|
|
11
|
+
const cache = `/h/.cache/rpp/banal/${BANAL_PIN.commit.slice(0, 12)}/banal`;
|
|
12
|
+
const on =
|
|
13
|
+
(...files: string[]) =>
|
|
14
|
+
(p: string) =>
|
|
15
|
+
files.includes(p);
|
|
16
|
+
const pick = (env: Record<string, string>, isFile: (p: string) => boolean) =>
|
|
17
|
+
pickBanal(lookupOrder(parseBanalSettings(env, dirs), project), isFile);
|
|
18
|
+
const kind = (r: ReturnType<typeof pick>) =>
|
|
19
|
+
r.ok ? r.value.provenance.kind : r.error.kind;
|
|
20
|
+
|
|
21
|
+
test("installedBanal: one directory per HotCRP commit", () => {
|
|
22
|
+
assert.equal(installedBanal(parseBanalSettings({}, dirs)), cache);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test("rpp's own copy when nothing else is named", () => {
|
|
26
|
+
assert.equal(kind(pick({}, on(cache))), "cache");
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test("a project's vendor/banal before rpp's copy", () => {
|
|
30
|
+
// Guards: a project that vendors banal keeps using its own copy.
|
|
31
|
+
assert.equal(kind(pick({}, on(cache, "/p/vendor/banal"))), "vendor");
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test("$BANAL before both", () => {
|
|
35
|
+
const r = pick({ BANAL: "/own" }, on("/own", cache, "/p/vendor/banal"));
|
|
36
|
+
assert.equal(r.ok && r.value.path, "/own");
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test("🔴 $BANAL naming a missing file finds NOTHING — no fallback to another banal", () => {
|
|
40
|
+
// Guards: an explicit choice that is wrong is an error, not a silent fall-back.
|
|
41
|
+
assert.deepEqual(
|
|
42
|
+
pick({ BANAL: "/nope" }, on(cache, "/p/vendor/banal")),
|
|
43
|
+
err({ kind: "explicit-not-found", path: "/nope" }),
|
|
44
|
+
);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test("nothing anywhere: not installed, naming where rpp's copy would be", () => {
|
|
48
|
+
assert.deepEqual(
|
|
49
|
+
pick({}, on()),
|
|
50
|
+
err({ kind: "not-installed", installed: cache }),
|
|
51
|
+
);
|
|
52
|
+
});
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where a banal may be, in order; which one is there; and which rule found it. The order:
|
|
3
|
+
* `$BANAL` (an explicit choice — if it names nothing, there is NO fallback), `<project>/vendor/banal`
|
|
4
|
+
* (a project that vendors its own copy), then the one `paperlint toolchain` installed.
|
|
5
|
+
*/
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
import type { Opaque } from "ts-essentials";
|
|
8
|
+
import type { AbsolutePath } from "../../domain/paths.ts";
|
|
9
|
+
import { err, ok, type Result } from "../../domain/result.ts";
|
|
10
|
+
import { BANAL_PIN } from "./pin.ts";
|
|
11
|
+
import type { BanalSettings } from "./settings.ts";
|
|
12
|
+
|
|
13
|
+
export type Provenance =
|
|
14
|
+
| { readonly kind: "env" }
|
|
15
|
+
| { readonly kind: "vendor" }
|
|
16
|
+
| { readonly kind: "cache" };
|
|
17
|
+
|
|
18
|
+
export interface BanalCandidate {
|
|
19
|
+
readonly path: AbsolutePath;
|
|
20
|
+
readonly provenance: Provenance;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** A candidate the `Files` port confirmed is a file. Minted by `pickBanal` only. */
|
|
24
|
+
export type LocatedBanal = Opaque<BanalCandidate, "LocatedBanal">;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* `paperlint toolchain`'s banal after its bytes hashed to the pin AND it measured the probe page. Minted by
|
|
28
|
+
* `ensureBanal` / `checkBanal` only (`./index.ts`): a "ready" line cannot be printed without one.
|
|
29
|
+
*/
|
|
30
|
+
export type PinnedBanal = Opaque<BanalCandidate, "PinnedBanal">;
|
|
31
|
+
|
|
32
|
+
/** Why there is no banal to run. */
|
|
33
|
+
export type BanalMissing =
|
|
34
|
+
/** `$BANAL` names a file that is not there. An explicit choice: no fallback to another banal. */
|
|
35
|
+
| { readonly kind: "explicit-not-found"; readonly path: string }
|
|
36
|
+
/** Nothing named, nothing vendored, and `paperlint toolchain` has not installed it (here). */
|
|
37
|
+
| { readonly kind: "not-installed"; readonly installed: string };
|
|
38
|
+
|
|
39
|
+
/** Where to look. An explicit `$BANAL` is the whole search; otherwise vendor, then the cache. */
|
|
40
|
+
export type LookupOrder =
|
|
41
|
+
| { readonly kind: "explicit"; readonly path: AbsolutePath }
|
|
42
|
+
| {
|
|
43
|
+
readonly kind: "search";
|
|
44
|
+
readonly vendor: AbsolutePath;
|
|
45
|
+
readonly cache: AbsolutePath;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
/** Where `paperlint toolchain` puts the pinned banal: one directory per HotCRP commit. */
|
|
49
|
+
export const installedBanal = (s: BanalSettings): AbsolutePath =>
|
|
50
|
+
join(s.cacheDir, BANAL_PIN.commit.slice(0, 12), "banal") as AbsolutePath;
|
|
51
|
+
|
|
52
|
+
export function lookupOrder(
|
|
53
|
+
s: BanalSettings,
|
|
54
|
+
projectRoot: AbsolutePath,
|
|
55
|
+
): LookupOrder {
|
|
56
|
+
if (s.explicit) return { kind: "explicit", path: s.explicit };
|
|
57
|
+
return {
|
|
58
|
+
kind: "search",
|
|
59
|
+
vendor: join(projectRoot, "vendor", "banal") as AbsolutePath,
|
|
60
|
+
cache: installedBanal(s),
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const located = (path: AbsolutePath, kind: Provenance["kind"]) =>
|
|
65
|
+
ok({ path, provenance: { kind } } as LocatedBanal);
|
|
66
|
+
|
|
67
|
+
export function pickBanal(
|
|
68
|
+
order: LookupOrder,
|
|
69
|
+
isFile: (p: AbsolutePath) => boolean,
|
|
70
|
+
): Result<LocatedBanal, BanalMissing> {
|
|
71
|
+
if (order.kind === "explicit")
|
|
72
|
+
return isFile(order.path)
|
|
73
|
+
? located(order.path, "env")
|
|
74
|
+
: err({ kind: "explicit-not-found", path: order.path });
|
|
75
|
+
if (isFile(order.vendor)) return located(order.vendor, "vendor");
|
|
76
|
+
if (isFile(order.cache)) return located(order.cache, "cache");
|
|
77
|
+
return err({ kind: "not-installed", installed: order.cache });
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Which rule found it, as a person reads it. */
|
|
81
|
+
export const provenanceLabel = (p: Provenance): string =>
|
|
82
|
+
({ env: "$BANAL", vendor: "vendor/banal", cache: "paperlint toolchain" })[
|
|
83
|
+
p.kind
|
|
84
|
+
];
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `output.ts` — every way a banal run can end, as a table of `ProcessExit` → `Result`, and the
|
|
3
|
+
* measurement → geometry projection. Pure: no perl, no disk. The real banal is `test/e2e/banal.mjs`.
|
|
4
|
+
*/
|
|
5
|
+
import assert from "node:assert/strict";
|
|
6
|
+
import { test } from "vitest";
|
|
7
|
+
import type { ProcessExit } from "../../ports/process.ts";
|
|
8
|
+
import { err } from "../../domain/result.ts";
|
|
9
|
+
import type { BanalFailure } from "./failure.ts";
|
|
10
|
+
import {
|
|
11
|
+
geometryOf,
|
|
12
|
+
parseBanalOutput,
|
|
13
|
+
type BanalMeasurement,
|
|
14
|
+
} from "./output.ts";
|
|
15
|
+
|
|
16
|
+
const exited = (
|
|
17
|
+
over: Partial<{ status: number; stdout: string; stderr: string }>,
|
|
18
|
+
): ProcessExit => ({
|
|
19
|
+
kind: "exited",
|
|
20
|
+
status: 0,
|
|
21
|
+
stdout: "",
|
|
22
|
+
stderr: "",
|
|
23
|
+
...over,
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
/** The failure a row expects, or the `kind` only when its detail is a zod message. */
|
|
27
|
+
const ROWS: readonly (readonly [
|
|
28
|
+
string,
|
|
29
|
+
ProcessExit,
|
|
30
|
+
BanalFailure | BanalFailure["kind"],
|
|
31
|
+
])[] = [
|
|
32
|
+
[
|
|
33
|
+
// Guards: perl missing is its own diagnosis, not "banal failed: ENOENT".
|
|
34
|
+
"perl not found",
|
|
35
|
+
{ kind: "not-found", file: "perl" },
|
|
36
|
+
{ kind: "perl-missing" },
|
|
37
|
+
],
|
|
38
|
+
[
|
|
39
|
+
"spawn failed",
|
|
40
|
+
{ kind: "spawn-failed", message: "EACCES" },
|
|
41
|
+
{ kind: "spawn-failed", message: "EACCES" },
|
|
42
|
+
],
|
|
43
|
+
[
|
|
44
|
+
"timed out",
|
|
45
|
+
{ kind: "timed-out", afterMs: 50, stdout: "", stderr: "" },
|
|
46
|
+
{ kind: "timed-out", afterMs: 50 },
|
|
47
|
+
],
|
|
48
|
+
[
|
|
49
|
+
"killed by a signal",
|
|
50
|
+
{ kind: "signalled", signal: "SIGKILL", stdout: "", stderr: "\n oom \n" },
|
|
51
|
+
{ kind: "signalled", signal: "SIGKILL", stderrHead: "oom" },
|
|
52
|
+
],
|
|
53
|
+
[
|
|
54
|
+
"a nonzero exit names the exit and banal's first line",
|
|
55
|
+
exited({ status: 1, stderr: "\nx.xml: Error: bad\nmore" }),
|
|
56
|
+
{ kind: "process-failed", status: 1, stderrHead: "x.xml: Error: bad" },
|
|
57
|
+
],
|
|
58
|
+
[
|
|
59
|
+
"output that is not JSON",
|
|
60
|
+
exited({ stdout: "Usage: banal" }),
|
|
61
|
+
{ kind: "no-json", head: "Usage: banal" },
|
|
62
|
+
],
|
|
63
|
+
[
|
|
64
|
+
// Guards: banal's own failure object exits 0 — it must not read as zero pages.
|
|
65
|
+
'banal\'s `"error": true` with exit 0',
|
|
66
|
+
exited({ stdout: '{"error": true, "pages": []}', stderr: "cannot read" }),
|
|
67
|
+
{ kind: "banal-error", stderrHead: "cannot read" },
|
|
68
|
+
],
|
|
69
|
+
[
|
|
70
|
+
"JSON that is not an object",
|
|
71
|
+
exited({ stdout: "[1, 2]" }),
|
|
72
|
+
"unexpected-shape",
|
|
73
|
+
],
|
|
74
|
+
[
|
|
75
|
+
"an object with no `pages`",
|
|
76
|
+
exited({ stdout: '{"columns": 2}' }),
|
|
77
|
+
"unexpected-shape",
|
|
78
|
+
],
|
|
79
|
+
];
|
|
80
|
+
|
|
81
|
+
for (const [label, exit, want] of ROWS) {
|
|
82
|
+
test(`parseBanalOutput: ${label}`, () => {
|
|
83
|
+
const got = parseBanalOutput(exit);
|
|
84
|
+
if (typeof want === "string")
|
|
85
|
+
assert.equal(!got.ok && got.error.kind, want, JSON.stringify(got));
|
|
86
|
+
else assert.deepEqual(got, err(want));
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
test("parseBanalOutput: a measurement is parsed, unknown fields kept", () => {
|
|
91
|
+
const got = parseBanalOutput(
|
|
92
|
+
exited({ stdout: '{"columns": 2, "pages": [{}], "extra": 1}' }),
|
|
93
|
+
);
|
|
94
|
+
assert.ok(got.ok);
|
|
95
|
+
assert.equal(got.value.columns, 2);
|
|
96
|
+
assert.equal(got.value.pages.length, 1);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
const M = (over: Partial<BanalMeasurement>): BanalMeasurement => ({
|
|
100
|
+
pages: [],
|
|
101
|
+
...over,
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test("geometryOf: a page without a type IS a body page", () => {
|
|
105
|
+
// Guards: banal omits `type` on body pages; counting type === "body" gave zero for every paper.
|
|
106
|
+
const g = geometryOf(
|
|
107
|
+
M({
|
|
108
|
+
pages: [
|
|
109
|
+
{},
|
|
110
|
+
{},
|
|
111
|
+
{},
|
|
112
|
+
{ type: "bib", reffontsize: 8.5 },
|
|
113
|
+
{ type: "appendix" },
|
|
114
|
+
],
|
|
115
|
+
}),
|
|
116
|
+
);
|
|
117
|
+
assert.deepEqual(
|
|
118
|
+
[g.bodyPages, g.refPages, g.appendixPages, g.refPt],
|
|
119
|
+
[3, 1, 1, 8.5],
|
|
120
|
+
);
|
|
121
|
+
assert.deepEqual(g.pagesByType, { body: 3, bib: 1, appendix: 1 });
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test("geometryOf: papersize is [height, width] in points → inches", () => {
|
|
125
|
+
const g = geometryOf(
|
|
126
|
+
M({ papersize: [792, 612], columns: 2, bodyfontsize: 10 }),
|
|
127
|
+
);
|
|
128
|
+
assert.deepEqual(
|
|
129
|
+
[g.pageWidthIn, g.pageHeightIn, g.columns, g.bodyPt],
|
|
130
|
+
[8.5, 11, 2, 10],
|
|
131
|
+
);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test("geometryOf: an empty measurement is nulls and zeros, not a crash", () => {
|
|
135
|
+
const g = geometryOf(M({ bodyfontsize: null }));
|
|
136
|
+
assert.deepEqual(
|
|
137
|
+
[g.pageWidthIn, g.columns, g.bodyPt, g.refPt, g.bodyPages],
|
|
138
|
+
[null, null, null, null, 0],
|
|
139
|
+
);
|
|
140
|
+
});
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* banal's output, PARSED — one schema, one type, read by both consumers (the facts file and the
|
|
3
|
+
* install probe). What banal prints was measured on the pinned banal 1.2 (2026-09-25): an object
|
|
4
|
+
* with `papersize` ([height, width] in points), `columns`, `bodyfontsize` — null on an empty
|
|
5
|
+
* document — and `pages`, one entry per page, where a page's `type` is OMITTED when it is "body".
|
|
6
|
+
*/
|
|
7
|
+
import { z } from "zod";
|
|
8
|
+
import type { PageGeometry } from "../../domain/geometry.ts";
|
|
9
|
+
import { firstLine } from "../../domain/text.ts";
|
|
10
|
+
import type { ProcessExit } from "../../ports/process.ts";
|
|
11
|
+
import { andThen, err, ok, type Result } from "../../domain/result.ts";
|
|
12
|
+
import type { BanalFailure } from "./failure.ts";
|
|
13
|
+
|
|
14
|
+
const page = z.looseObject({
|
|
15
|
+
type: z.string().optional(),
|
|
16
|
+
reffontsize: z.number().nullable().optional(),
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
/** banal's own failure object: exit 0 and `{"error": true, …}`. It has no measurement in it. */
|
|
20
|
+
const banalError = z.looseObject({ error: z.literal(true) });
|
|
21
|
+
|
|
22
|
+
export const measurementSchema = z.looseObject({
|
|
23
|
+
papersize: z.tuple([z.number(), z.number()]).optional(),
|
|
24
|
+
columns: z.number().nullable().optional(),
|
|
25
|
+
bodyfontsize: z.number().nullable().optional(),
|
|
26
|
+
/** Required: banal emits it on every run, an empty document included (measured). */
|
|
27
|
+
pages: z.array(page),
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
/** A measurement banal printed. banal's `{"error": true}` object cannot be one: it is refused first. */
|
|
31
|
+
export type BanalMeasurement = z.infer<typeof measurementSchema>;
|
|
32
|
+
|
|
33
|
+
interface Streams {
|
|
34
|
+
readonly stdout: string;
|
|
35
|
+
readonly stderr: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** The process-level outcome, before stdout is read: banal's streams, or why there are none. */
|
|
39
|
+
function streamsOf(r: ProcessExit): Result<Streams, BanalFailure> {
|
|
40
|
+
switch (r.kind) {
|
|
41
|
+
case "not-found":
|
|
42
|
+
return err({ kind: "perl-missing" });
|
|
43
|
+
case "spawn-failed":
|
|
44
|
+
return err({ kind: "spawn-failed", message: r.message });
|
|
45
|
+
case "timed-out":
|
|
46
|
+
return err({ kind: "timed-out", afterMs: r.afterMs });
|
|
47
|
+
case "signalled":
|
|
48
|
+
return err({
|
|
49
|
+
kind: "signalled",
|
|
50
|
+
signal: r.signal,
|
|
51
|
+
stderrHead: firstLine(r.stderr),
|
|
52
|
+
});
|
|
53
|
+
case "exited":
|
|
54
|
+
return r.status === 0
|
|
55
|
+
? ok(r)
|
|
56
|
+
: err({
|
|
57
|
+
kind: "process-failed",
|
|
58
|
+
status: r.status,
|
|
59
|
+
stderrHead: firstLine(r.stderr),
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function jsonOf(stdout: string): Result<unknown, null> {
|
|
65
|
+
try {
|
|
66
|
+
return ok(JSON.parse(stdout));
|
|
67
|
+
} catch {
|
|
68
|
+
return err(null);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** What banal printed, as a measurement — or which failure it is. */
|
|
73
|
+
function measurementOf(o: Streams): Result<BanalMeasurement, BanalFailure> {
|
|
74
|
+
const json = jsonOf(o.stdout);
|
|
75
|
+
if (!json.ok)
|
|
76
|
+
return err({
|
|
77
|
+
kind: "no-json",
|
|
78
|
+
head: firstLine(o.stdout) || firstLine(o.stderr),
|
|
79
|
+
});
|
|
80
|
+
if (banalError.safeParse(json.value).success)
|
|
81
|
+
return err({ kind: "banal-error", stderrHead: firstLine(o.stderr) });
|
|
82
|
+
const m = measurementSchema.safeParse(json.value);
|
|
83
|
+
return m.success
|
|
84
|
+
? ok(m.data)
|
|
85
|
+
: err({
|
|
86
|
+
kind: "unexpected-shape",
|
|
87
|
+
issues: m.error.issues.map(
|
|
88
|
+
(i) => `${i.path.join(".") || "(root)"}: ${i.message}`,
|
|
89
|
+
),
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** A banal run as a measurement, or which failure it was. Total over `ProcessExit`. */
|
|
94
|
+
export const parseBanalOutput = (
|
|
95
|
+
r: ProcessExit,
|
|
96
|
+
): Result<BanalMeasurement, BanalFailure> =>
|
|
97
|
+
andThen(streamsOf(r), measurementOf);
|
|
98
|
+
|
|
99
|
+
const inches = (pt: number | undefined): number | null =>
|
|
100
|
+
pt === undefined ? null : Number((pt / 72).toFixed(3));
|
|
101
|
+
|
|
102
|
+
type Page = BanalMeasurement["pages"][number];
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* 🔴 banal OMITS a page's `type` when it is "body" (its line 1022: `push … if $page->{type} ne
|
|
106
|
+
* "body"`), so counting pages whose type equals "body" gives ZERO for every paper in the world —
|
|
107
|
+
* that is how the first page-limit gate was written, and it could not fire once. A page without a
|
|
108
|
+
* type IS a body page.
|
|
109
|
+
*/
|
|
110
|
+
const typeOf = (p: Page): string => p.type ?? "body";
|
|
111
|
+
|
|
112
|
+
/** Pages per type, body pages included under "body". */
|
|
113
|
+
function countByType(pages: readonly Page[]): Record<string, number> {
|
|
114
|
+
const byType: Record<string, number> = {};
|
|
115
|
+
for (const p of pages) byType[typeOf(p)] = (byType[typeOf(p)] ?? 0) + 1;
|
|
116
|
+
return byType;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** The reference font size: the first bibliography page that has one. */
|
|
120
|
+
const refFontSize = (pages: readonly Page[]): number | null =>
|
|
121
|
+
pages.find((p) => p.type === "bib" && p.reffontsize != null)?.reffontsize ??
|
|
122
|
+
null;
|
|
123
|
+
|
|
124
|
+
/** The measurement → the domain's page geometry. */
|
|
125
|
+
export function geometryOf(m: BanalMeasurement): PageGeometry {
|
|
126
|
+
const byType = countByType(m.pages);
|
|
127
|
+
const refPages = byType["bib"] ?? 0;
|
|
128
|
+
const appendixPages = byType["appendix"] ?? 0;
|
|
129
|
+
return {
|
|
130
|
+
// banal gives papersize as [height, width] in points — checked against live output 2026-08-26.
|
|
131
|
+
pageWidthIn: inches(m.papersize?.[1]),
|
|
132
|
+
pageHeightIn: inches(m.papersize?.[0]),
|
|
133
|
+
columns: m.columns ?? null,
|
|
134
|
+
bodyPt: m.bodyfontsize ?? null,
|
|
135
|
+
refPt: refFontSize(m.pages),
|
|
136
|
+
bodyPages: m.pages.length - refPages - appendixPages,
|
|
137
|
+
refPages,
|
|
138
|
+
appendixPages,
|
|
139
|
+
pagesByType: byType,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The banal paperlint runs: HotCRP at one commit, one file, one sha256. The licence boundary is in
|
|
3
|
+
* `./index.ts`: paperlint downloads and runs banal, it never contains it.
|
|
4
|
+
*/
|
|
5
|
+
import { parseSha256, type Sha256 } from "../../domain/sha256.ts";
|
|
6
|
+
|
|
7
|
+
/** Where to download banal from, and the bytes to accept. The pin by default; a test passes a local file. */
|
|
8
|
+
export interface BanalSource {
|
|
9
|
+
readonly url: string;
|
|
10
|
+
readonly sha256: Sha256;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface BanalPin extends BanalSource {
|
|
14
|
+
readonly version: string;
|
|
15
|
+
readonly commit: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Parsed at module load: a typo in the pin fails on import, not at the first download. */
|
|
19
|
+
export const BANAL_PIN: BanalPin = {
|
|
20
|
+
version: "1.2",
|
|
21
|
+
commit: "f3e4352133f3184c7c42b0d5e6501124bead18e6",
|
|
22
|
+
url: "https://raw.githubusercontent.com/kohler/hotcrp/f3e4352133f3184c7c42b0d5e6501124bead18e6/src/banal",
|
|
23
|
+
sha256: parseSha256(
|
|
24
|
+
"fd8cc4ae189b9da02460ae442a34f14434e5784210489fb668313ac671006911",
|
|
25
|
+
),
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/** How a person reads the pin: `banal 1.2 (HotCRP f3e4352)`. */
|
|
29
|
+
export const pinLabel = (pin: BanalPin = BANAL_PIN): string =>
|
|
30
|
+
`banal ${pin.version} (HotCRP ${pin.commit.slice(0, 7)})`;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Does a banal RUN — accepted by a measurement, not by the download's exit code. The probe is one
|
|
3
|
+
* US-letter page of 50 lines of 10 pt text: enough for banal to measure a body font size.
|
|
4
|
+
*/
|
|
5
|
+
import { err, ok, type Result } from "../../domain/result.ts";
|
|
6
|
+
import type { BanalFailure } from "./failure.ts";
|
|
7
|
+
import type { BanalMeasurement } from "./output.ts";
|
|
8
|
+
import type { PageLayout } from "../../domain/page-layout.ts";
|
|
9
|
+
|
|
10
|
+
/** One text line of 90 characters, 10 pt, at line `i` of a one-column page. */
|
|
11
|
+
const probeLine = (i: number) => ({
|
|
12
|
+
top: 72 + i * 12,
|
|
13
|
+
left: 72,
|
|
14
|
+
width: 468,
|
|
15
|
+
height: 10,
|
|
16
|
+
size: 10,
|
|
17
|
+
font: "Times-Roman",
|
|
18
|
+
text: "x".repeat(90),
|
|
19
|
+
upright: true,
|
|
20
|
+
fill: { kind: "unknown" } as const,
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
export const PROBE_PAGE: PageLayout = {
|
|
24
|
+
widthPt: 612,
|
|
25
|
+
heightPt: 792,
|
|
26
|
+
boxes: Array.from({ length: 50 }, (_, i) => probeLine(i)),
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/** A probe measurement is accepted when banal saw the one page and measured its body size. */
|
|
30
|
+
export const acceptProbe = (
|
|
31
|
+
m: BanalMeasurement,
|
|
32
|
+
): Result<BanalMeasurement, BanalFailure> =>
|
|
33
|
+
m.pages.length === 1 && typeof m.bodyfontsize === "number"
|
|
34
|
+
? ok(m)
|
|
35
|
+
: err({ kind: "probe-rejected", got: m });
|