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,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE BANAL INPUT XML: the pdftohtml-format XML (text boxes with position, size and font per page)
|
|
3
|
+
* that paperlint writes from the domain's page layout, which is the input banal reads — PURE. No disk, no pdf.js,
|
|
4
|
+
* no process.
|
|
5
|
+
*
|
|
6
|
+
* `pdf-facts.ts` reads the PDF with pdf.js and hands this module plain data: every text item's box,
|
|
7
|
+
* size, font, fill colour and whether it is upright. This module decides which of them a page-layout
|
|
8
|
+
* tool would count, and writes them as the banal input XML, in the dialect `pdftohtml -xml` writes
|
|
9
|
+
* and banal (HotCRP's page-geometry script, see `./index.ts`) reads. banal then measures that XML exactly as it measures the XML poppler's
|
|
10
|
+
* `pdftohtml` writes — which is how paperlint gets banal's numbers without poppler (issue #61).
|
|
11
|
+
*
|
|
12
|
+
* ── WHAT MAKES THE XML EQUAL TO pdftohtml's, as far as banal can tell ─────────────
|
|
13
|
+
* Measured on 50 PDFs / 598 pages (2026-09-25): real banal on this XML gave the same page size,
|
|
14
|
+
* columns, body and reference font size, and page counts by type as real banal on real pdftohtml.
|
|
15
|
+
* It matched only with the conditions below; each gave a wrong answer when switched off.
|
|
16
|
+
*
|
|
17
|
+
* 1. ROTATED TEXT IS NOT WRITTEN. pdftohtml writes it with width 0 and banal drops zero-width
|
|
18
|
+
* text; writing it as a horizontal box would count a rotated margin note as body text.
|
|
19
|
+
* 2. INVISIBLE TEXT (render mode 3, an OCR layer) IS NOT WRITTEN — pdftohtml does not write it.
|
|
20
|
+
* 3. LIGHT TEXT IS WRITTEN WITH ITS COLOUR, and banal drops it itself. What counts as light is
|
|
21
|
+
* banal's decision (a watermark, grey code comments), so the colour goes into `<fontspec>`
|
|
22
|
+
* exactly as pdftohtml writes it and no threshold is copied here.
|
|
23
|
+
* 4. SIZES AND COORDINATES AT ZOOM 3, the scale banal assumes for a modern pdftohtml. A font size
|
|
24
|
+
* is written as `round(size × 3)` — an integer, as pdftohtml writes it — and banal applies its
|
|
25
|
+
* own rounding to it; coordinates are written unrounded and banal rounds them to 1/8 of a
|
|
26
|
+
* decipoint. Rounding them here would round twice.
|
|
27
|
+
*
|
|
28
|
+
* banal also has to be told which pdftohtml wrote the XML, because it asks `pdftohtml -v` and
|
|
29
|
+
* derives the zoom and a font-size correction from the answer. `./index.ts` answers that question
|
|
30
|
+
* with `XML_DIALECT` below; the dialect and the writer live together so they cannot disagree.
|
|
31
|
+
*/
|
|
32
|
+
import type { Fill, PageLayout, TextBox } from "../../domain/page-layout.ts";
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* The pdftohtml whose XML this module writes: the zoom it writes at, and the version banal is told.
|
|
36
|
+
* banal (1.2, lines 1848-1867) picks zoom 3 and a font-size correction of 1 for any version >= 0.85;
|
|
37
|
+
* told an older version it uses 3, and every font size it reports moves.
|
|
38
|
+
*/
|
|
39
|
+
export const XML_DIALECT = { zoom: 3, version: "24.02.0" } as const;
|
|
40
|
+
|
|
41
|
+
const BLACK = "#000000";
|
|
42
|
+
|
|
43
|
+
/** Whether pdftohtml would write this box at all (conditions 1 and 2 above). */
|
|
44
|
+
export const written = (b: TextBox): boolean =>
|
|
45
|
+
b.upright && b.fill.kind !== "invisible" && b.text.trim() !== "";
|
|
46
|
+
|
|
47
|
+
const colourOf = (f: Fill): string => (f.kind === "rgb" ? f.hex : BLACK);
|
|
48
|
+
|
|
49
|
+
/** XML-escape text and attribute values the way pdftohtml does (a newline becomes a space). */
|
|
50
|
+
export function xmlEscape(s: string): string {
|
|
51
|
+
return s
|
|
52
|
+
.replace(/&/g, "&")
|
|
53
|
+
.replace(/</g, "<")
|
|
54
|
+
.replace(/>/g, ">")
|
|
55
|
+
.replace(/"/g, """)
|
|
56
|
+
.replace(/[\r\n]/g, " ");
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** A number as pdftohtml's `-noroundcoord` writes it: fixed, six decimals. */
|
|
60
|
+
const fixed = (n: number): string => n.toFixed(6);
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* The document-wide font table. pdftohtml numbers fonts across the whole document and declares a
|
|
64
|
+
* font on the first page that uses it, before that page's text; banal keeps the table across pages.
|
|
65
|
+
*/
|
|
66
|
+
class FontTable {
|
|
67
|
+
private readonly ids = new Map<string, number>();
|
|
68
|
+
|
|
69
|
+
/** The id of this font, and whether this is its first use. */
|
|
70
|
+
idOf(size: number, font: string, colour: string): [number, boolean] {
|
|
71
|
+
const key = `${String(size)}|${font}|${colour}`;
|
|
72
|
+
const known = this.ids.get(key);
|
|
73
|
+
if (known !== undefined) return [known, false];
|
|
74
|
+
const id = this.ids.size;
|
|
75
|
+
this.ids.set(key, id);
|
|
76
|
+
return [id, true];
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** One page: its `<page>` line, the fonts it introduces, then its text lines. */
|
|
81
|
+
function pageXml(p: PageLayout, n: number, fonts: FontTable): string[] {
|
|
82
|
+
const z = XML_DIALECT.zoom;
|
|
83
|
+
const specs: string[] = [];
|
|
84
|
+
const texts: string[] = [];
|
|
85
|
+
for (const b of p.boxes.filter(written)) {
|
|
86
|
+
const size = Math.round(b.size * z);
|
|
87
|
+
const colour = colourOf(b.fill);
|
|
88
|
+
const [id, fresh] = fonts.idOf(size, b.font, colour);
|
|
89
|
+
if (fresh)
|
|
90
|
+
specs.push(
|
|
91
|
+
`\t<fontspec id="${String(id)}" size="${String(size)}" family="${xmlEscape(b.font)}" color="${colour}"/>`,
|
|
92
|
+
);
|
|
93
|
+
texts.push(
|
|
94
|
+
`<text top="${fixed(b.top * z)}" left="${fixed(b.left * z)}" width="${fixed(b.width * z)}" height="${fixed(b.height * z)}" font="${String(id)}">${xmlEscape(b.text)}</text>`,
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
const head = `<page number="${String(n)}" position="absolute" top="0" left="0" height="${String(Math.round(p.heightPt * z))}" width="${String(Math.round(p.widthPt * z))}">`;
|
|
98
|
+
return [head, ...specs, ...texts, "</page>"];
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** The pages as `pdftohtml -xml -i -zoom 3 -noroundcoord` would write them. */
|
|
102
|
+
export function pdf2xml(pages: readonly PageLayout[]): string {
|
|
103
|
+
const fonts = new FontTable();
|
|
104
|
+
const lines = [
|
|
105
|
+
'<?xml version="1.0" encoding="UTF-8"?>',
|
|
106
|
+
'<!DOCTYPE pdf2xml SYSTEM "pdf2xml.dtd">',
|
|
107
|
+
`<pdf2xml producer="research-paper-pipeline (pdf.js)" version="${XML_DIALECT.version}">`,
|
|
108
|
+
...pages.flatMap((p, i) => pageXml(p, i + 1, fonts)),
|
|
109
|
+
"</pdf2xml>",
|
|
110
|
+
];
|
|
111
|
+
return `${lines.join("\n")}\n`;
|
|
112
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `Download` through `curl -fsSL`. Not Node's `fetch`: undici's `fetch` ignores `HTTPS_PROXY` unless
|
|
3
|
+
* a proxy agent is installed, and rpp's CI and the containers it runs in reach the network only
|
|
4
|
+
* through a proxy. curl reads the proxy variables itself. (A claim from documentation, not measured
|
|
5
|
+
* here — the port is what makes trying the other adapter cheap.)
|
|
6
|
+
*/
|
|
7
|
+
import { mkdtempSync, readFileSync, realpathSync, rmSync } from "node:fs";
|
|
8
|
+
import { join } from "node:path";
|
|
9
|
+
import { firstLine } from "../../domain/text.ts";
|
|
10
|
+
import type { Download } from "../../ports/download.ts";
|
|
11
|
+
import type { ProcessExit, RunProcess } from "../../ports/process.ts";
|
|
12
|
+
import { err, ok } from "../../domain/result.ts";
|
|
13
|
+
|
|
14
|
+
/** Why a curl run did not download, in its own words; null when it exited 0. */
|
|
15
|
+
export function curlFailure(r: ProcessExit): string | null {
|
|
16
|
+
if (r.kind === "exited" && r.status === 0) return null;
|
|
17
|
+
if (r.kind === "not-found") return "curl is not installed";
|
|
18
|
+
if (r.kind === "spawn-failed") return r.message;
|
|
19
|
+
if (r.kind === "timed-out") return `no answer after ${String(r.afterMs)} ms`;
|
|
20
|
+
const what =
|
|
21
|
+
r.kind === "exited" ? `curl exited ${String(r.status)}` : r.signal;
|
|
22
|
+
return firstLine(r.stderr) || what;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const readOrNull = (p: string): Uint8Array | null => {
|
|
26
|
+
try {
|
|
27
|
+
return readFileSync(p);
|
|
28
|
+
} catch {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* `run` is the process port (so a test can take curl away); `env` is curl's whole environment, the
|
|
35
|
+
* proxy variables included; `tmpDir` holds the file curl writes, removed before `fetch` returns.
|
|
36
|
+
*/
|
|
37
|
+
export function curlDownload(o: {
|
|
38
|
+
readonly run: RunProcess;
|
|
39
|
+
readonly env: Readonly<Record<string, string>>;
|
|
40
|
+
readonly tmpDir: string;
|
|
41
|
+
}): Download {
|
|
42
|
+
return {
|
|
43
|
+
fetch(url: string, timeoutMs: number) {
|
|
44
|
+
const dir = realpathSync(mkdtempSync(join(o.tmpDir, "rpp-download-")));
|
|
45
|
+
const out = join(dir, "body");
|
|
46
|
+
try {
|
|
47
|
+
const seconds = String(Math.ceil(timeoutMs / 1000));
|
|
48
|
+
const failed = curlFailure(
|
|
49
|
+
o.run.run({
|
|
50
|
+
file: "curl",
|
|
51
|
+
args: [
|
|
52
|
+
"-fsSL",
|
|
53
|
+
"--retry",
|
|
54
|
+
"2",
|
|
55
|
+
"--max-time",
|
|
56
|
+
seconds,
|
|
57
|
+
"-o",
|
|
58
|
+
out,
|
|
59
|
+
url,
|
|
60
|
+
],
|
|
61
|
+
env: o.env,
|
|
62
|
+
timeoutMs: timeoutMs + 30_000,
|
|
63
|
+
}),
|
|
64
|
+
);
|
|
65
|
+
const bytes = failed ? null : readOrNull(out);
|
|
66
|
+
if (bytes) return ok(bytes);
|
|
67
|
+
return err({ detail: failed ?? "curl wrote no file" });
|
|
68
|
+
} finally {
|
|
69
|
+
rmSync(dir, { recursive: true, force: true });
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `Download` contract against the real curl, on `file://` URLs (no network): bytes come back,
|
|
3
|
+
* the temp file is removed, and a missing curl is named as that.
|
|
4
|
+
*/
|
|
5
|
+
import assert from "node:assert/strict";
|
|
6
|
+
import {
|
|
7
|
+
mkdirSync,
|
|
8
|
+
mkdtempSync,
|
|
9
|
+
readdirSync,
|
|
10
|
+
realpathSync,
|
|
11
|
+
rmSync,
|
|
12
|
+
writeFileSync,
|
|
13
|
+
} from "node:fs";
|
|
14
|
+
import { tmpdir } from "node:os";
|
|
15
|
+
import { join } from "node:path";
|
|
16
|
+
import { pathToFileURL } from "node:url";
|
|
17
|
+
import { afterAll as after, test } from "vitest";
|
|
18
|
+
import type { AbsolutePath } from "../../domain/paths.ts";
|
|
19
|
+
import { curlDownload } from "./download.io.ts";
|
|
20
|
+
import { spawnProcess } from "../node/index.ts";
|
|
21
|
+
|
|
22
|
+
const root = realpathSync(mkdtempSync(join(tmpdir(), "rpp-download-test-")));
|
|
23
|
+
after(() => rmSync(root, { recursive: true, force: true }));
|
|
24
|
+
const at = (...p: string[]) => join(root, ...p) as AbsolutePath;
|
|
25
|
+
|
|
26
|
+
const dl = (env: Record<string, string>) => {
|
|
27
|
+
const tmp = at("dl-tmp");
|
|
28
|
+
mkdirSync(tmp, { recursive: true });
|
|
29
|
+
return { d: curlDownload({ run: spawnProcess(), env, tmpDir: tmp }), tmp };
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
test("Download: a file:// URL comes back as its bytes, and the temp file is gone", () => {
|
|
33
|
+
const src = at("served");
|
|
34
|
+
writeFileSync(src, "banal bytes");
|
|
35
|
+
const { d, tmp } = dl({ PATH: "/usr/bin:/bin" });
|
|
36
|
+
const r = d.fetch(pathToFileURL(src).href, 10_000);
|
|
37
|
+
assert.equal(r.ok && new TextDecoder().decode(r.value), "banal bytes");
|
|
38
|
+
assert.deepEqual(readdirSync(tmp), []);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test("Download: a URL that does not answer is a failure with curl's words", () => {
|
|
42
|
+
const { d } = dl({ PATH: "/usr/bin:/bin" });
|
|
43
|
+
const r = d.fetch(pathToFileURL(at("missing")).href, 10_000);
|
|
44
|
+
assert.equal(r.ok, false);
|
|
45
|
+
assert.ok(!r.ok && r.error.detail.length > 0);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test("🔴 Download: no curl on PATH is named as that, not as a failed URL", () => {
|
|
49
|
+
const empty = at("empty-bin");
|
|
50
|
+
mkdirSync(empty, { recursive: true });
|
|
51
|
+
const { d } = dl({ PATH: empty });
|
|
52
|
+
const r = d.fetch("https://example.test/banal", 10_000);
|
|
53
|
+
// Guards: a missing curl is its own diagnosis — once, it read "spawnSync curl ENOENT".
|
|
54
|
+
assert.deepEqual(!r.ok && r.error, { detail: "curl is not installed" });
|
|
55
|
+
});
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-memory adapters for the app layer's tests: every port, no disk, no processes, no network.
|
|
3
|
+
* Each one RECORDS what it was asked, so a test asserts on the calls instead of on side effects.
|
|
4
|
+
*/
|
|
5
|
+
import type { AbsolutePath } from "../../domain/paths.ts";
|
|
6
|
+
import type { Download } from "../../ports/download.ts";
|
|
7
|
+
import type { Files } from "../../ports/files.ts";
|
|
8
|
+
import type { Command, ProcessExit, RunProcess } from "../../ports/process.ts";
|
|
9
|
+
import type { NotAPromise, Scratch, Workspace } from "../../ports/workspace.ts";
|
|
10
|
+
import { err, ok } from "../../domain/result.ts";
|
|
11
|
+
|
|
12
|
+
export interface ScriptedProcess extends RunProcess {
|
|
13
|
+
readonly calls: Command[];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** A `RunProcess` that answers every `Command` with `script(c)` and records it. */
|
|
17
|
+
export function scriptedProcess(
|
|
18
|
+
script: (c: Command) => ProcessExit,
|
|
19
|
+
): ScriptedProcess {
|
|
20
|
+
const calls: Command[] = [];
|
|
21
|
+
return {
|
|
22
|
+
calls,
|
|
23
|
+
run(c) {
|
|
24
|
+
calls.push(c);
|
|
25
|
+
return script(c);
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** A process that exits 0 and prints `stdout`. */
|
|
31
|
+
export const exitedWith = (stdout: string, status = 0): ProcessExit => ({
|
|
32
|
+
kind: "exited",
|
|
33
|
+
status,
|
|
34
|
+
stdout,
|
|
35
|
+
stderr: "",
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
export interface MemoryFiles extends Files {
|
|
39
|
+
readonly map: Map<string, Uint8Array>;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function memoryFiles(
|
|
43
|
+
initial: Readonly<Record<string, string | Uint8Array>> = {},
|
|
44
|
+
): MemoryFiles {
|
|
45
|
+
const bytes = (v: string | Uint8Array) =>
|
|
46
|
+
typeof v === "string" ? new TextEncoder().encode(v) : v;
|
|
47
|
+
const map = new Map(
|
|
48
|
+
Object.entries(initial).map(([k, v]) => [k, bytes(v)] as const),
|
|
49
|
+
);
|
|
50
|
+
return {
|
|
51
|
+
map,
|
|
52
|
+
isFile: (p) => map.has(p),
|
|
53
|
+
readBytes: (p) => map.get(p) ?? null,
|
|
54
|
+
writeAtomic: (p, b) => void map.set(p, b),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface MemoryWorkspace extends Workspace {
|
|
59
|
+
/** Every file written into a scratch directory, in order. */
|
|
60
|
+
readonly written: {
|
|
61
|
+
readonly name: string;
|
|
62
|
+
readonly content: string;
|
|
63
|
+
readonly mode: "read" | "exec";
|
|
64
|
+
}[];
|
|
65
|
+
/** How each `within` ended. */
|
|
66
|
+
readonly ended: ("returned" | "threw")[];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function memoryWorkspace(dir = "/scratch"): MemoryWorkspace {
|
|
70
|
+
const written: MemoryWorkspace["written"] = [];
|
|
71
|
+
const ended: ("returned" | "threw")[] = [];
|
|
72
|
+
const scratch: Scratch = {
|
|
73
|
+
dir: dir as AbsolutePath,
|
|
74
|
+
write(name, content, mode) {
|
|
75
|
+
written.push({ name, content, mode });
|
|
76
|
+
return `${dir}/${name}` as AbsolutePath;
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
return {
|
|
80
|
+
written,
|
|
81
|
+
ended,
|
|
82
|
+
within<T>(_prefix: string, use: (s: Scratch) => NotAPromise<T>): T {
|
|
83
|
+
try {
|
|
84
|
+
const r = use(scratch);
|
|
85
|
+
ended.push("returned");
|
|
86
|
+
return r;
|
|
87
|
+
} catch (e) {
|
|
88
|
+
ended.push("threw");
|
|
89
|
+
throw e;
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export interface FixedDownload extends Download {
|
|
96
|
+
readonly urls: string[];
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** A `Download` that returns `body` for every URL, or fails with `detail` when `body` is a failure. */
|
|
100
|
+
export function fixedDownload(
|
|
101
|
+
body: string | Uint8Array | { readonly detail: string },
|
|
102
|
+
): FixedDownload {
|
|
103
|
+
const urls: string[] = [];
|
|
104
|
+
return {
|
|
105
|
+
urls,
|
|
106
|
+
fetch(url) {
|
|
107
|
+
urls.push(url);
|
|
108
|
+
if (typeof body === "string") return ok(new TextEncoder().encode(body));
|
|
109
|
+
return body instanceof Uint8Array ? ok(body) : err(body);
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Every generic port at once — what a test hands an adapter built over several of them. */
|
|
115
|
+
export interface MemoryPorts {
|
|
116
|
+
readonly run: RunProcess;
|
|
117
|
+
readonly files: Files;
|
|
118
|
+
readonly workspace: Workspace;
|
|
119
|
+
readonly download: Download;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** All four ports from in-memory parts; any part can be replaced. */
|
|
123
|
+
export function memoryPorts(over: Partial<MemoryPorts> = {}): MemoryPorts {
|
|
124
|
+
return {
|
|
125
|
+
run: scriptedProcess(() => exitedWith("")),
|
|
126
|
+
files: memoryFiles(),
|
|
127
|
+
workspace: memoryWorkspace(),
|
|
128
|
+
download: fixedDownload({ detail: "no network in memory" }),
|
|
129
|
+
...over,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/** `Files` over `node:fs`. */
|
|
2
|
+
import {
|
|
3
|
+
mkdirSync,
|
|
4
|
+
readFileSync,
|
|
5
|
+
renameSync,
|
|
6
|
+
rmSync,
|
|
7
|
+
statSync,
|
|
8
|
+
writeFileSync,
|
|
9
|
+
} from "node:fs";
|
|
10
|
+
import { dirname } from "node:path";
|
|
11
|
+
import type { AbsolutePath } from "../../domain/paths.ts";
|
|
12
|
+
import type { Files } from "../../ports/files.ts";
|
|
13
|
+
|
|
14
|
+
const code = (e: unknown): unknown =>
|
|
15
|
+
e instanceof Error ? (e as NodeJS.ErrnoException).code : undefined;
|
|
16
|
+
|
|
17
|
+
export const nodeFiles: Files = {
|
|
18
|
+
isFile(p: AbsolutePath): boolean {
|
|
19
|
+
return statSync(p, { throwIfNoEntry: false })?.isFile() ?? false;
|
|
20
|
+
},
|
|
21
|
+
readBytes(p: AbsolutePath): Uint8Array | null {
|
|
22
|
+
try {
|
|
23
|
+
return readFileSync(p);
|
|
24
|
+
} catch (e) {
|
|
25
|
+
if (code(e) === "ENOENT") return null;
|
|
26
|
+
throw e;
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
writeAtomic(p: AbsolutePath, bytes: Uint8Array): void {
|
|
30
|
+
const part = `${p}.part`;
|
|
31
|
+
mkdirSync(dirname(p), { recursive: true });
|
|
32
|
+
try {
|
|
33
|
+
writeFileSync(part, bytes);
|
|
34
|
+
renameSync(part, p);
|
|
35
|
+
} finally {
|
|
36
|
+
rmSync(part, { force: true });
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/** The `Files` contract against the real disk: absence is null, and writes go through `.part`. */
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { existsSync, mkdtempSync, realpathSync, rmSync } from "node:fs";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import { afterAll as after, test } from "vitest";
|
|
7
|
+
import type { AbsolutePath } from "../../domain/paths.ts";
|
|
8
|
+
import { nodeFiles } from "./files.io.ts";
|
|
9
|
+
|
|
10
|
+
const root = realpathSync(mkdtempSync(join(tmpdir(), "rpp-files-test-")));
|
|
11
|
+
after(() => rmSync(root, { recursive: true, force: true }));
|
|
12
|
+
const at = (...p: string[]) => join(root, ...p) as AbsolutePath;
|
|
13
|
+
|
|
14
|
+
test("Files: a missing file reads as null and is not a file", () => {
|
|
15
|
+
assert.equal(nodeFiles.readBytes(at("nope")), null);
|
|
16
|
+
assert.equal(nodeFiles.isFile(at("nope")), false);
|
|
17
|
+
assert.equal(nodeFiles.isFile(at()), false);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test("Files: writeAtomic creates the directory and leaves no .part behind", () => {
|
|
21
|
+
const p = at("a", "b", "banal");
|
|
22
|
+
nodeFiles.writeAtomic(p, new TextEncoder().encode("x"));
|
|
23
|
+
assert.equal(
|
|
24
|
+
new TextDecoder().decode(nodeFiles.readBytes(p) ?? undefined),
|
|
25
|
+
"x",
|
|
26
|
+
);
|
|
27
|
+
assert.equal(existsSync(`${p}.part`), false);
|
|
28
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the host knows and the environment does not say — the home, the temp directory, the cwd —
|
|
3
|
+
* read once. A composition root calls this and passes the value in; nothing below it reads the host
|
|
4
|
+
* again. Which of these a tool cares about, and what it derives from them, is that tool's adapter's
|
|
5
|
+
* business (banal's: `adapters/banal/settings.ts`).
|
|
6
|
+
*/
|
|
7
|
+
import { homedir, tmpdir } from "node:os";
|
|
8
|
+
import type { HostDirs } from "../../domain/host.ts";
|
|
9
|
+
|
|
10
|
+
export const hostDirs = (over: Partial<HostDirs> = {}): HostDirs => ({
|
|
11
|
+
home: homedir(),
|
|
12
|
+
tmp: tmpdir(),
|
|
13
|
+
cwd: process.cwd(),
|
|
14
|
+
...over,
|
|
15
|
+
});
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Node's implementations of the generic ports: processes, files, scratch directories. What a
|
|
3
|
+
* composition root builds and hands to the adapters that need them (banal's, for one).
|
|
4
|
+
*/
|
|
5
|
+
import type { Files } from "../../ports/files.ts";
|
|
6
|
+
import type { RunProcess } from "../../ports/process.ts";
|
|
7
|
+
import type { Workspace } from "../../ports/workspace.ts";
|
|
8
|
+
import { nodeFiles } from "./files.io.ts";
|
|
9
|
+
import { spawnProcess, type SpawnSync } from "./process.io.ts";
|
|
10
|
+
import { tmpWorkspace } from "./workspace.io.ts";
|
|
11
|
+
|
|
12
|
+
export interface NodeAdapterOptions {
|
|
13
|
+
/** Scratch directories go here. */
|
|
14
|
+
readonly tmpDir: string;
|
|
15
|
+
/** `spawnSync` or a stand-in with its shape. */
|
|
16
|
+
readonly spawn?: SpawnSync;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** The node ports, built once at the root. */
|
|
20
|
+
export interface NodePorts {
|
|
21
|
+
readonly run: RunProcess;
|
|
22
|
+
readonly files: Files;
|
|
23
|
+
readonly workspace: Workspace;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function nodeAdapters(o: NodeAdapterOptions): NodePorts {
|
|
27
|
+
return {
|
|
28
|
+
run: spawnProcess(o.spawn),
|
|
29
|
+
files: nodeFiles,
|
|
30
|
+
workspace: tmpWorkspace(o.tmpDir),
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export { nodeFiles } from "./files.io.ts";
|
|
35
|
+
export { spawnProcess, type SpawnSync } from "./process.io.ts";
|
|
36
|
+
export { hostDirs } from "./host.io.ts";
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `RunProcess` over Node's `spawnSync`. The one place that reads a child's result as Node reports
|
|
3
|
+
* it — `error`, `status`, `signal`, all nullable — and turns it into a `ProcessExit`, so no caller
|
|
4
|
+
* re-derives "what happened" from three fields.
|
|
5
|
+
*/
|
|
6
|
+
import {
|
|
7
|
+
spawnSync,
|
|
8
|
+
type SpawnSyncOptionsWithStringEncoding,
|
|
9
|
+
type SpawnSyncReturns,
|
|
10
|
+
} from "node:child_process";
|
|
11
|
+
import type { Command, ProcessExit, RunProcess } from "../../ports/process.ts";
|
|
12
|
+
|
|
13
|
+
/** Node's `spawnSync`, or a stand-in with its shape (a harness records the calls through one). */
|
|
14
|
+
export type SpawnSync = typeof spawnSync;
|
|
15
|
+
|
|
16
|
+
const errno = (r: SpawnSyncReturns<string>): string | undefined =>
|
|
17
|
+
(r.error as NodeJS.ErrnoException | undefined)?.code;
|
|
18
|
+
|
|
19
|
+
/** What a finished `spawnSync` came to. ENOENT is the executable itself: `c.file` was not found. */
|
|
20
|
+
export function exitOf(r: SpawnSyncReturns<string>, c: Command): ProcessExit {
|
|
21
|
+
const out = {
|
|
22
|
+
stdout: String(r.stdout ?? ""),
|
|
23
|
+
stderr: String(r.stderr ?? ""),
|
|
24
|
+
};
|
|
25
|
+
const code = errno(r);
|
|
26
|
+
if (code === "ENOENT") return { kind: "not-found", file: c.file };
|
|
27
|
+
if (code === "ETIMEDOUT")
|
|
28
|
+
return { kind: "timed-out", afterMs: c.timeoutMs, ...out };
|
|
29
|
+
if (r.error) return { kind: "spawn-failed", message: r.error.message };
|
|
30
|
+
if (r.signal) return { kind: "signalled", signal: r.signal, ...out };
|
|
31
|
+
return { kind: "exited", status: r.status ?? 1, ...out };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** A `RunProcess` that runs every `Command` with `spawn` (Node's `spawnSync` by default). */
|
|
35
|
+
export function spawnProcess(spawn: SpawnSync = spawnSync): RunProcess {
|
|
36
|
+
return {
|
|
37
|
+
run(c: Command): ProcessExit {
|
|
38
|
+
const opts: SpawnSyncOptionsWithStringEncoding = {
|
|
39
|
+
encoding: "utf8",
|
|
40
|
+
env: { ...c.env },
|
|
41
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
42
|
+
timeout: c.timeoutMs,
|
|
43
|
+
...(c.maxOutputBytes ? { maxBuffer: c.maxOutputBytes } : {}),
|
|
44
|
+
...(c.cwd ? { cwd: c.cwd } : {}),
|
|
45
|
+
};
|
|
46
|
+
return exitOf(spawn(c.file, [...c.args], opts), c);
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `RunProcess` contract, against the real `spawnSync`: what each way a program can end comes
|
|
3
|
+
* back as. The only process tests allowed real I/O — the core and app tests take `ProcessExit`
|
|
4
|
+
* literals instead.
|
|
5
|
+
*/
|
|
6
|
+
import assert from "node:assert/strict";
|
|
7
|
+
import { test } from "vitest";
|
|
8
|
+
import type { Command } from "../../ports/process.ts";
|
|
9
|
+
import { spawnProcess } from "./process.io.ts";
|
|
10
|
+
|
|
11
|
+
const run = spawnProcess();
|
|
12
|
+
const sh = (script: string, timeoutMs = 10_000): Command => ({
|
|
13
|
+
file: "/bin/sh",
|
|
14
|
+
args: ["-c", script],
|
|
15
|
+
env: { PATH: "/usr/bin:/bin" },
|
|
16
|
+
timeoutMs,
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test("a program that is not there is `not-found`, and names the program", () => {
|
|
20
|
+
// Guards: ENOENT is the executable itself — "perl is missing" rather than "banal failed".
|
|
21
|
+
assert.deepEqual(
|
|
22
|
+
run.run({ ...sh(""), file: "rpp-no-such-program-xyz", args: [] }),
|
|
23
|
+
{ kind: "not-found", file: "rpp-no-such-program-xyz" },
|
|
24
|
+
);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test("an exit status, stdout and stderr come back as they were", () => {
|
|
28
|
+
assert.deepEqual(run.run(sh("echo out; echo err >&2; exit 3")), {
|
|
29
|
+
kind: "exited",
|
|
30
|
+
status: 3,
|
|
31
|
+
stdout: "out\n",
|
|
32
|
+
stderr: "err\n",
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test("a program that outlives its time is `timed-out`, not an exit", () => {
|
|
37
|
+
const r = run.run(sh("sleep 5", 50));
|
|
38
|
+
// Guards: a hang ends, and says it hung — not "exit null".
|
|
39
|
+
assert.equal(r.kind, "timed-out");
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test("the environment is the Command's, whole: nothing leaks in from the parent", () => {
|
|
43
|
+
const r = run.run({ ...sh('echo "[$HOME][$X]"'), env: { X: "1" } });
|
|
44
|
+
// Guards: `Command.env` is the child's entire environment — the adapter merges nothing.
|
|
45
|
+
assert.deepEqual(r.kind === "exited" && r.stdout, "[][1]\n");
|
|
46
|
+
});
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `Workspace` over a temporary directory: `mkdtemp` + `realpath` (macOS's `/var` is a symlink to
|
|
3
|
+
* `/private/var`, and a path through it is not the one a child process reports) + `rm` in `finally`.
|
|
4
|
+
*/
|
|
5
|
+
import {
|
|
6
|
+
chmodSync,
|
|
7
|
+
mkdtempSync,
|
|
8
|
+
realpathSync,
|
|
9
|
+
rmSync,
|
|
10
|
+
writeFileSync,
|
|
11
|
+
} from "node:fs";
|
|
12
|
+
import { join } from "node:path";
|
|
13
|
+
import type { AbsolutePath } from "../../domain/paths.ts";
|
|
14
|
+
import type { NotAPromise, Scratch, Workspace } from "../../ports/workspace.ts";
|
|
15
|
+
|
|
16
|
+
function scratchIn(dir: AbsolutePath): Scratch {
|
|
17
|
+
return {
|
|
18
|
+
dir,
|
|
19
|
+
write(name, content, mode): AbsolutePath {
|
|
20
|
+
const at = join(dir, name) as AbsolutePath;
|
|
21
|
+
writeFileSync(at, content);
|
|
22
|
+
if (mode === "exec") chmodSync(at, 0o755);
|
|
23
|
+
return at;
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Scratch directories under `tmpDir`. */
|
|
29
|
+
export function tmpWorkspace(tmpDir: string): Workspace {
|
|
30
|
+
return {
|
|
31
|
+
within<T>(prefix: string, use: (s: Scratch) => NotAPromise<T>): T {
|
|
32
|
+
const dir = realpathSync(mkdtempSync(join(tmpDir, prefix)));
|
|
33
|
+
try {
|
|
34
|
+
return use(scratchIn(dir as AbsolutePath));
|
|
35
|
+
} finally {
|
|
36
|
+
rmSync(dir, { recursive: true, force: true });
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
}
|