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,216 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Both halves for `new-paper.ts` — the module behind `paperlint new` and `paperlint init --paper`.
|
|
3
|
+
*
|
|
4
|
+
* What is checked, and why each one matters:
|
|
5
|
+
* - the scaffold is what `paperlint lint` ACCEPTS: the old first run opened with "missing
|
|
6
|
+
* PIPELINE-STATUS.md", and a scaffold that still produced a finding would move that error
|
|
7
|
+
* from a missing file into a created one;
|
|
8
|
+
* - the scorecard carries `researchQuestion` and NO `stages` — a new paper has shipped nothing,
|
|
9
|
+
* and a placeholder stage would be a red `paper/stages` on the very first run;
|
|
10
|
+
* - the scorecard PARSES as a scorecard (`pipeline-check.mjs` finds its four sections). The
|
|
11
|
+
* template used to live in a code fence; moved into a file, it must still be the format the
|
|
12
|
+
* skills' checker reads;
|
|
13
|
+
* - it never overwrites, and on an existing folder adds only what is missing;
|
|
14
|
+
* - the project's `<papers>/.template/` wins over the package, file by file.
|
|
15
|
+
*
|
|
16
|
+
* ⚠️ Assertions at the TOP LEVEL: `vigiles test` imports the file and counts "did not throw"
|
|
17
|
+
* as a pass.
|
|
18
|
+
*/
|
|
19
|
+
import assert from "node:assert/strict";
|
|
20
|
+
import {
|
|
21
|
+
existsSync,
|
|
22
|
+
mkdirSync,
|
|
23
|
+
mkdtempSync,
|
|
24
|
+
readFileSync,
|
|
25
|
+
realpathSync,
|
|
26
|
+
rmSync,
|
|
27
|
+
writeFileSync,
|
|
28
|
+
} from "node:fs";
|
|
29
|
+
import { spawnSync } from "node:child_process";
|
|
30
|
+
import { tmpdir } from "node:os";
|
|
31
|
+
import { dirname, join } from "node:path";
|
|
32
|
+
import { fileURLToPath } from "node:url";
|
|
33
|
+
import { load } from "js-yaml";
|
|
34
|
+
import { frontmatterBlock } from "../lib/markdown.mjs";
|
|
35
|
+
|
|
36
|
+
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
37
|
+
const ROOT = dirname(HERE);
|
|
38
|
+
const {
|
|
39
|
+
newPaper,
|
|
40
|
+
nameProblem,
|
|
41
|
+
wantedFiles,
|
|
42
|
+
PACKAGE_TEMPLATES,
|
|
43
|
+
OVERRIDE_DIR,
|
|
44
|
+
STATUS_FILE,
|
|
45
|
+
SOURCE_FILE,
|
|
46
|
+
} = await import(join(HERE, "new-paper.ts"));
|
|
47
|
+
const { run } = await import(join(HERE, "cli.ts"));
|
|
48
|
+
|
|
49
|
+
let n = 0;
|
|
50
|
+
const check = (label, cond) => {
|
|
51
|
+
n++;
|
|
52
|
+
assert.ok(cond, label);
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const work = realpathSync(mkdtempSync(join(tmpdir(), "rpp-new-")));
|
|
56
|
+
const read = (...p) => readFileSync(join(...p), "utf8");
|
|
57
|
+
const front = (text) => load(frontmatterBlock(text) ?? "") ?? {};
|
|
58
|
+
|
|
59
|
+
/** `paperlint lint <dir>` in-process, output captured. */
|
|
60
|
+
async function lint(dir) {
|
|
61
|
+
const out = [];
|
|
62
|
+
const code = await run(["lint", dir], {
|
|
63
|
+
log: (...a) => out.push(a.join(" ")),
|
|
64
|
+
err: (...a) => out.push(a.join(" ")),
|
|
65
|
+
cwd: work,
|
|
66
|
+
});
|
|
67
|
+
return { code, text: out.join("\n") };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
try {
|
|
71
|
+
// ── the templates are FILES in the package, one per required file ──────────────────────
|
|
72
|
+
for (const f of [STATUS_FILE, SOURCE_FILE.tex, SOURCE_FILE.md])
|
|
73
|
+
check(
|
|
74
|
+
`the package ships templates/paper/${f}`,
|
|
75
|
+
existsSync(join(PACKAGE_TEMPLATES, f)),
|
|
76
|
+
);
|
|
77
|
+
check(
|
|
78
|
+
"🔴 the templates ship in the npm tarball — `templates` is in package.json `files`",
|
|
79
|
+
JSON.parse(read(ROOT, "package.json")).files.includes("templates"),
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
// ── names ───────────────────────────────────────────────────────────────────────────
|
|
83
|
+
for (const ok of ["demo", "aisec-2026", "v1.2_final"])
|
|
84
|
+
check(`\`${ok}\` is a valid name`, nameProblem(ok) === null);
|
|
85
|
+
for (const bad of ["Demo", "my paper", "a/b", "../x", "", "ÿ"])
|
|
86
|
+
check(`\`${bad}\` is refused`, typeof nameProblem(bad) === "string");
|
|
87
|
+
for (const dot of [".template", ".x", ".."])
|
|
88
|
+
check(
|
|
89
|
+
`\`${dot}\` is refused — discovery skips dot-directories, so it would never be linted`,
|
|
90
|
+
/starting with a dot/.test(nameProblem(dot) ?? ""),
|
|
91
|
+
);
|
|
92
|
+
check(
|
|
93
|
+
"a refused name creates nothing",
|
|
94
|
+
newPaper(work, "Bad", "tex").ok === false && !existsSync(join(work, "Bad")),
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
// ── a fresh LaTeX paper ─────────────────────────────────────────────────────────────
|
|
98
|
+
const papers = join(work, "papers");
|
|
99
|
+
const r = newPaper(papers, "demo", "tex");
|
|
100
|
+
check(
|
|
101
|
+
"a fresh folder gets the scorecard and a .tex source, both from the package",
|
|
102
|
+
r.ok &&
|
|
103
|
+
r.fresh &&
|
|
104
|
+
r.files.map((f) => `${f.file}:${f.status}:${f.from}`).join(" ") ===
|
|
105
|
+
"PIPELINE-STATUS.md:created:package paper.tex:created:package",
|
|
106
|
+
);
|
|
107
|
+
const status = read(papers, "demo", STATUS_FILE);
|
|
108
|
+
const fm = front(status);
|
|
109
|
+
check(
|
|
110
|
+
"🔴 the scorecard has NO `stages` — a new paper has shipped nothing and owes nothing",
|
|
111
|
+
!("stages" in fm),
|
|
112
|
+
);
|
|
113
|
+
check(
|
|
114
|
+
"and carries `researchQuestion`, present and empty",
|
|
115
|
+
"researchQuestion" in fm && fm.researchQuestion === "",
|
|
116
|
+
);
|
|
117
|
+
check(
|
|
118
|
+
"the name is filled in, and no placeholder survives in any file",
|
|
119
|
+
/PIPELINE-STATUS — demo/.test(status) &&
|
|
120
|
+
!status.includes("{{name}}") &&
|
|
121
|
+
!read(papers, "demo", "paper.tex").includes("{{name}}"),
|
|
122
|
+
);
|
|
123
|
+
const l = await lint(join(papers, "demo"));
|
|
124
|
+
check(
|
|
125
|
+
"🔴 `paperlint lint` passes the scaffold clean — the first run is green, not a missing-file error",
|
|
126
|
+
l.code === 0 && /no findings/.test(l.text),
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
// The format contract of the skills' checker: four sections, parsed.
|
|
130
|
+
const pc = spawnSync(
|
|
131
|
+
process.execPath,
|
|
132
|
+
[
|
|
133
|
+
join(ROOT, "skills", "paper-pipeline", "scripts", "pipeline-check.mjs"),
|
|
134
|
+
join(papers, "demo"),
|
|
135
|
+
"--json",
|
|
136
|
+
"--today=2026-09-23",
|
|
137
|
+
],
|
|
138
|
+
{ cwd: work, encoding: "utf8" },
|
|
139
|
+
);
|
|
140
|
+
let parsed = null;
|
|
141
|
+
try {
|
|
142
|
+
parsed = JSON.parse(pc.stdout);
|
|
143
|
+
} catch {
|
|
144
|
+
parsed = null;
|
|
145
|
+
}
|
|
146
|
+
check(
|
|
147
|
+
"the scorecard PARSES for pipeline-check — its four sections are found (it prints JSON, not the format warning)",
|
|
148
|
+
Array.isArray(parsed) && !/expected the four sections/.test(pc.stderr),
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
// ── never overwrite; on an existing folder only the missing files ─────────────────────
|
|
152
|
+
writeFileSync(join(papers, "demo", STATUS_FILE), "mine\n");
|
|
153
|
+
const again = newPaper(papers, "demo", "md");
|
|
154
|
+
check(
|
|
155
|
+
"🔴 a second run overwrites nothing and adds no second source",
|
|
156
|
+
again.ok &&
|
|
157
|
+
!again.fresh &&
|
|
158
|
+
read(papers, "demo", STATUS_FILE) === "mine\n" &&
|
|
159
|
+
!existsSync(join(papers, "demo", "paper.md")) &&
|
|
160
|
+
again.files.every((f) => f.status === "kept"),
|
|
161
|
+
);
|
|
162
|
+
mkdirSync(join(papers, "old"), { recursive: true });
|
|
163
|
+
writeFileSync(join(papers, "old", "paper.md"), "# Old\n");
|
|
164
|
+
const migrated = newPaper(papers, "old", "tex");
|
|
165
|
+
check(
|
|
166
|
+
"an old folder with only paper.md gets ONLY the scorecard — that is the migration",
|
|
167
|
+
migrated.ok &&
|
|
168
|
+
existsSync(join(papers, "old", STATUS_FILE)) &&
|
|
169
|
+
!existsSync(join(papers, "old", "paper.tex")) &&
|
|
170
|
+
read(papers, "old", "paper.md") === "# Old\n",
|
|
171
|
+
);
|
|
172
|
+
check(
|
|
173
|
+
"wantedFiles asks for a source only when neither format is there",
|
|
174
|
+
wantedFiles(join(papers, "old"), "tex").join() === STATUS_FILE &&
|
|
175
|
+
wantedFiles(join(papers, "nowhere"), "md").join() ===
|
|
176
|
+
`${STATUS_FILE},paper.md`,
|
|
177
|
+
);
|
|
178
|
+
writeFileSync(join(papers, "afile"), "x");
|
|
179
|
+
check(
|
|
180
|
+
"a name taken by a FILE is refused, not written through",
|
|
181
|
+
newPaper(papers, "afile", "tex").ok === false,
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
// ── the project's template wins, file by file ─────────────────────────────────────────
|
|
185
|
+
mkdirSync(join(papers, OVERRIDE_DIR), { recursive: true });
|
|
186
|
+
writeFileSync(
|
|
187
|
+
join(papers, OVERRIDE_DIR, STATUS_FILE),
|
|
188
|
+
"---\n---\n# House scorecard for {{name}}\n",
|
|
189
|
+
);
|
|
190
|
+
const own = newPaper(papers, "house", "md");
|
|
191
|
+
check(
|
|
192
|
+
"🔴 <papers>/.template/ is preferred for the file it holds, the package fills the rest",
|
|
193
|
+
own.ok &&
|
|
194
|
+
read(papers, "house", STATUS_FILE) ===
|
|
195
|
+
"---\n---\n# House scorecard for house\n" &&
|
|
196
|
+
own.files.find((f) => f.file === STATUS_FILE)?.from === "project" &&
|
|
197
|
+
own.files.find((f) => f.file === "paper.md")?.from === "package",
|
|
198
|
+
);
|
|
199
|
+
|
|
200
|
+
// ── a missing template is a broken install, not an empty file ─────────────────────────
|
|
201
|
+
const broken = newPaper(join(work, "b"), "x", "tex", {
|
|
202
|
+
packageTemplates: join(work, "no-such-dir"),
|
|
203
|
+
});
|
|
204
|
+
check(
|
|
205
|
+
"a missing package template refuses and leaves no half-made folder",
|
|
206
|
+
broken.ok === false &&
|
|
207
|
+
/no template for/.test(broken.reason) &&
|
|
208
|
+
!existsSync(join(work, "b", "x")),
|
|
209
|
+
);
|
|
210
|
+
} finally {
|
|
211
|
+
rmSync(work, { recursive: true, force: true });
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
console.log(
|
|
215
|
+
`✓ ${String(n)} assertions passed — paperlint new scaffolds what paperlint lint accepts`,
|
|
216
|
+
);
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Battery for `new-paper.ts`.
|
|
3
|
+
*
|
|
4
|
+
* Every case reintroduces a way `paperlint new` damages a paper folder or produces one the linter
|
|
5
|
+
* refuses — the first a loss nobody notices until the text is gone, the second the exact
|
|
6
|
+
* "missing file on the first run" this command was written to end.
|
|
7
|
+
*/
|
|
8
|
+
import { fileURLToPath } from "node:url";
|
|
9
|
+
import { dirname, join, resolve } from "node:path";
|
|
10
|
+
import { runMutations } from "../lib/mutation-driver.mjs";
|
|
11
|
+
|
|
12
|
+
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
13
|
+
const ROOT = resolve(HERE, "..");
|
|
14
|
+
const SRC = join(HERE, "new-paper.ts");
|
|
15
|
+
const HARNESS = join(HERE, "new-paper.harness.mjs");
|
|
16
|
+
|
|
17
|
+
process.exit(
|
|
18
|
+
runMutations({
|
|
19
|
+
root: ROOT,
|
|
20
|
+
runner: "node",
|
|
21
|
+
cases: [
|
|
22
|
+
{
|
|
23
|
+
name: "an existing file is overwritten",
|
|
24
|
+
harness: HARNESS,
|
|
25
|
+
expect: "a second run overwrites nothing",
|
|
26
|
+
disables:
|
|
27
|
+
"the one promise that makes `paperlint new` safe on an existing folder. A scorecard with a " +
|
|
28
|
+
"year of journal in it is replaced by the empty template",
|
|
29
|
+
edits: [
|
|
30
|
+
[
|
|
31
|
+
SRC,
|
|
32
|
+
' if (existsSync(join(dir, file))) {\n files.push({ file, status: "kept" });\n continue;\n }\n',
|
|
33
|
+
"",
|
|
34
|
+
],
|
|
35
|
+
[SRC, '{ encoding: "utf8", flag: "wx" }', '{ encoding: "utf8" }'],
|
|
36
|
+
],
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
name: "a second source is added beside an existing one",
|
|
40
|
+
harness: HARNESS,
|
|
41
|
+
// The first owner is the re-run on a .tex paper asked for md: it must add no paper.md.
|
|
42
|
+
expect: "adds no second source",
|
|
43
|
+
disables:
|
|
44
|
+
"the migration of an old folder: a paper written in Markdown gains a stub paper.tex, " +
|
|
45
|
+
"and every tex rule starts linting a file the author never wrote",
|
|
46
|
+
edits: [
|
|
47
|
+
[
|
|
48
|
+
SRC,
|
|
49
|
+
" return hasSource ? [STATUS_FILE] : [STATUS_FILE, SOURCE_FILE[format]];",
|
|
50
|
+
" return [STATUS_FILE, SOURCE_FILE[format]];",
|
|
51
|
+
],
|
|
52
|
+
],
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
name: "the project's template is ignored",
|
|
56
|
+
harness: HARNESS,
|
|
57
|
+
expect: "is preferred for the file it holds",
|
|
58
|
+
disables:
|
|
59
|
+
"the override slot. A project with its own richer scorecard gets the package's every time",
|
|
60
|
+
edits: [
|
|
61
|
+
[
|
|
62
|
+
SRC,
|
|
63
|
+
" const [src, from] = existsSync(project)",
|
|
64
|
+
" const [src, from] = false",
|
|
65
|
+
],
|
|
66
|
+
],
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
name: "a leading dot is accepted",
|
|
70
|
+
harness: HARNESS,
|
|
71
|
+
expect: "discovery skips dot-directories",
|
|
72
|
+
disables:
|
|
73
|
+
"the refusal of a name that would be created and then never linted — the scaffold " +
|
|
74
|
+
"would sit beside the papers, invisible to every check",
|
|
75
|
+
edits: [[SRC, ' if (name.startsWith("."))', " if (false)"]],
|
|
76
|
+
},
|
|
77
|
+
],
|
|
78
|
+
}),
|
|
79
|
+
);
|
package/src/new-paper.ts
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `paperlint new <name>` — a paper folder the linter accepts, made from a template FILE.
|
|
3
|
+
*
|
|
4
|
+
* 🔴 WHY THIS EXISTS. `paperlint lint` requires `PIPELINE-STATUS.md` in every paper folder, and until
|
|
5
|
+
* this command nothing made one: the template sat inside a fenced block of a reference page and a
|
|
6
|
+
* human copied it. The README's own "First run" example opened with the error for the missing file.
|
|
7
|
+
*
|
|
8
|
+
* Every rule below is taken from a tool that already solved it (docs/prior-art/paper-folder-scaffolding.md):
|
|
9
|
+
* - the template is a FILE with a lookup order — the project's `<papers>/.template/<file>` first,
|
|
10
|
+
* the package's `templates/paper/<file>` second (Hugo's archetypes);
|
|
11
|
+
* - it NEVER overwrites: on an existing folder it adds only the missing required files, which is
|
|
12
|
+
* also the migration for an old folder that has only `paper.md` (`cargo init`);
|
|
13
|
+
* - the name is validated, because it becomes a path and, through the hooks, a shell argument.
|
|
14
|
+
*
|
|
15
|
+
* The only substitution is `{{name}}` → the folder name. Nothing else in a template is touched.
|
|
16
|
+
*/
|
|
17
|
+
/* eslint-disable boundaries/dependencies -- legacy I/O, moves behind a port in #76 */
|
|
18
|
+
import {
|
|
19
|
+
existsSync,
|
|
20
|
+
mkdirSync,
|
|
21
|
+
readFileSync,
|
|
22
|
+
statSync,
|
|
23
|
+
writeFileSync,
|
|
24
|
+
} from "node:fs";
|
|
25
|
+
/* eslint-enable boundaries/dependencies */
|
|
26
|
+
import { join } from "node:path";
|
|
27
|
+
import { fileURLToPath } from "node:url";
|
|
28
|
+
|
|
29
|
+
export type PaperFormat = "tex" | "md";
|
|
30
|
+
export const FORMATS: readonly PaperFormat[] = ["tex", "md"];
|
|
31
|
+
export const DEFAULT_FORMAT: PaperFormat = "tex";
|
|
32
|
+
|
|
33
|
+
/** The package's default templates. Read from disk, so they ship in the tarball (`files`). */
|
|
34
|
+
export const PACKAGE_TEMPLATES = fileURLToPath(
|
|
35
|
+
new URL("../templates/paper/", import.meta.url),
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* The project's override directory, inside the papers directory. It starts with a dot, and that
|
|
40
|
+
* is what keeps it from being a paper: discovery (`papersIn`, `checkStructure`, `detectPapers`)
|
|
41
|
+
* skips dot-directories, and `buildConfig` ignores it for ESLint.
|
|
42
|
+
*/
|
|
43
|
+
export const OVERRIDE_DIR = ".template";
|
|
44
|
+
|
|
45
|
+
export const STATUS_FILE = "PIPELINE-STATUS.md";
|
|
46
|
+
export const SOURCE_FILE: Readonly<Record<PaperFormat, string>> = {
|
|
47
|
+
tex: "paper.tex",
|
|
48
|
+
md: "paper.md",
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export const isFormat = (v: unknown): v is PaperFormat =>
|
|
52
|
+
FORMATS.includes(v as PaperFormat);
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Why a name is refused, or null. `[a-z0-9._-]+` is the charset; a LEADING dot is refused on top
|
|
56
|
+
* of it, because discovery skips dot-directories — a paper named `.x` would be created and then
|
|
57
|
+
* never linted, which is worse than a refusal.
|
|
58
|
+
*/
|
|
59
|
+
export function nameProblem(name: string): string | null {
|
|
60
|
+
if (!/^[a-z0-9._-]+$/.test(name))
|
|
61
|
+
return `\`${name}\` — a paper name may hold only a-z, 0-9, dot, underscore and hyphen (it becomes a path and a shell argument)`;
|
|
62
|
+
if (name.startsWith("."))
|
|
63
|
+
return `\`${name}\` — a name starting with a dot is skipped by paper discovery, so it would never be linted`;
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface FileOutcome {
|
|
68
|
+
readonly file: string;
|
|
69
|
+
/** `created` from a template, `kept` because it was already there. */
|
|
70
|
+
readonly status: "created" | "kept";
|
|
71
|
+
/** Which template it came from, for a created file. */
|
|
72
|
+
readonly from?: "project" | "package";
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export type NewPaperResult =
|
|
76
|
+
| {
|
|
77
|
+
readonly ok: true;
|
|
78
|
+
readonly dir: string;
|
|
79
|
+
/** The folder did not exist before this run. */
|
|
80
|
+
readonly fresh: boolean;
|
|
81
|
+
readonly files: readonly FileOutcome[];
|
|
82
|
+
}
|
|
83
|
+
| { readonly ok: false; readonly reason: string };
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* The files a paper folder must end up with: the scorecard, plus a source in `format` — unless
|
|
87
|
+
* it already has a source in EITHER format, in which case that one stands and nothing is added.
|
|
88
|
+
*/
|
|
89
|
+
export function wantedFiles(dir: string, format: PaperFormat): string[] {
|
|
90
|
+
const hasSource = Object.values(SOURCE_FILE).some((f) =>
|
|
91
|
+
existsSync(join(dir, f)),
|
|
92
|
+
);
|
|
93
|
+
return hasSource ? [STATUS_FILE] : [STATUS_FILE, SOURCE_FILE[format]];
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Documented in README.md#starting-a-paper — update it when this changes.
|
|
97
|
+
export function newPaper(
|
|
98
|
+
papersRoot: string,
|
|
99
|
+
name: string,
|
|
100
|
+
format: PaperFormat,
|
|
101
|
+
{ packageTemplates = PACKAGE_TEMPLATES }: { packageTemplates?: string } = {},
|
|
102
|
+
): NewPaperResult {
|
|
103
|
+
const problem = nameProblem(name);
|
|
104
|
+
if (problem) return { ok: false, reason: problem };
|
|
105
|
+
const dir = join(papersRoot, name);
|
|
106
|
+
const fresh = !existsSync(dir);
|
|
107
|
+
if (!fresh && !statSync(dir).isDirectory())
|
|
108
|
+
return { ok: false, reason: `${dir} exists and is not a directory` };
|
|
109
|
+
|
|
110
|
+
const files: FileOutcome[] = [];
|
|
111
|
+
const toWrite: { file: string; text: string; from: "project" | "package" }[] =
|
|
112
|
+
[];
|
|
113
|
+
for (const file of wantedFiles(dir, format)) {
|
|
114
|
+
if (existsSync(join(dir, file))) {
|
|
115
|
+
files.push({ file, status: "kept" });
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
const project = join(papersRoot, OVERRIDE_DIR, file);
|
|
119
|
+
const fromPackage = join(packageTemplates, file);
|
|
120
|
+
const [src, from] = existsSync(project)
|
|
121
|
+
? [project, "project" as const]
|
|
122
|
+
: [fromPackage, "package" as const];
|
|
123
|
+
// A missing package template is a broken install, not a reason to write an empty file.
|
|
124
|
+
if (!existsSync(src))
|
|
125
|
+
return {
|
|
126
|
+
ok: false,
|
|
127
|
+
reason: `no template for ${file}: ${src} is missing`,
|
|
128
|
+
};
|
|
129
|
+
const text = readFileSync(src, "utf8").split("{{name}}").join(name);
|
|
130
|
+
toWrite.push({ file, text, from });
|
|
131
|
+
}
|
|
132
|
+
// Read every template before writing any file, so a missing one leaves no half-made folder.
|
|
133
|
+
mkdirSync(dir, { recursive: true });
|
|
134
|
+
for (const { file, text, from } of toWrite) {
|
|
135
|
+
// `wx`: fail rather than overwrite, even if something appeared since the check above.
|
|
136
|
+
writeFileSync(join(dir, file), text, { encoding: "utf8", flag: "wx" });
|
|
137
|
+
files.push({ file, status: "created", from });
|
|
138
|
+
}
|
|
139
|
+
return { ok: true, dir, fresh, files };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** The report lines, shared by `paperlint new` and the first-paper offer in `paperlint init`. */
|
|
143
|
+
export function reportNewPaper(
|
|
144
|
+
result: NewPaperResult,
|
|
145
|
+
here: (p: string) => string,
|
|
146
|
+
): string[] {
|
|
147
|
+
if (!result.ok) return [` ✗ ${result.reason}`];
|
|
148
|
+
const out = [
|
|
149
|
+
` ${result.fresh ? "✓ created" : "✓ already there, only missing files added:"} ${here(result.dir)}`,
|
|
150
|
+
];
|
|
151
|
+
for (const f of result.files)
|
|
152
|
+
out.push(
|
|
153
|
+
f.status === "created"
|
|
154
|
+
? ` + ${f.file} (from the ${f.from === "project" ? `project's ${OVERRIDE_DIR}/` : "package"} template)`
|
|
155
|
+
: ` = ${f.file} (kept — never overwritten)`,
|
|
156
|
+
);
|
|
157
|
+
return out;
|
|
158
|
+
}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `pdf-facts.ts` — pdf.js reading REAL PDFs, committed under `fixtures/pdf-facts/`.
|
|
3
|
+
*
|
|
4
|
+
* The pure decisions are tested in `pdf-geometry.harness.mjs` with hand-written words. This file is
|
|
5
|
+
* the other half: that the shell asks pdf.js the right questions, with the right options, and maps
|
|
6
|
+
* what comes back. The expected numbers are the ones poppler reported for the same files in the
|
|
7
|
+
* 2026-09-24 measurement (issue #61), so a pass here is agreement with the tool this replaced.
|
|
8
|
+
*/
|
|
9
|
+
import assert from "node:assert/strict";
|
|
10
|
+
import { writeFileSync, mkdtempSync, realpathSync, rmSync } from "node:fs";
|
|
11
|
+
import { tmpdir } from "node:os";
|
|
12
|
+
import { dirname, join, resolve } from "node:path";
|
|
13
|
+
import { fileURLToPath } from "node:url";
|
|
14
|
+
|
|
15
|
+
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
16
|
+
const FIX = resolve(HERE, "..", "fixtures", "pdf-facts");
|
|
17
|
+
const { readPdf } = await import(join(HERE, "pdf-facts.ts"));
|
|
18
|
+
const { classifyLastPage } = await import(join(HERE, "pdf-geometry.ts"));
|
|
19
|
+
|
|
20
|
+
let n = 0;
|
|
21
|
+
const check = (label, cond, detail = "") => {
|
|
22
|
+
assert.ok(cond, detail ? `${label} — ${detail}` : label);
|
|
23
|
+
n++;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const read = async (name) => {
|
|
27
|
+
const r = await readPdf(join(FIX, name));
|
|
28
|
+
assert.ok(r.ok, `${name} must read: ${JSON.stringify(r)}`);
|
|
29
|
+
return r.facts;
|
|
30
|
+
};
|
|
31
|
+
const kinds = (facts) =>
|
|
32
|
+
facts.fonts.kind === "drawn" ? facts.fonts.list.map((f) => f.kind) : [];
|
|
33
|
+
const named = (facts, name) =>
|
|
34
|
+
facts.fonts.kind === "drawn"
|
|
35
|
+
? facts.fonts.list.find((f) => f.name === name)
|
|
36
|
+
: undefined;
|
|
37
|
+
|
|
38
|
+
// ── t3-all: every text font is Type 3 ──────────────────────────────────────────────────
|
|
39
|
+
{
|
|
40
|
+
const f = await read("t3-all.pdf");
|
|
41
|
+
check(
|
|
42
|
+
"t3-all: four Type 3 fonts, as poppler lists — including one no text item names",
|
|
43
|
+
kinds(f).length === 4 && kinds(f).every((k) => k === "type3"),
|
|
44
|
+
JSON.stringify(f.fonts),
|
|
45
|
+
);
|
|
46
|
+
const last = classifyLastPage(f.last);
|
|
47
|
+
check(
|
|
48
|
+
"t3-all: one page, measured, within 7.2 pt of poppler's 150.1 / 553.2",
|
|
49
|
+
f.pages === 1 &&
|
|
50
|
+
last.kind === "measured" &&
|
|
51
|
+
Math.abs(last.columns[0] - 150.1) <= 7.2 &&
|
|
52
|
+
Math.abs(last.columns[1] - 553.2) <= 7.2,
|
|
53
|
+
JSON.stringify(last),
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ── ttf: a LuaLaTeX PDF, composite TrueType ────────────────────────────────────────────
|
|
58
|
+
{
|
|
59
|
+
const f = await read("ttf.pdf");
|
|
60
|
+
const arsenal = named(f, "Arsenal SC Regular");
|
|
61
|
+
check(
|
|
62
|
+
"ttf: the composite TrueType font is CIDTrueType — the program is read, not guessed",
|
|
63
|
+
arsenal?.kind === "embedded" && arsenal.program === "CIDTrueType",
|
|
64
|
+
JSON.stringify(f.fonts),
|
|
65
|
+
);
|
|
66
|
+
check(
|
|
67
|
+
"ttf: two pages, and the 8-word last page is a stub",
|
|
68
|
+
f.pages === 2 && classifyLastPage(f.last).kind === "stub",
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// ── t3-mixed: the sub/uni bug of the poppler reader ────────────────────────────────────
|
|
73
|
+
{
|
|
74
|
+
const f = await read("t3-mixed.pdf");
|
|
75
|
+
// pdffonts prints this font as `Times-Roman Type 1 Custom no no yes`. The old reader
|
|
76
|
+
// tested `yes` anywhere in the row, found the `uni` column's, and called the font embedded.
|
|
77
|
+
const times = named(f, "Times-Roman");
|
|
78
|
+
check(
|
|
79
|
+
"🔴 t3-mixed: Times-Roman (pdffonts: `no no yes`) is NOT embedded",
|
|
80
|
+
times?.kind === "not-embedded",
|
|
81
|
+
JSON.stringify(times),
|
|
82
|
+
);
|
|
83
|
+
const cols = classifyLastPage(f.last);
|
|
84
|
+
check(
|
|
85
|
+
"t3-mixed: columns within 0.2 pt of poppler's 153.5 / 554.9 — the fonts' own ascent is used",
|
|
86
|
+
cols.kind === "measured" &&
|
|
87
|
+
Math.abs(cols.columns[0] - 153.5) <= 0.2 &&
|
|
88
|
+
Math.abs(cols.columns[1] - 554.9) <= 0.2,
|
|
89
|
+
JSON.stringify(cols),
|
|
90
|
+
);
|
|
91
|
+
check(
|
|
92
|
+
"t3-mixed: four fonts — two embedded Type 1, one Type 3, one not embedded",
|
|
93
|
+
JSON.stringify(kinds(f)) ===
|
|
94
|
+
JSON.stringify(["embedded", "embedded", "type3", "not-embedded"]),
|
|
95
|
+
JSON.stringify(f.fonts),
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// ── corrupt-font: poppler says embedded, the program does not parse ────────────────────
|
|
100
|
+
{
|
|
101
|
+
const f = await read("corrupt-font.pdf");
|
|
102
|
+
check(
|
|
103
|
+
"corrupt-font: a font program that does not parse counts as not embedded",
|
|
104
|
+
named(f, "CMR10")?.kind === "not-embedded" &&
|
|
105
|
+
named(f, "CMBX12")?.kind === "embedded",
|
|
106
|
+
JSON.stringify(f.fonts),
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// ── what cannot be read says so ─────────────────────────────────────────────────────────
|
|
111
|
+
{
|
|
112
|
+
const r = await readPdf(join(FIX, "encrypted.pdf"));
|
|
113
|
+
check(
|
|
114
|
+
"an encrypted PDF is `encrypted`, not a crash",
|
|
115
|
+
!r.ok && r.reason === "encrypted",
|
|
116
|
+
JSON.stringify(r),
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
const tmp = realpathSync(mkdtempSync(join(tmpdir(), "rpp-pdf-facts-")));
|
|
120
|
+
try {
|
|
121
|
+
const notPdf = join(tmp, "paper.pdf");
|
|
122
|
+
writeFileSync(notPdf, "%PDF-stale\n");
|
|
123
|
+
const bad = await readPdf(notPdf);
|
|
124
|
+
check(
|
|
125
|
+
"a file that is not a PDF is `unreadable`, with pdf.js's reason",
|
|
126
|
+
!bad.ok && bad.reason === "unreadable" && /PDF/.test(bad.detail),
|
|
127
|
+
JSON.stringify(bad),
|
|
128
|
+
);
|
|
129
|
+
const missing = await readPdf(join(tmp, "nope.pdf"));
|
|
130
|
+
check(
|
|
131
|
+
"a missing file is `unreadable`, not an exception",
|
|
132
|
+
!missing.ok && missing.reason === "unreadable",
|
|
133
|
+
JSON.stringify(missing),
|
|
134
|
+
);
|
|
135
|
+
} finally {
|
|
136
|
+
rmSync(tmp, { recursive: true, force: true });
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// ── hidden-text: the boxes banal is fed ────────────────────────────────────────────────
|
|
140
|
+
{
|
|
141
|
+
const f = await read("hidden-text.pdf");
|
|
142
|
+
const boxes = f.layout.flatMap((p, i) =>
|
|
143
|
+
p.boxes.map((b) => ({ ...b, page: i + 1 })),
|
|
144
|
+
);
|
|
145
|
+
const chars = (pred) =>
|
|
146
|
+
boxes.filter(pred).reduce((sum, b) => sum + b.text.length, 0);
|
|
147
|
+
check(
|
|
148
|
+
"hidden-text: one layout per page, each US letter",
|
|
149
|
+
f.layout.length === 3 &&
|
|
150
|
+
f.layout.every((p) => p.widthPt === 612 && p.heightPt === 792),
|
|
151
|
+
JSON.stringify(f.layout.map((p) => [p.widthPt, p.heightPt])),
|
|
152
|
+
);
|
|
153
|
+
// Guards: the rotation test in viewer space — the rotated margin text must reach the writer
|
|
154
|
+
// marked as not upright, or it is counted as body text.
|
|
155
|
+
check(
|
|
156
|
+
"hidden-text: the rotated text is marked not upright, and only it",
|
|
157
|
+
boxes
|
|
158
|
+
.filter((b) => !b.upright)
|
|
159
|
+
.every((b) => b.text.includes("Rotated") || b.text.includes("rotated")) &&
|
|
160
|
+
chars((b) => !b.upright) > 200,
|
|
161
|
+
);
|
|
162
|
+
// Guards: the fill walk on a real operator list — the render-mode-3 layer is found.
|
|
163
|
+
check(
|
|
164
|
+
"hidden-text: the render-mode-3 text is marked invisible",
|
|
165
|
+
chars((b) => b.fill.kind === "invisible") > 150 &&
|
|
166
|
+
boxes
|
|
167
|
+
.filter((b) => b.fill.kind === "invisible")
|
|
168
|
+
.every((b) => /visi|text|layer/i.test(b.text)),
|
|
169
|
+
);
|
|
170
|
+
check(
|
|
171
|
+
"hidden-text: the gray!8 text carries its colour, #f5f5f5",
|
|
172
|
+
chars((b) => b.fill.kind === "rgb" && b.fill.hex === "#f5f5f5") > 3000,
|
|
173
|
+
);
|
|
174
|
+
check(
|
|
175
|
+
"hidden-text: the body text is upright, visible and not light",
|
|
176
|
+
chars(
|
|
177
|
+
(b) =>
|
|
178
|
+
b.upright &&
|
|
179
|
+
b.fill.kind !== "invisible" &&
|
|
180
|
+
!(b.fill.kind === "rgb" && b.fill.hex !== "#000000") &&
|
|
181
|
+
Math.abs(b.size - 9.96) < 0.1,
|
|
182
|
+
) > 3000,
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
console.log(
|
|
187
|
+
`✓ ${String(n)} assertions passed — pdf-facts: pdf.js on real PDFs agrees with poppler, and says what it cannot read`,
|
|
188
|
+
);
|