paperlint 3.2.0 → 4.1.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/workflows/ci.yml +16 -23
- package/CLAUDE.md +33 -13
- package/CONTRIBUTING.md +23 -7
- package/README.md +11 -8
- package/dist/adapters/banal/failure.d.ts +1 -7
- package/dist/adapters/banal/failure.d.ts.map +1 -1
- package/dist/adapters/banal/failure.js +5 -7
- package/dist/adapters/banal/failure.js.map +1 -1
- package/dist/adapters/banal/index.d.ts.map +1 -1
- package/dist/adapters/banal/index.js +8 -11
- package/dist/adapters/banal/index.js.map +1 -1
- package/dist/adapters/banal/install.d.ts.map +1 -1
- package/dist/adapters/banal/install.js +4 -3
- package/dist/adapters/banal/install.js.map +1 -1
- package/dist/adapters/banal/invocation.d.ts +10 -5
- package/dist/adapters/banal/invocation.d.ts.map +1 -1
- package/dist/adapters/banal/invocation.js +13 -1
- package/dist/adapters/banal/invocation.js.map +1 -1
- package/dist/adapters/banal/locate.d.ts +12 -9
- package/dist/adapters/banal/locate.d.ts.map +1 -1
- package/dist/adapters/banal/locate.js +19 -12
- package/dist/adapters/banal/locate.js.map +1 -1
- package/dist/adapters/banal/run.d.ts +10 -1
- package/dist/adapters/banal/run.d.ts.map +1 -1
- package/dist/adapters/banal/run.js +8 -4
- package/dist/adapters/banal/run.js.map +1 -1
- package/dist/adapters/banal/settings.d.ts +1 -1
- package/dist/adapters/banal/settings.d.ts.map +1 -1
- package/dist/adapters/banal/settings.js +2 -1
- package/dist/adapters/banal/settings.js.map +1 -1
- package/dist/adapters/latex/document-class.d.ts +22 -0
- package/dist/adapters/latex/document-class.d.ts.map +1 -0
- package/dist/adapters/latex/document-class.js +102 -0
- package/dist/adapters/latex/document-class.js.map +1 -0
- package/dist/adapters/latex/hidden.d.ts +16 -0
- package/dist/adapters/latex/hidden.d.ts.map +1 -0
- package/dist/adapters/latex/hidden.js +49 -0
- package/dist/adapters/latex/hidden.js.map +1 -0
- package/dist/adapters/latex/index.d.ts +29 -0
- package/dist/adapters/latex/index.d.ts.map +1 -0
- package/dist/adapters/latex/index.js +18 -0
- package/dist/adapters/latex/index.js.map +1 -0
- package/dist/adapters/latex/nodes.d.ts +54 -0
- package/dist/adapters/latex/nodes.d.ts.map +1 -0
- package/dist/adapters/latex/nodes.js +93 -0
- package/dist/adapters/latex/nodes.js.map +1 -0
- package/dist/adapters/latex/outline.d.ts +10 -0
- package/dist/adapters/latex/outline.d.ts.map +1 -0
- package/dist/adapters/latex/outline.js +39 -0
- package/dist/adapters/latex/outline.js.map +1 -0
- package/dist/adapters/latex/parse.d.ts +42 -0
- package/dist/adapters/latex/parse.d.ts.map +1 -0
- package/dist/adapters/latex/parse.js +94 -0
- package/dist/adapters/latex/parse.js.map +1 -0
- package/dist/adapters/latex/rendered.d.ts +10 -0
- package/dist/adapters/latex/rendered.d.ts.map +1 -0
- package/dist/adapters/latex/rendered.js +39 -0
- package/dist/adapters/latex/rendered.js.map +1 -0
- package/dist/adapters/memory/index.d.ts.map +1 -1
- package/dist/adapters/memory/index.js +8 -2
- package/dist/adapters/memory/index.js.map +1 -1
- package/dist/adapters/node/process.io.d.ts +6 -3
- package/dist/adapters/node/process.io.d.ts.map +1 -1
- package/dist/adapters/node/process.io.js.map +1 -1
- package/dist/adapters/node/workspace.io.d.ts.map +1 -1
- package/dist/adapters/node/workspace.io.js +4 -2
- package/dist/adapters/node/workspace.io.js.map +1 -1
- package/dist/adapters/pdfjs/fill.d.ts.map +1 -1
- package/dist/adapters/pdfjs/fill.js +12 -12
- package/dist/adapters/pdfjs/fill.js.map +1 -1
- package/dist/adapters/references/index.d.ts.map +1 -1
- package/dist/adapters/references/index.js +4 -2
- package/dist/adapters/references/index.js.map +1 -1
- package/dist/build-engine.d.ts +1 -1
- package/dist/build-engine.d.ts.map +1 -1
- package/dist/build-engine.js +8 -1
- package/dist/build-engine.js.map +1 -1
- package/dist/build.d.ts +2 -3
- package/dist/build.d.ts.map +1 -1
- package/dist/build.js +9 -10
- package/dist/build.js.map +1 -1
- package/dist/caller-path.d.ts +3 -0
- package/dist/caller-path.d.ts.map +1 -0
- package/dist/caller-path.js +13 -0
- package/dist/caller-path.js.map +1 -0
- package/dist/cli.d.ts +10 -17
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +91 -17
- package/dist/cli.js.map +1 -1
- package/dist/doctor.d.ts +12 -3
- package/dist/doctor.d.ts.map +1 -1
- package/dist/doctor.js +2 -1
- package/dist/doctor.js.map +1 -1
- package/dist/domain/paths.d.ts +8 -2
- package/dist/domain/paths.d.ts.map +1 -1
- package/dist/domain/paths.js +12 -1
- package/dist/domain/paths.js.map +1 -1
- package/dist/domain/ready.d.ts +24 -0
- package/dist/domain/ready.d.ts.map +1 -0
- package/dist/domain/ready.js +11 -0
- package/dist/domain/ready.js.map +1 -0
- package/dist/domain/record.d.ts +8 -0
- package/dist/domain/record.d.ts.map +1 -0
- package/dist/domain/record.js +8 -0
- package/dist/domain/record.js.map +1 -0
- package/dist/domain/sha256.d.ts.map +1 -1
- package/dist/domain/sha256.js +4 -2
- package/dist/domain/sha256.js.map +1 -1
- package/dist/domain/tex-document.d.ts +84 -0
- package/dist/domain/tex-document.d.ts.map +1 -0
- package/dist/domain/tex-document.js +43 -0
- package/dist/domain/tex-document.js.map +1 -0
- package/dist/engine.d.ts +23 -22
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js.map +1 -1
- package/dist/eslint-rules/bib-reachable-entry.d.ts +1 -1
- package/dist/eslint-rules/bib-reachable-entry.js.map +1 -1
- package/dist/eslint-rules/install-path-literals.d.ts +2 -2
- package/dist/eslint-rules/install-path-literals.js.map +1 -1
- package/dist/eslint-rules/paper-research-question.d.ts +1 -1
- package/dist/eslint-rules/paper-research-question.js.map +1 -1
- package/dist/eslint-rules/paper-stages.d.ts +2 -2
- package/dist/eslint-rules/paper-stages.js.map +1 -1
- package/dist/eslint-rules/paper-typography.d.ts +6 -6
- package/dist/eslint-rules/paper-typography.js.map +1 -1
- package/dist/eslint-rules/pdf-last-page-balance.d.ts +1 -1
- package/dist/eslint-rules/pdf-last-page-balance.js.map +1 -1
- package/dist/eslint-rules/review-frontmatter.d.ts +2 -2
- package/dist/eslint-rules/review-frontmatter.js.map +1 -1
- package/dist/eslint-rules/sibling-frontmatter.d.ts +1 -1
- package/dist/eslint-rules/sibling-frontmatter.d.ts.map +1 -1
- package/dist/eslint-rules/temp-root-realpath.d.ts +1 -1
- package/dist/eslint-rules/temp-root-realpath.js.map +1 -1
- package/dist/eslint-rules/tex-build.d.ts +2 -2
- package/dist/eslint-rules/tex-build.js.map +1 -1
- package/dist/facts-file.d.ts.map +1 -1
- package/dist/facts-file.js +31 -28
- package/dist/facts-file.js.map +1 -1
- package/dist/hooks-settings.d.ts.map +1 -1
- package/dist/hooks-settings.js +17 -2
- package/dist/hooks-settings.js.map +1 -1
- package/dist/init.d.ts +3 -4
- package/dist/init.d.ts.map +1 -1
- package/dist/init.js +18 -8
- package/dist/init.js.map +1 -1
- package/dist/lib/paper-config.d.ts.map +1 -1
- package/dist/lib/paper-config.js +1 -5
- package/dist/lib/paper-config.js.map +1 -1
- package/dist/new-paper.d.ts +5 -0
- package/dist/new-paper.d.ts.map +1 -1
- package/dist/new-paper.js +8 -1
- package/dist/new-paper.js.map +1 -1
- package/dist/package-dirs.d.ts +5 -0
- package/dist/package-dirs.d.ts.map +1 -0
- package/dist/package-dirs.js +14 -0
- package/dist/package-dirs.js.map +1 -0
- package/dist/paper-files.d.ts +4 -4
- package/dist/paper-files.d.ts.map +1 -1
- package/dist/paper-files.js +6 -6
- package/dist/paper-files.js.map +1 -1
- package/dist/paper-settings.js +3 -3
- package/dist/paper-settings.js.map +1 -1
- package/dist/pdf-facts.d.ts +51 -1
- package/dist/pdf-facts.d.ts.map +1 -1
- package/dist/pdf-facts.js +23 -15
- package/dist/pdf-facts.js.map +1 -1
- package/dist/pdf-geometry.d.ts.map +1 -1
- package/dist/pdf-geometry.js +2 -3
- package/dist/pdf-geometry.js.map +1 -1
- package/dist/ports/latex.d.ts +24 -0
- package/dist/ports/latex.d.ts.map +1 -0
- package/dist/ports/latex.js +2 -0
- package/dist/ports/latex.js.map +1 -0
- package/dist/ports/tool-installer.d.ts +2 -14
- package/dist/ports/tool-installer.d.ts.map +1 -1
- package/dist/presets.d.ts +15 -2
- package/dist/presets.d.ts.map +1 -1
- package/dist/presets.js +35 -10
- package/dist/presets.js.map +1 -1
- package/dist/references.d.ts.map +1 -1
- package/dist/references.js +4 -4
- package/dist/references.js.map +1 -1
- package/dist/rules-config.d.ts +26 -1
- package/dist/rules-config.d.ts.map +1 -1
- package/dist/rules-config.js +9 -9
- package/dist/rules-config.js.map +1 -1
- package/dist/tex-requirements.d.ts +15 -0
- package/dist/tex-requirements.d.ts.map +1 -1
- package/dist/tex-requirements.js +28 -10
- package/dist/tex-requirements.js.map +1 -1
- package/dist/tex-venue-rules.d.ts +83 -0
- package/dist/tex-venue-rules.d.ts.map +1 -0
- package/dist/tex-venue-rules.js +254 -0
- package/dist/tex-venue-rules.js.map +1 -0
- package/dist/types.d.ts +5 -3
- package/dist/types.d.ts.map +1 -1
- package/dist/venue-rules.d.ts +1 -1
- package/dist/venue-rules.d.ts.map +1 -1
- package/dist/venue-rules.js +20 -7
- package/dist/venue-rules.js.map +1 -1
- package/docs/configuration.md +5 -4
- package/docs/e2e.md +69 -40
- package/docs/install.md +1 -1
- package/docs/prior-art/README.md +1 -1
- package/docs/prior-art/package-location.md +5 -5
- package/docs/prior-art/repro/README.md +1 -1
- package/docs/prior-art/test-tooling.md +21 -10
- package/docs/rules/tex/required-section.md +73 -0
- package/docs/rules/tex/template.md +62 -0
- package/docs/rules/tex/venue-leftover.md +71 -0
- package/docs/rules.md +70 -48
- package/docs/testing.md +17 -4
- package/docs/texlive-install-decision.md +1 -1
- package/docs/toolchain.md +2 -2
- package/eslint-rules/bib-reachable-entry.ts +1 -1
- package/eslint-rules/install-path-literals.ts +2 -2
- package/eslint-rules/paper-research-question.ts +1 -1
- package/eslint-rules/paper-stages.ts +2 -2
- package/eslint-rules/paper-typography.ts +2 -2
- package/eslint-rules/pdf-last-page-balance.ts +1 -1
- package/eslint-rules/review-frontmatter.ts +1 -1
- package/eslint-rules/temp-root-realpath.ts +1 -1
- package/eslint-rules/tex-build.ts +2 -2
- package/eslint.config.mjs +162 -15
- package/fixtures/build-e2e/acmart/expect.json +26 -0
- package/fixtures/build-e2e/aidc/PIPELINE-STATUS.md +3 -0
- package/fixtures/build-e2e/aidc/expect.json +14 -0
- package/fixtures/build-e2e/aidc/paper.tex +46 -0
- package/fixtures/build-e2e/aidc/paperlint.json +1 -0
- package/fixtures/build-e2e/broken/expect.json +11 -0
- package/fixtures/build-e2e/cite/expect.json +27 -0
- package/fixtures/build-e2e/empty/expect.json +12 -0
- package/fixtures/build-e2e/fallback/expect.json +53 -0
- package/fixtures/build-e2e/guards/expect.json +6 -0
- package/fixtures/build-e2e/no-source/expect.json +10 -0
- package/fixtures/build-e2e/unbalanced/expect.json +60 -0
- package/fixtures/install-path-literals/clean.fixture.mjs +1 -1
- package/fixtures/install-path-literals/clean.md +2 -2
- package/fixtures/install-path-literals/defect.fixture.mjs +1 -1
- package/fixtures/install-path-literals/defect.md +2 -2
- package/fixtures/pdf-facts/README.md +1 -1
- package/fixtures/real-markdown-paper/baseline.ts +1 -1
- package/fixtures/render-paper/build-clean.sh +3 -5
- package/hooks/paper-status-gates.sh +9 -19
- package/lib/paper-config.harness.mjs +1 -1
- package/lib/paper-config.ts +1 -5
- package/lib/skill-checks.mjs +24 -381
- package/lib/skill-checks.test.mjs +63 -115
- package/lib/skill-corpus.mjs +10 -251
- package/lib/skill-corpus.test.mjs +2 -13
- package/lib/skill-trigger-cases.mjs +1 -1
- package/package.json +13 -2
- package/{skills/submit-paper/references/venues → presets}/acm-sigconf.jsonc +4 -1
- package/{skills/submit-paper/references/venues → presets}/agenticdev.jsonc +4 -0
- package/presets/aidc.jsonc +73 -0
- package/{skills/submit-paper/references/venues → presets}/aisec.jsonc +4 -0
- package/presets/ieee-conference.jsonc +66 -0
- package/{skills/submit-paper/references/venues → presets}/realm.jsonc +8 -1
- package/{skills/submit-paper/references/venues → presets/tex}/paper-guards.tex +1 -1
- package/{skills/submit-paper/references/venues → presets}/venue-profile.schema.json +30 -2
- package/scripts/check.harness.ts +64 -10
- package/scripts/check.test.ts +60 -13
- package/scripts/check.ts +83 -30
- package/scripts/coverage-config.test.ts +0 -1
- package/scripts/harness-api.frozen.json +0 -1
- package/scripts/test.test.ts +1 -1
- package/scripts/test.ts +1 -1
- package/skills/analyze-sibling-paper/SKILL.md +1 -39
- package/skills/analyze-sibling-paper/SKILL.md.spec.ts +0 -38
- package/skills/argument-arc/SKILL.md +2 -36
- package/skills/argument-arc/SKILL.md.spec.ts +1 -43
- package/skills/build-benchmark/SKILL.md +1 -41
- package/skills/build-benchmark/SKILL.md.spec.ts +0 -40
- package/skills/camera-ready/SKILL.md +1 -34
- package/skills/camera-ready/SKILL.md.spec.ts +0 -33
- package/skills/cold-read-diff/SKILL.md +1 -35
- package/skills/cold-read-diff/SKILL.md.spec.ts +0 -34
- package/skills/draft-paper/SKILL.md +2 -41
- package/skills/draft-paper/SKILL.md.spec.ts +1 -50
- package/skills/extend-paper/SKILL.md +2 -33
- package/skills/extend-paper/SKILL.md.spec.ts +1 -42
- package/skills/find-venue/SKILL.md +2 -38
- package/skills/find-venue/SKILL.md.spec.ts +1 -47
- package/skills/grade-paper-writing/SKILL.md +3 -38
- package/skills/grade-paper-writing/SKILL.md.spec.ts +2 -47
- package/skills/grade-paper-writing/prose-lint.mjs +2 -3
- package/skills/grade-paper-writing/prose-lint.test.ts +1 -1
- package/skills/harden-paper/SKILL.md +1 -39
- package/skills/harden-paper/SKILL.md.spec.ts +0 -38
- package/skills/map-prior-work/SKILL.md +1 -35
- package/skills/map-prior-work/SKILL.md.spec.ts +0 -34
- package/skills/osf-artifact-upload/osf-artifact-upload.harness.ts +3 -4
- package/skills/paper-adversarial-review/SKILL.md +2 -35
- package/skills/paper-adversarial-review/SKILL.md.spec.ts +1 -43
- package/skills/paper-pipeline/PIPELINE-MAP.md +2 -2
- package/skills/paper-pipeline/SKILL.md +3 -29
- package/skills/paper-pipeline/SKILL.md.spec.ts +2 -28
- package/skills/paper-pipeline/grade-paper-writing-ablation.eval.mjs +1 -1
- package/skills/paper-pipeline/paper-pipeline.harness.mjs +25 -40
- package/skills/paper-pipeline/references/acceptance-gate.md +1 -6
- package/skills/paper-pipeline/references/pipeline-status-template.md +8 -35
- package/skills/paper-pipeline/scripts/README.md +30 -225
- package/skills/paper-pipeline/scripts/artifact-coverage.harness.mjs +4 -8
- package/skills/paper-pipeline/scripts/consumer.d.mts +0 -2
- package/skills/paper-pipeline/scripts/consumer.harness.mjs +6 -104
- package/skills/paper-pipeline/scripts/consumer.mjs +24 -119
- package/skills/paper-pipeline/scripts/generated-code.harness.mjs +2 -3
- package/skills/paper-pipeline/scripts/population-map.harness.mjs +3 -3
- package/skills/paper-status/SKILL.md +2 -42
- package/skills/paper-status/SKILL.md.spec.ts +1 -41
- package/skills/pc-panel-review/SKILL.md +2 -46
- package/skills/pc-panel-review/SKILL.md.spec.ts +1 -45
- package/skills/plan-paper-timeline/SKILL.md +2 -36
- package/skills/plan-paper-timeline/SKILL.md.spec.ts +0 -36
- package/skills/render-paper/SKILL.md +2 -38
- package/skills/render-paper/SKILL.md.spec.ts +1 -37
- package/skills/render-paper/check-render.sh +16 -63
- package/skills/render-paper/render-paper.harness.mjs +17 -59
- package/skills/research-ideate/SKILL.md +2 -39
- package/skills/research-ideate/SKILL.md.spec.ts +1 -47
- package/skills/study-accepted-papers/SKILL.md +1 -34
- package/skills/study-accepted-papers/SKILL.md.spec.ts +0 -33
- package/skills/submit-paper/SKILL.md +1 -33
- package/skills/submit-paper/SKILL.md.spec.ts +0 -32
- package/skills/sweep-design-space/SKILL.md +1 -38
- package/skills/sweep-design-space/SKILL.md.spec.ts +0 -37
- package/skills/tighten-paper/SKILL.md +2 -39
- package/skills/tighten-paper/SKILL.md.spec.ts +1 -47
- package/skills/tighten-paper/structure.mjs +4 -11
- package/skills/verify-citations/SKILL.md +2 -38
- package/skills/verify-citations/SKILL.md.spec.ts +1 -37
- package/src/adapters/banal/failure.ts +9 -10
- package/src/adapters/banal/index.test.ts +3 -3
- package/src/adapters/banal/index.ts +8 -10
- package/src/adapters/banal/install.ts +9 -4
- package/src/adapters/banal/invocation.test.ts +20 -8
- package/src/adapters/banal/invocation.ts +24 -9
- package/src/adapters/banal/locate.test.ts +2 -2
- package/src/adapters/banal/locate.ts +27 -21
- package/src/adapters/banal/output.test.ts +1 -1
- package/src/adapters/banal/run.test.ts +3 -3
- package/src/adapters/banal/run.ts +16 -10
- package/src/adapters/banal/settings.ts +2 -2
- package/src/adapters/banal/xml.test.ts +1 -1
- package/src/adapters/curl/download.test.ts +4 -4
- package/src/adapters/latex/document-class.ts +147 -0
- package/src/adapters/latex/hidden.ts +70 -0
- package/src/adapters/latex/index.test.ts +361 -0
- package/src/adapters/latex/index.ts +55 -0
- package/src/adapters/latex/nodes.ts +141 -0
- package/src/adapters/latex/outline.ts +64 -0
- package/src/adapters/latex/parse.ts +136 -0
- package/src/adapters/latex/rendered.ts +59 -0
- package/src/adapters/memory/index.ts +4 -3
- package/src/adapters/node/files.test.ts +5 -3
- package/src/adapters/node/process.io.ts +9 -2
- package/src/adapters/node/process.test.ts +4 -4
- package/src/adapters/node/workspace.io.ts +9 -4
- package/src/adapters/pdfjs/fill.test.ts +1 -1
- package/src/adapters/pdfjs/fill.ts +18 -10
- package/src/adapters/references/index.ts +7 -2
- package/src/build-engine.test.ts +11 -6
- package/src/build-engine.ts +10 -5
- package/src/build.harness.mjs +5 -7
- package/src/build.test.ts +10 -11
- package/src/build.ts +12 -13
- package/src/caller-path.ts +13 -0
- package/src/cli.harness.mjs +1 -3
- package/src/cli.test.ts +21 -16
- package/src/cli.ts +115 -33
- package/src/doctor.test.ts +1 -1
- package/src/doctor.ts +14 -5
- package/src/domain/paths.test.ts +15 -0
- package/src/domain/paths.ts +20 -1
- package/src/domain/ready.ts +31 -0
- package/src/domain/record.test.ts +20 -0
- package/src/domain/record.ts +12 -0
- package/src/domain/sha256.ts +5 -3
- package/src/domain/tex-document.test.ts +80 -0
- package/src/domain/tex-document.ts +110 -0
- package/src/engine.harness.mjs +1 -1
- package/src/engine.ts +30 -2
- package/src/facts-file.test.ts +4 -7
- package/src/facts-file.ts +45 -30
- package/src/hooks-settings.harness.ts +17 -0
- package/src/hooks-settings.ts +28 -4
- package/src/init.test.ts +25 -5
- package/src/init.ts +26 -12
- package/src/latex-loop.harness.ts +1 -1
- package/src/new-paper.harness.mjs +7 -23
- package/src/new-paper.test.ts +28 -3
- package/src/new-paper.ts +15 -3
- package/src/package-dirs.ts +17 -0
- package/src/paper-files.test.ts +22 -19
- package/src/paper-files.ts +16 -27
- package/src/paper-settings-commands.test.ts +6 -1
- package/src/paper-settings.test.ts +2 -2
- package/src/paper-settings.ts +5 -5
- package/src/pdf-facts.test.ts +16 -5
- package/src/pdf-facts.ts +79 -22
- package/src/pdf-geometry.ts +5 -3
- package/src/ports/latex.ts +29 -0
- package/src/ports/tool-installer.ts +2 -17
- package/src/presets.test.ts +65 -4
- package/src/presets.ts +65 -28
- package/src/references.test.ts +25 -19
- package/src/references.ts +4 -5
- package/src/rule-cases.test.ts +474 -0
- package/src/rule-docs.test.ts +159 -0
- package/src/rules-config.ts +40 -11
- package/src/tex-requirements.harness.ts +13 -23
- package/src/tex-requirements.test.ts +21 -0
- package/src/tex-requirements.ts +64 -14
- package/src/tex-venue-rules.test.ts +659 -0
- package/src/tex-venue-rules.ts +429 -0
- package/src/toolchain.harness.mjs +1 -1
- package/src/toolchain.test.ts +9 -5
- package/src/toolchain.ts +2 -2
- package/src/types.ts +5 -3
- package/src/venue-rules.test.ts +104 -8
- package/src/venue-rules.ts +22 -12
- package/templates/paper/paper.tex +2 -1
- package/skills/paper-pipeline/scripts/announce.mjs +0 -67
- package/skills/paper-pipeline/scripts/announce.test.mjs +0 -102
- package/skills/paper-pipeline/scripts/fixtures/clean/PIPELINE-STATUS.md +0 -51
- package/skills/paper-pipeline/scripts/fixtures/dirty/PIPELINE-STATUS.md +0 -52
- package/skills/paper-pipeline/scripts/fixtures/dirty/paper.md +0 -6
- package/skills/paper-pipeline/scripts/fixtures/scorecard.ts +0 -100
- package/skills/paper-pipeline/scripts/ledger.cli.test.mjs +0 -302
- package/skills/paper-pipeline/scripts/ledger.mjs +0 -623
- package/skills/paper-pipeline/scripts/ledger.test.mjs +0 -254
- package/skills/paper-pipeline/scripts/pipeline-check.cli.test.mjs +0 -474
- package/skills/paper-pipeline/scripts/pipeline-check.harness.mjs +0 -328
- package/skills/paper-pipeline/scripts/pipeline-check.mjs +0 -750
- package/skills/paper-pipeline/scripts/pipeline-edges.mjs +0 -169
- package/skills/paper-pipeline/scripts/pipeline-edges.test.mjs +0 -73
- package/skills/paper-pipeline/scripts/run-mechanical.mjs +0 -648
- package/skills/paper-pipeline/scripts/run-mechanical.test.mjs +0 -468
- package/skills/paper-pipeline/scripts/status.mjs +0 -295
- package/skills/paper-pipeline/scripts/status.test.mjs +0 -232
- /package/{skills/submit-paper/references/venues → presets/tex}/agenticdev.tex +0 -0
- /package/{skills/submit-paper/references/venues → presets}/tex-base.jsonc +0 -0
package/src/doctor.test.ts
CHANGED
|
@@ -44,7 +44,7 @@ test("doctor: a settings file silent on papersDir, an occupied entry, an unreada
|
|
|
44
44
|
const code = doctor({
|
|
45
45
|
cwd: root,
|
|
46
46
|
log: (s: string) => lines.push(s),
|
|
47
|
-
run: (
|
|
47
|
+
run: () => ({ status: 0 }),
|
|
48
48
|
cliPapers: join(root, "papers"),
|
|
49
49
|
skillLinks: () => ({
|
|
50
50
|
ok: true,
|
package/src/doctor.ts
CHANGED
|
@@ -37,6 +37,7 @@ import { PAPER_MARKERS } from "./build.ts";
|
|
|
37
37
|
import { linkSkills, SKILLS_HOME, type LinkReport } from "./link-skills.ts";
|
|
38
38
|
import { doctorHooks } from "./hooks-settings.ts";
|
|
39
39
|
import { messageOf } from "./domain/text.ts";
|
|
40
|
+
import { fieldOf } from "./domain/record.ts";
|
|
40
41
|
|
|
41
42
|
export interface Program {
|
|
42
43
|
readonly bin: string;
|
|
@@ -99,7 +100,17 @@ export const PROGRAMS: readonly Program[] = [
|
|
|
99
100
|
},
|
|
100
101
|
];
|
|
101
102
|
|
|
102
|
-
|
|
103
|
+
/**
|
|
104
|
+
* Asks the shell whether a program is on PATH. `spawnSync` is one; a fake answers with a status
|
|
105
|
+
* alone, because a status is all `found` reads.
|
|
106
|
+
*/
|
|
107
|
+
export type ProgramProbe = (
|
|
108
|
+
file: string,
|
|
109
|
+
args: readonly string[],
|
|
110
|
+
options: { readonly shell: true; readonly stdio: "ignore" },
|
|
111
|
+
) => { readonly status: number | null };
|
|
112
|
+
|
|
113
|
+
export const found = (bin: string, run: ProgramProbe = spawnSync): boolean =>
|
|
103
114
|
run("command", ["-v", bin], { shell: true, stdio: "ignore" }).status === 0;
|
|
104
115
|
|
|
105
116
|
/** Lists a directory's entries; the disk by default, a test stages an unreadable child. */
|
|
@@ -176,9 +187,7 @@ function declarationVerdict(raw: string | null): {
|
|
|
176
187
|
bad: 1,
|
|
177
188
|
};
|
|
178
189
|
}
|
|
179
|
-
const declared = (settings
|
|
180
|
-
PAPERS_DIR_FIELD
|
|
181
|
-
];
|
|
190
|
+
const declared = fieldOf(settings, PAPERS_DIR_FIELD);
|
|
182
191
|
return {
|
|
183
192
|
lines: [
|
|
184
193
|
declared === undefined
|
|
@@ -234,7 +243,7 @@ export interface DoctorOptions {
|
|
|
234
243
|
cwd?: string;
|
|
235
244
|
/** The project root the hooks are anchored to. Claude Code sets this; it defaults to `cwd`. */
|
|
236
245
|
projectDir?: string;
|
|
237
|
-
run?:
|
|
246
|
+
run?: ProgramProbe;
|
|
238
247
|
/** The directory the CLI resolved from its own config, so both sides can be compared. */
|
|
239
248
|
cliPapers?: string | null;
|
|
240
249
|
/** Reads the skill links without writing any. Injected only so a test can fake the install. */
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { test } from "vitest";
|
|
3
|
+
import { absolutePath, joinPath } from "./paths.ts";
|
|
4
|
+
|
|
5
|
+
test("absolutePath keeps an absolute path as it is", () => {
|
|
6
|
+
assert.equal(absolutePath("/a/b"), "/a/b");
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
test("absolutePath refuses a relative path instead of letting the process's cwd decide it", () => {
|
|
10
|
+
assert.throws(() => absolutePath("a/b"), /not an absolute path: a\/b/);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
test("joinPath stays under its absolute base", () => {
|
|
14
|
+
assert.equal(joinPath(absolutePath("/a"), "b", "c"), "/a/b/c");
|
|
15
|
+
});
|
package/src/domain/paths.ts
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
/** Paths as the domain holds them. */
|
|
2
|
+
import { isAbsolute, join } from "node:path";
|
|
2
3
|
import type { Opaque } from "ts-essentials";
|
|
3
4
|
|
|
4
|
-
/**
|
|
5
|
+
/**
|
|
6
|
+
* An absolute path. Minted by `absolutePath` (a checked string) or `joinPath` (an absolute base and
|
|
7
|
+
* segments) only, so nothing holding one has to ask whether it is relative.
|
|
8
|
+
*/
|
|
5
9
|
export type AbsolutePath = Opaque<string, "AbsolutePath">;
|
|
10
|
+
|
|
11
|
+
// The brand's one owner: the predicate IS the check, so no assertion is needed to mint the type.
|
|
12
|
+
const isAbsolutePath = (p: string): p is AbsolutePath => isAbsolute(p);
|
|
13
|
+
|
|
14
|
+
/** A path the caller holds as text, checked where it becomes a value. A relative one throws. */
|
|
15
|
+
export function absolutePath(p: string): AbsolutePath {
|
|
16
|
+
if (!isAbsolutePath(p)) throw new Error(`not an absolute path: ${p}`);
|
|
17
|
+
return p;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** `segments` under `base`: absolute because `base` is. */
|
|
21
|
+
export const joinPath = (
|
|
22
|
+
base: AbsolutePath,
|
|
23
|
+
...segments: string[]
|
|
24
|
+
): AbsolutePath => absolutePath(join(base, ...segments));
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/** A program `paperlint toolchain` installed or found: present, verified, running. */
|
|
2
|
+
|
|
3
|
+
// Not exported: a `Ready` carries this property, and the one way to write it is `ready` below.
|
|
4
|
+
const READY = Symbol("Ready");
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Present, verified, running. BRANDED: a literal `{ where, fresh, verified }` is not a `Ready`, so
|
|
8
|
+
* only an installer's adapter — one minting call each (banal's is `ready` in
|
|
9
|
+
* `adapters/banal/index.ts`) — can make one, and a ready line cannot be printed without it.
|
|
10
|
+
*/
|
|
11
|
+
export interface Ready {
|
|
12
|
+
/** Where it is: the program's path or its bin directory. */
|
|
13
|
+
readonly where: string;
|
|
14
|
+
/** This run installed it. */
|
|
15
|
+
readonly fresh: boolean;
|
|
16
|
+
/** What was checked before calling it ready, as a person reads it. */
|
|
17
|
+
readonly verified: string;
|
|
18
|
+
readonly [READY]: true;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** An installer's verdict that its program is ready. Called by an installer adapter, after its checks. */
|
|
22
|
+
export const ready = (r: {
|
|
23
|
+
readonly where: string;
|
|
24
|
+
readonly fresh: boolean;
|
|
25
|
+
readonly verified: string;
|
|
26
|
+
}): Ready => ({
|
|
27
|
+
where: r.where,
|
|
28
|
+
fresh: r.fresh,
|
|
29
|
+
verified: r.verified,
|
|
30
|
+
[READY]: true,
|
|
31
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { test } from "vitest";
|
|
3
|
+
import { fieldOf, isRecord, numbersOf } from "./record.ts";
|
|
4
|
+
|
|
5
|
+
test("isRecord: an object, and not null, an array or a string", () => {
|
|
6
|
+
assert.deepEqual(
|
|
7
|
+
[isRecord({}), isRecord(null), isRecord([]), isRecord("x")],
|
|
8
|
+
[true, false, false, false],
|
|
9
|
+
);
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
test("fieldOf reads an own or inherited field of a record, and nothing of anything else", () => {
|
|
13
|
+
assert.equal(fieldOf({ a: 1 }, "a"), 1);
|
|
14
|
+
assert.equal(fieldOf(new TypeError("x"), "name"), "TypeError");
|
|
15
|
+
assert.equal(fieldOf("text", "length"), undefined);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test("numbersOf keeps numbers as they are", () => {
|
|
19
|
+
assert.deepEqual(numbersOf([1, -0.5, 0]), [1, -0.5, 0]);
|
|
20
|
+
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** Reading fields off a value typed `unknown` — a parsed JSON file, an object a library hands back. */
|
|
2
|
+
|
|
3
|
+
/** Any non-null object that is not an array, readable by key. */
|
|
4
|
+
export const isRecord = (v: unknown): v is Readonly<Record<string, unknown>> =>
|
|
5
|
+
typeof v === "object" && v !== null && !Array.isArray(v);
|
|
6
|
+
|
|
7
|
+
/** `v[key]` when `v` is a record; undefined for anything else. Inherited fields count, as `.` reads them. */
|
|
8
|
+
export const fieldOf = (v: unknown, key: string): unknown =>
|
|
9
|
+
isRecord(v) ? v[key] : undefined;
|
|
10
|
+
|
|
11
|
+
/** A number array a library types as `any[]`, read as the numbers it holds. */
|
|
12
|
+
export const numbersOf = (xs: readonly unknown[]): number[] => xs.map(Number);
|
package/src/domain/sha256.ts
CHANGED
|
@@ -9,13 +9,15 @@ import type { Opaque } from "ts-essentials";
|
|
|
9
9
|
export type Sha256 = Opaque<string, "Sha256">;
|
|
10
10
|
|
|
11
11
|
const SHA256 = /^[0-9a-f]{64}$/;
|
|
12
|
+
// The brand's one owner: the predicate IS the check, so no assertion is needed to mint the type.
|
|
13
|
+
const isSha256 = (s: string): s is Sha256 => SHA256.test(s);
|
|
12
14
|
|
|
13
15
|
/** A sha256 written down as text — a pin, an option — checked where it is written, not where it is used. */
|
|
14
16
|
export function parseSha256(s: string): Sha256 {
|
|
15
|
-
if (!
|
|
17
|
+
if (!isSha256(s))
|
|
16
18
|
throw new Error(`not a sha256 (64 lowercase hex digits): ${s}`);
|
|
17
|
-
return s
|
|
19
|
+
return s;
|
|
18
20
|
}
|
|
19
21
|
|
|
20
22
|
export const sha256Hex = (bytes: Uint8Array): Sha256 =>
|
|
21
|
-
createHash("sha256").update(bytes).digest("hex")
|
|
23
|
+
parseSha256(createHash("sha256").update(bytes).digest("hex"));
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/** The values a LaTeX source is read into: a run's text and the span of a range inside it. */
|
|
2
|
+
import { describe, expect, it } from "vitest";
|
|
3
|
+
import {
|
|
4
|
+
collapse,
|
|
5
|
+
documentClassLine,
|
|
6
|
+
missingOptions,
|
|
7
|
+
runText,
|
|
8
|
+
spanIn,
|
|
9
|
+
venueClass,
|
|
10
|
+
type TextRun,
|
|
11
|
+
} from "./tex-document.ts";
|
|
12
|
+
|
|
13
|
+
// "ab" at 10, then " " at 20 (one character standing for a longer whitespace), then "cd" at 30.
|
|
14
|
+
const RUN: TextRun = {
|
|
15
|
+
segments: [
|
|
16
|
+
{ text: "ab", at: 10 },
|
|
17
|
+
{ text: " ", at: 20 },
|
|
18
|
+
{ text: "cd", at: 30 },
|
|
19
|
+
],
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
describe("a text run", () => {
|
|
23
|
+
it("its text is its segments' text, in order", () => {
|
|
24
|
+
expect(runText(RUN)).toBe("ab cd");
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("spanIn: from the first character's offset to past the last's, across segments", () => {
|
|
28
|
+
expect(spanIn(RUN, 0, 2)).toEqual({ start: 10, end: 12 });
|
|
29
|
+
expect(spanIn(RUN, 1, 4)).toEqual({ start: 11, end: 31 });
|
|
30
|
+
expect(spanIn(RUN, 3, 5)).toEqual({ start: 30, end: 32 });
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("🔴 spanIn: an empty range, or one reaching outside the run, has no span — never {0, 0}", () => {
|
|
34
|
+
expect(spanIn(RUN, 2, 2)).toBe(null);
|
|
35
|
+
expect(spanIn(RUN, -1, 2)).toBe(null);
|
|
36
|
+
expect(spanIn(RUN, 3, 9)).toBe(null);
|
|
37
|
+
expect(spanIn(RUN, 9, 10)).toBe(null);
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
describe("a document class", () => {
|
|
42
|
+
it("documentClassLine writes it back; with no options, no brackets", () => {
|
|
43
|
+
expect(
|
|
44
|
+
documentClassLine({
|
|
45
|
+
cls: "IEEEtran",
|
|
46
|
+
options: ["conference", "compsoc"],
|
|
47
|
+
}),
|
|
48
|
+
).toBe("\\documentclass[conference,compsoc]{IEEEtran}");
|
|
49
|
+
expect(documentClassLine({ cls: "article", options: [] })).toBe(
|
|
50
|
+
"\\documentclass{article}",
|
|
51
|
+
);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("missingOptions: every option of the wanted class the paper lacks, in the wanted order", () => {
|
|
55
|
+
expect(
|
|
56
|
+
missingOptions(
|
|
57
|
+
{ cls: "x", options: ["b"] },
|
|
58
|
+
{ cls: "x", options: ["a", "b", "c"] },
|
|
59
|
+
),
|
|
60
|
+
).toEqual(["a", "c"]);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("venueClass: another class is replaced; the same class gains the missing options, or is kept", () => {
|
|
64
|
+
const want = { cls: "IEEEtran", options: ["conference", "compsoc"] };
|
|
65
|
+
expect(venueClass({ cls: "article", options: ["11pt"] }, want)).toEqual({
|
|
66
|
+
kind: "replace",
|
|
67
|
+
by: want,
|
|
68
|
+
});
|
|
69
|
+
expect(
|
|
70
|
+
venueClass({ cls: "IEEEtran", options: ["review", "conference"] }, want),
|
|
71
|
+
).toEqual({ kind: "add", options: ["compsoc"] });
|
|
72
|
+
expect(
|
|
73
|
+
venueClass({ cls: "IEEEtran", options: ["compsoc", "conference"] }, want),
|
|
74
|
+
).toEqual({ kind: "keep" });
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("collapse: every run of whitespace, newlines included, and trims", () => {
|
|
78
|
+
expect(collapse(" LLM Usage\n Statement ")).toBe("LLM Usage Statement");
|
|
79
|
+
});
|
|
80
|
+
});
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a paper's LaTeX source says, in the paper's vocabulary — its document class, its sections,
|
|
3
|
+
* the text a reader sees — as values. No parser here: the LaTeX adapter (`src/adapters/latex/`)
|
|
4
|
+
* produces these, and the rules and `paperlint new` read them through the `LatexReader` port.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/** A span of the source: `[start, end)` offsets. */
|
|
8
|
+
export interface Span {
|
|
9
|
+
readonly start: number;
|
|
10
|
+
readonly end: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Where something stands in the source, or that the parser gave it no position. */
|
|
14
|
+
export type Place =
|
|
15
|
+
{ readonly kind: "at"; readonly span: Span } | { readonly kind: "unplaced" };
|
|
16
|
+
|
|
17
|
+
/** Whitespace collapsed to single spaces and trimmed — how a title or a class name is compared. */
|
|
18
|
+
export const collapse = (s: string): string =>
|
|
19
|
+
s.split(/\s+/u).filter(Boolean).join(" ");
|
|
20
|
+
|
|
21
|
+
/** A LaTeX document class and the options it is loaded with, in source order. */
|
|
22
|
+
export interface DocumentClass {
|
|
23
|
+
readonly cls: string;
|
|
24
|
+
readonly options: readonly string[];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** A paper's `\documentclass`: none, one that names no class, or a class with its options. */
|
|
28
|
+
export type ClassLine =
|
|
29
|
+
| { readonly kind: "missing" }
|
|
30
|
+
| { readonly kind: "empty"; readonly place: Place }
|
|
31
|
+
| ({ readonly kind: "class"; readonly place: Place } & DocumentClass);
|
|
32
|
+
|
|
33
|
+
/** The options of `want` that `got` does not carry — every one, in `want`'s order. */
|
|
34
|
+
export const missingOptions = (
|
|
35
|
+
got: DocumentClass,
|
|
36
|
+
want: DocumentClass,
|
|
37
|
+
): readonly string[] => want.options.filter((o) => !got.options.includes(o));
|
|
38
|
+
|
|
39
|
+
/** What a paper's class line needs to satisfy a venue: nothing, options added, or the venue's line. */
|
|
40
|
+
export type ClassChange =
|
|
41
|
+
| { readonly kind: "keep" }
|
|
42
|
+
| { readonly kind: "add"; readonly options: readonly string[] }
|
|
43
|
+
| { readonly kind: "replace"; readonly by: DocumentClass };
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* What a paper's class must change to satisfy a venue's `want`: another class is replaced by `want`;
|
|
47
|
+
* the same class keeps its options and gains the ones it lacks, in `want`'s order. This decides WHAT
|
|
48
|
+
* changes; writing it into a source is the LaTeX adapter's.
|
|
49
|
+
*/
|
|
50
|
+
export function venueClass(
|
|
51
|
+
have: DocumentClass,
|
|
52
|
+
want: DocumentClass,
|
|
53
|
+
): ClassChange {
|
|
54
|
+
if (have.cls !== want.cls) return { kind: "replace", by: want };
|
|
55
|
+
const missing = missingOptions(have, want);
|
|
56
|
+
return missing.length === 0
|
|
57
|
+
? { kind: "keep" }
|
|
58
|
+
: { kind: "add", options: missing };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** A class as a `\documentclass` line, for a message and for the line `paperlint new` writes. */
|
|
62
|
+
export const documentClassLine = (d: DocumentClass): string =>
|
|
63
|
+
`\\documentclass${d.options.length > 0 ? `[${d.options.join(",")}]` : ""}{${d.cls}}`;
|
|
64
|
+
|
|
65
|
+
/** One `\section` or `\section*`, its title's text with whitespace collapsed. */
|
|
66
|
+
export interface Heading {
|
|
67
|
+
readonly title: string;
|
|
68
|
+
readonly place: Place;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** The document's sections in source order, where its back matter begins, and where it ends. */
|
|
72
|
+
export interface Outline {
|
|
73
|
+
readonly sections: readonly Heading[];
|
|
74
|
+
/** The first `\appendix` or bibliography, or null: sections from here on are not body. */
|
|
75
|
+
readonly backMatter: number | null;
|
|
76
|
+
/** Where `\end{document}` stands, or null when there is no document environment. */
|
|
77
|
+
readonly end: number | null;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Text a reader sees, copied from one place: `text[i]` stands at source offset `at + i`. */
|
|
81
|
+
export interface Segment {
|
|
82
|
+
readonly text: string;
|
|
83
|
+
readonly at: number;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** A run of rendered text: contiguous segments, never empty. */
|
|
87
|
+
export interface TextRun {
|
|
88
|
+
readonly segments: readonly [Segment, ...Segment[]];
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** The run's text. */
|
|
92
|
+
export const runText = (run: TextRun): string =>
|
|
93
|
+
run.segments.map((s) => s.text).join("");
|
|
94
|
+
|
|
95
|
+
/** The source offset of the `i`-th character of the text these segments make, or null outside it. */
|
|
96
|
+
function offsetIn(segments: readonly Segment[], i: number): number | null {
|
|
97
|
+
const [s, ...rest] = segments;
|
|
98
|
+
if (s === undefined || i < 0) return null;
|
|
99
|
+
return i < s.text.length ? s.at + i : offsetIn(rest, i - s.text.length);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* The source span of `runText(run).slice(from, to)`: from its first character to past its last.
|
|
104
|
+
* Null when the range is empty or reaches outside the run — never an invented position.
|
|
105
|
+
*/
|
|
106
|
+
export function spanIn(run: TextRun, from: number, to: number): Span | null {
|
|
107
|
+
const start = offsetIn(run.segments, from);
|
|
108
|
+
const last = to > from ? offsetIn(run.segments, to - 1) : null;
|
|
109
|
+
return start === null || last === null ? null : { start, end: last + 1 };
|
|
110
|
+
}
|
package/src/engine.harness.mjs
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* 1. `resolveEngine` over `EngineFacts` — one row per branch of the order;
|
|
6
6
|
* 2. the fact readers: `missingPackages` over kpsewhich output, `missingTools`, `whichOnPath`,
|
|
7
7
|
* `probeTree` through a fake runner.
|
|
8
|
-
* All pure or port-driven: no TeX needed. The real-TeX half is `test/e2e/build.ts`.
|
|
8
|
+
* All pure or port-driven: no TeX needed. The real-TeX half is `test/e2e/tex/build.e2e.ts`.
|
|
9
9
|
*/
|
|
10
10
|
import {
|
|
11
11
|
chmodSync,
|
package/src/engine.ts
CHANGED
|
@@ -26,8 +26,36 @@ import { basename, delimiter, join } from "node:path";
|
|
|
26
26
|
import type { TexRequirements } from "./tex-requirements.ts";
|
|
27
27
|
import { printed } from "./domain/text.ts";
|
|
28
28
|
|
|
29
|
-
/**
|
|
30
|
-
export
|
|
29
|
+
/** The options paperlint hands a process runner: text out, both streams captured. */
|
|
30
|
+
export interface RunOptions {
|
|
31
|
+
readonly encoding: "utf8" | "latin1";
|
|
32
|
+
readonly stdio: ["ignore", "pipe", "pipe"];
|
|
33
|
+
readonly env?: NodeJS.ProcessEnv;
|
|
34
|
+
readonly cwd?: string;
|
|
35
|
+
readonly maxBuffer?: number;
|
|
36
|
+
readonly timeout?: number;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* What paperlint reads of a finished process. `spawnSync`'s result has this shape; a fake needs
|
|
41
|
+
* nothing more. Both streams are optional: they are `undefined` when the spawn itself failed.
|
|
42
|
+
*/
|
|
43
|
+
export interface RunResult {
|
|
44
|
+
readonly status: number | null;
|
|
45
|
+
readonly stdout?: string;
|
|
46
|
+
readonly stderr?: string;
|
|
47
|
+
readonly error?: Error;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* A process runner: `spawnSync`'s text overload, narrowed to what paperlint passes and reads — the
|
|
52
|
+
* port the harness replaces. `spawnSync` itself is one.
|
|
53
|
+
*/
|
|
54
|
+
export type Runner = (
|
|
55
|
+
file: string,
|
|
56
|
+
args: readonly string[],
|
|
57
|
+
options: RunOptions,
|
|
58
|
+
) => RunResult;
|
|
31
59
|
|
|
32
60
|
/** One TeX Live and what it lacks for the papers at hand. */
|
|
33
61
|
export interface TreeProbe {
|
package/src/facts-file.test.ts
CHANGED
|
@@ -5,12 +5,12 @@
|
|
|
5
5
|
* in-memory files with a FAKE pdf.js reader and a FAKE measurer, so each outcome — read failed, not
|
|
6
6
|
* measured, measured — is chosen rather than hoped for. Reading real PDFs is
|
|
7
7
|
* `pdf-facts.harness.ts`'s job; banal as the measurer is `adapters/banal/index.test.ts`, and the
|
|
8
|
-
* real banal is `test/e2e/banal.ts`.
|
|
8
|
+
* real banal is `test/e2e/tex/banal.e2e.ts`.
|
|
9
9
|
*/
|
|
10
10
|
import assert from "node:assert/strict";
|
|
11
11
|
import { readdirSync, readFileSync } from "node:fs";
|
|
12
12
|
import { join } from "node:path";
|
|
13
|
-
import {
|
|
13
|
+
import { presetsDir } from "./package-dirs.ts";
|
|
14
14
|
import { test } from "vitest";
|
|
15
15
|
import { memoryFiles } from "./adapters/memory/index.ts";
|
|
16
16
|
import {
|
|
@@ -154,12 +154,9 @@ const good: PdfReader = () =>
|
|
|
154
154
|
});
|
|
155
155
|
/** The shipped presets at their real paths: `extends` resolves through the same Files port. */
|
|
156
156
|
const SHIPPED_PRESETS = Object.fromEntries(
|
|
157
|
-
readdirSync(
|
|
157
|
+
readdirSync(presetsDir())
|
|
158
158
|
.filter((f) => /\.jsonc?$/.test(f))
|
|
159
|
-
.map((f) => [
|
|
160
|
-
join(packageVenuesDir(), f),
|
|
161
|
-
readFileSync(join(packageVenuesDir(), f)),
|
|
162
|
-
]),
|
|
159
|
+
.map((f) => [join(presetsDir(), f), readFileSync(join(presetsDir(), f))]),
|
|
163
160
|
);
|
|
164
161
|
|
|
165
162
|
/** Options over in-memory files: the PDF and paperlint.json on "disk", a measurer answering `geometry`. */
|
package/src/facts-file.ts
CHANGED
|
@@ -45,11 +45,11 @@ import {
|
|
|
45
45
|
} from "./domain/geometry.ts";
|
|
46
46
|
import { parseSha256, sha256Hex, type Sha256 } from "./domain/sha256.ts";
|
|
47
47
|
import type { MeasureGeometry } from "./ports/measure-geometry.ts";
|
|
48
|
-
import
|
|
48
|
+
import { callerPath } from "./caller-path.ts";
|
|
49
49
|
import type { Files } from "./ports/files.ts";
|
|
50
50
|
import { err, ok, type Result } from "./domain/result.ts";
|
|
51
51
|
import { paperPreset, paperPresetProblem } from "./presets.ts";
|
|
52
|
-
import {
|
|
52
|
+
import { presetsDir } from "./package-dirs.ts";
|
|
53
53
|
import { messageOf } from "./domain/text.ts";
|
|
54
54
|
|
|
55
55
|
export const FACTS_SCHEMA = 2;
|
|
@@ -60,12 +60,6 @@ export const FACTS_FILE = "paper.facts.json";
|
|
|
60
60
|
export const factsPath = (paperDir: string): string =>
|
|
61
61
|
join(paperDir, FACTS_DIR, FACTS_FILE);
|
|
62
62
|
|
|
63
|
-
/**
|
|
64
|
-
* A path as the caller gave it. The `Files` adapter resolves a relative one against the process's
|
|
65
|
-
* cwd, which is what these callers have always meant; the brand is not a claim this code checked it.
|
|
66
|
-
*/
|
|
67
|
-
const at = (p: string): AbsolutePath => p as AbsolutePath;
|
|
68
|
-
|
|
69
63
|
/** What the facts need from a paper's `paperlint.json`: its venue's label, its kind, where its PDF is. */
|
|
70
64
|
export interface VenueDecl {
|
|
71
65
|
/** The venue preset's display label (`agenticdev`), or null when the paper extends none. */
|
|
@@ -82,7 +76,7 @@ export interface VenueDecl {
|
|
|
82
76
|
export function declaredVenue(
|
|
83
77
|
files: Files,
|
|
84
78
|
paperDir: string,
|
|
85
|
-
venuesDir: string =
|
|
79
|
+
venuesDir: string = presetsDir(),
|
|
86
80
|
): VenueDecl | null {
|
|
87
81
|
const p = paperPreset(paperDir, { files, venuesDir });
|
|
88
82
|
const problem = paperPresetProblem(paperDir, p);
|
|
@@ -213,7 +207,7 @@ export async function measurePaper(
|
|
|
213
207
|
): Promise<Result<Measured, string>> {
|
|
214
208
|
const r = await o.readPdf(pdf);
|
|
215
209
|
if (!r.ok) return err(describeFailure(r, pdf));
|
|
216
|
-
const bytes = o.files.readBytes(
|
|
210
|
+
const bytes = o.files.readBytes(callerPath(pdf));
|
|
217
211
|
if (bytes === null) return err(`${pdf}: gone after pdf.js read it`);
|
|
218
212
|
const geometry = o.measure.measure(r.facts.layout);
|
|
219
213
|
const decl = declaredVenue(o.files, paperDir);
|
|
@@ -239,7 +233,7 @@ export function writeFactsFile(
|
|
|
239
233
|
): string {
|
|
240
234
|
const out = factsPath(paperDir);
|
|
241
235
|
files.writeAtomic(
|
|
242
|
-
|
|
236
|
+
callerPath(out),
|
|
243
237
|
new TextEncoder().encode(`${JSON.stringify(facts, null, 2)}\n`),
|
|
244
238
|
);
|
|
245
239
|
return out;
|
|
@@ -288,6 +282,34 @@ const MAYBE_NUMBERS = [
|
|
|
288
282
|
] as const;
|
|
289
283
|
const NUMBERS = ["body_pages", "ref_pages", "appendix_pages"] as const;
|
|
290
284
|
|
|
285
|
+
/** The geometry columns as the file holds them once checked. */
|
|
286
|
+
type GeometryColumns = {
|
|
287
|
+
readonly [K in (typeof MAYBE_NUMBERS)[number]]: number | null;
|
|
288
|
+
} & { readonly [K in (typeof NUMBERS)[number]]: number } & {
|
|
289
|
+
readonly pages_by_type: Readonly<Record<string, number>>;
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
const isCounts = (v: unknown): v is Readonly<Record<string, number>> =>
|
|
293
|
+
isRecord(v) && Object.values(v).every(isNum);
|
|
294
|
+
|
|
295
|
+
/** Every geometry column has its type: the check the type claims, and nothing else. */
|
|
296
|
+
const hasGeometryColumns = (
|
|
297
|
+
d: Readonly<Record<string, unknown>>,
|
|
298
|
+
): d is Readonly<Record<string, unknown>> & GeometryColumns =>
|
|
299
|
+
MAYBE_NUMBERS.every((k) => numOrNull(d[k])) &&
|
|
300
|
+
NUMBERS.every((k) => isNum(d[k])) &&
|
|
301
|
+
isCounts(d["pages_by_type"]);
|
|
302
|
+
|
|
303
|
+
/** Which geometry column is wrong, for a record `hasGeometryColumns` refused. */
|
|
304
|
+
function geometryError(d: Readonly<Record<string, unknown>>): string {
|
|
305
|
+
const bad =
|
|
306
|
+
MAYBE_NUMBERS.find((k) => !numOrNull(d[k])) ??
|
|
307
|
+
NUMBERS.find((k) => !isNum(d[k]));
|
|
308
|
+
return bad
|
|
309
|
+
? `\`${bad}\` is not a number`
|
|
310
|
+
: "`pages_by_type` is not an object of page counts";
|
|
311
|
+
}
|
|
312
|
+
|
|
291
313
|
/** The geometry columns when a measurer ran; the shape error otherwise. */
|
|
292
314
|
function geometryOf(
|
|
293
315
|
d: Readonly<Record<string, unknown>>,
|
|
@@ -295,24 +317,17 @@ function geometryOf(
|
|
|
295
317
|
if (d["geometry_source"] === null) return ok(null);
|
|
296
318
|
if (typeof d["geometry_source"] !== "string")
|
|
297
319
|
return err("`geometry_source` is neither a measurer's name nor null");
|
|
298
|
-
|
|
299
|
-
MAYBE_NUMBERS.find((k) => !numOrNull(d[k])) ??
|
|
300
|
-
NUMBERS.find((k) => !isNum(d[k]));
|
|
301
|
-
if (bad) return err(`\`${bad}\` is not a number`);
|
|
302
|
-
const pagesByType = d["pages_by_type"];
|
|
303
|
-
if (!isRecord(pagesByType) || !Object.values(pagesByType).every(isNum))
|
|
304
|
-
return err("`pages_by_type` is not an object of page counts");
|
|
305
|
-
const n = (k: (typeof MAYBE_NUMBERS)[number]) => d[k] as number | null;
|
|
320
|
+
if (!hasGeometryColumns(d)) return err(geometryError(d));
|
|
306
321
|
return ok({
|
|
307
|
-
page_w_in:
|
|
308
|
-
page_h_in:
|
|
309
|
-
columns:
|
|
310
|
-
body_pt:
|
|
311
|
-
ref_pt:
|
|
312
|
-
body_pages: d
|
|
313
|
-
ref_pages: d
|
|
314
|
-
appendix_pages: d
|
|
315
|
-
pages_by_type:
|
|
322
|
+
page_w_in: d.page_w_in,
|
|
323
|
+
page_h_in: d.page_h_in,
|
|
324
|
+
columns: d.columns,
|
|
325
|
+
body_pt: d.body_pt,
|
|
326
|
+
ref_pt: d.ref_pt,
|
|
327
|
+
body_pages: d.body_pages,
|
|
328
|
+
ref_pages: d.ref_pages,
|
|
329
|
+
appendix_pages: d.appendix_pages,
|
|
330
|
+
pages_by_type: d.pages_by_type,
|
|
316
331
|
});
|
|
317
332
|
}
|
|
318
333
|
|
|
@@ -348,7 +363,7 @@ export function parseFactsText(text: string): Result<ReadFacts, FactsProblem> {
|
|
|
348
363
|
if (!artifact.ok) return err({ kind: "broken", why: artifact.error });
|
|
349
364
|
const rawFonts = d["fonts"];
|
|
350
365
|
const fonts = Array.isArray(rawFonts) ? rawFonts.map(fontOf) : null;
|
|
351
|
-
if (fonts === null || fonts.
|
|
366
|
+
if (fonts === null || !fonts.every((f) => f !== null))
|
|
352
367
|
return err({
|
|
353
368
|
kind: "broken",
|
|
354
369
|
why: "`fonts` is not a list of { name, type, embedded, program }",
|
|
@@ -357,7 +372,7 @@ export function parseFactsText(text: string): Result<ReadFacts, FactsProblem> {
|
|
|
357
372
|
if (!geometry.ok) return err({ kind: "broken", why: geometry.error });
|
|
358
373
|
return ok({
|
|
359
374
|
...artifact.value,
|
|
360
|
-
fonts
|
|
375
|
+
fonts,
|
|
361
376
|
geometry: geometry.value,
|
|
362
377
|
});
|
|
363
378
|
}
|
|
@@ -118,6 +118,23 @@ try {
|
|
|
118
118
|
threw,
|
|
119
119
|
);
|
|
120
120
|
|
|
121
|
+
const malformed = join(work, "malformed-hooks.json");
|
|
122
|
+
writeFileSync(
|
|
123
|
+
malformed,
|
|
124
|
+
'{"hooks":{"PreToolUse":[{"hooks":[{"type":"command"}]}]}}',
|
|
125
|
+
);
|
|
126
|
+
let why = "";
|
|
127
|
+
try {
|
|
128
|
+
shippedWiring(malformed);
|
|
129
|
+
} catch (e) {
|
|
130
|
+
why = e instanceof Error ? e.message : String(e);
|
|
131
|
+
}
|
|
132
|
+
check(
|
|
133
|
+
"a wiring file whose `hooks` is not Claude Code's shape THROWS, naming the shape",
|
|
134
|
+
why.startsWith(`${malformed}: "hooks" is not a map of event`),
|
|
135
|
+
why,
|
|
136
|
+
);
|
|
137
|
+
|
|
121
138
|
// ── which spelling runs which hook ─────────────────────────────────────────────────────
|
|
122
139
|
const ours = `node "\${CLAUDE_PROJECT_DIR}/${MANAGED_BY}" hook paper-edit-guard`;
|
|
123
140
|
const cases: [string, { name: string; ours: boolean } | null][] = [
|