paperlint 3.1.3 → 4.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/workflows/ci.yml +12 -7
- package/CLAUDE.md +5 -5
- package/CONTRIBUTING.md +34 -5
- 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 +4 -3
- 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/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/files.io.d.ts.map +1 -1
- package/dist/adapters/node/files.io.js +2 -1
- package/dist/adapters/node/files.io.js.map +1 -1
- package/dist/adapters/node/process.io.d.ts +5 -7
- package/dist/adapters/node/process.io.d.ts.map +1 -1
- package/dist/adapters/node/process.io.js +4 -3
- 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 +13 -7
- 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 +8 -7
- package/dist/adapters/references/index.js.map +1 -1
- package/dist/adapters/references/pool.d.ts.map +1 -1
- package/dist/adapters/references/pool.js +4 -5
- package/dist/adapters/references/pool.js.map +1 -1
- package/dist/adapters/references/reach.io.d.ts.map +1 -1
- package/dist/adapters/references/reach.io.js +2 -1
- package/dist/adapters/references/reach.io.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 +10 -3
- 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 +19 -17
- 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 +93 -46
- 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 +5 -3
- package/dist/doctor.js.map +1 -1
- package/dist/domain/lookup-cache.d.ts.map +1 -1
- package/dist/domain/lookup-cache.js +10 -8
- package/dist/domain/lookup-cache.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/text.d.ts +12 -0
- package/dist/domain/text.d.ts.map +1 -1
- package/dist/domain/text.js +17 -0
- package/dist/domain/text.js.map +1 -1
- package/dist/engine.d.ts +23 -22
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +4 -3
- package/dist/engine.js.map +1 -1
- package/dist/eslint-rules/bib-reachable-entry.d.ts +22 -0
- package/dist/eslint-rules/bib-reachable-entry.d.ts.map +1 -0
- package/dist/eslint-rules/bib-reachable-entry.js +75 -0
- package/dist/eslint-rules/bib-reachable-entry.js.map +1 -0
- package/dist/eslint-rules/install-path-literals.d.ts +80 -0
- package/dist/eslint-rules/install-path-literals.d.ts.map +1 -0
- package/dist/eslint-rules/install-path-literals.js +105 -0
- package/dist/eslint-rules/install-path-literals.js.map +1 -0
- package/dist/eslint-rules/latex-language.d.ts +136 -0
- package/dist/eslint-rules/latex-language.d.ts.map +1 -0
- package/dist/eslint-rules/latex-language.js +639 -0
- package/dist/eslint-rules/latex-language.js.map +1 -0
- package/dist/eslint-rules/paper-research-question.d.ts +31 -0
- package/dist/eslint-rules/paper-research-question.d.ts.map +1 -0
- package/dist/eslint-rules/paper-research-question.js +188 -0
- package/dist/eslint-rules/paper-research-question.js.map +1 -0
- package/dist/eslint-rules/paper-stages.d.ts +83 -0
- package/dist/eslint-rules/paper-stages.d.ts.map +1 -0
- package/dist/eslint-rules/paper-stages.js +358 -0
- package/dist/eslint-rules/paper-stages.js.map +1 -0
- package/dist/eslint-rules/paper-typography.d.ts +74 -0
- package/dist/eslint-rules/paper-typography.d.ts.map +1 -0
- package/dist/eslint-rules/paper-typography.js +404 -0
- package/dist/eslint-rules/paper-typography.js.map +1 -0
- package/dist/eslint-rules/papers.d.ts +42 -0
- package/dist/eslint-rules/papers.d.ts.map +1 -0
- package/dist/eslint-rules/papers.js +145 -0
- package/dist/eslint-rules/papers.js.map +1 -0
- package/dist/eslint-rules/pdf-last-page-balance.d.ts +72 -0
- package/dist/eslint-rules/pdf-last-page-balance.d.ts.map +1 -0
- package/dist/eslint-rules/pdf-last-page-balance.js +237 -0
- package/dist/eslint-rules/pdf-last-page-balance.js.map +1 -0
- package/dist/eslint-rules/review-frontmatter.d.ts +57 -0
- package/dist/eslint-rules/review-frontmatter.d.ts.map +1 -0
- package/dist/eslint-rules/review-frontmatter.js +126 -0
- package/dist/eslint-rules/review-frontmatter.js.map +1 -0
- package/dist/eslint-rules/review-frontmatter.schema.json +36 -0
- package/dist/eslint-rules/rule-context.d.ts +55 -0
- package/dist/eslint-rules/rule-context.d.ts.map +1 -0
- package/dist/eslint-rules/rule-context.js +9 -0
- package/dist/eslint-rules/rule-context.js.map +1 -0
- package/dist/eslint-rules/sibling-frontmatter.d.ts +25 -0
- package/dist/eslint-rules/sibling-frontmatter.d.ts.map +1 -0
- package/dist/eslint-rules/sibling-frontmatter.js +16 -0
- package/dist/eslint-rules/sibling-frontmatter.js.map +1 -0
- package/dist/eslint-rules/sibling-frontmatter.schema.json +14 -0
- package/dist/eslint-rules/temp-root-realpath.d.ts +71 -0
- package/dist/eslint-rules/temp-root-realpath.d.ts.map +1 -0
- package/dist/eslint-rules/temp-root-realpath.js +139 -0
- package/dist/eslint-rules/temp-root-realpath.js.map +1 -0
- package/dist/eslint-rules/tex-build.d.ts +35 -0
- package/dist/eslint-rules/tex-build.d.ts.map +1 -0
- package/dist/eslint-rules/tex-build.js +313 -0
- package/dist/eslint-rules/tex-build.js.map +1 -0
- package/dist/facts-file.d.ts.map +1 -1
- package/dist/facts-file.js +31 -27
- package/dist/facts-file.js.map +1 -1
- package/dist/hooks-settings.d.ts +1 -1
- package/dist/hooks-settings.d.ts.map +1 -1
- package/dist/hooks-settings.js +31 -11
- package/dist/hooks-settings.js.map +1 -1
- package/dist/init.d.ts +8 -9
- package/dist/init.d.ts.map +1 -1
- package/dist/init.js +33 -15
- package/dist/init.js.map +1 -1
- package/dist/latex-loop.d.ts +2 -2
- package/dist/latex-loop.d.ts.map +1 -1
- package/dist/latex-loop.js +2 -2
- package/dist/latex-loop.js.map +1 -1
- package/dist/lib/agent-cli-version.d.ts +65 -0
- package/dist/lib/agent-cli-version.d.ts.map +1 -0
- package/dist/lib/agent-cli-version.js +77 -0
- package/dist/lib/agent-cli-version.js.map +1 -0
- package/dist/lib/check.d.ts +19 -0
- package/dist/lib/check.d.ts.map +1 -0
- package/dist/lib/check.js +52 -0
- package/dist/lib/check.js.map +1 -0
- package/dist/lib/markdown.d.ts +180 -0
- package/dist/lib/markdown.d.ts.map +1 -0
- package/dist/lib/markdown.js +302 -0
- package/dist/lib/markdown.js.map +1 -0
- package/dist/lib/paper-config.d.ts +84 -0
- package/dist/lib/paper-config.d.ts.map +1 -0
- package/dist/lib/paper-config.js +138 -0
- package/dist/lib/paper-config.js.map +1 -0
- package/dist/lib/skill-eval-fixture.d.ts +29 -0
- package/dist/lib/skill-eval-fixture.d.ts.map +1 -0
- package/dist/lib/skill-eval-fixture.js +62 -0
- package/dist/lib/skill-eval-fixture.js.map +1 -0
- package/dist/link-skills.d.ts.map +1 -1
- package/dist/link-skills.js +5 -4
- package/dist/link-skills.js.map +1 -1
- package/dist/new-paper.d.ts.map +1 -1
- package/dist/new-paper.js +7 -3
- package/dist/new-paper.js.map +1 -1
- 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.d.ts.map +1 -1
- package/dist/paper-settings.js +8 -7
- 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 +25 -16
- package/dist/pdf-facts.js.map +1 -1
- package/dist/pdf-geometry.d.ts.map +1 -1
- package/dist/pdf-geometry.js +3 -4
- package/dist/pdf-geometry.js.map +1 -1
- package/dist/ports/tool-installer.d.ts +2 -14
- package/dist/ports/tool-installer.d.ts.map +1 -1
- package/dist/presets.d.ts.map +1 -1
- package/dist/presets.js +19 -6
- package/dist/presets.js.map +1 -1
- package/dist/references.d.ts.map +1 -1
- package/dist/references.js +15 -10
- 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 +15 -12
- package/dist/rules-config.js.map +1 -1
- package/dist/structure.js +1 -1
- package/dist/structure.js.map +1 -1
- package/dist/tex-requirements.d.ts +4 -0
- package/dist/tex-requirements.d.ts.map +1 -1
- package/dist/tex-requirements.js +22 -5
- package/dist/tex-requirements.js.map +1 -1
- package/dist/toolchain.d.ts.map +1 -1
- package/dist/toolchain.js +21 -16
- package/dist/toolchain.js.map +1 -1
- package/dist/types.d.ts +5 -3
- package/dist/types.d.ts.map +1 -1
- package/dist/venue-rules.d.ts.map +1 -1
- package/dist/venue-rules.js +21 -8
- package/dist/venue-rules.js.map +1 -1
- package/docs/configuration.md +2 -2
- package/docs/e2e.md +7 -7
- 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 +2 -2
- package/docs/testing.md +11 -8
- package/docs/toolchain.md +1 -1
- package/eslint-rules/{bib-reachable-entry.mjs → bib-reachable-entry.ts} +5 -4
- package/eslint-rules/install-path-literals.harness.mjs +1 -1
- package/eslint-rules/{install-path-literals.mjs → install-path-literals.ts} +18 -10
- package/eslint-rules/latex-language.harness.mjs +2 -2
- package/eslint-rules/latex-language.test.mjs +7 -1
- package/eslint-rules/{latex-language.mjs → latex-language.ts} +297 -109
- package/eslint-rules/paper-research-question.harness.mjs +3 -3
- package/eslint-rules/{paper-research-question.mjs → paper-research-question.ts} +35 -16
- package/eslint-rules/paper-stages.harness.mjs +2 -2
- package/eslint-rules/paper-stages.test.mjs +8 -2
- package/eslint-rules/{paper-stages.mjs → paper-stages.ts} +76 -31
- package/eslint-rules/paper-typography.test.mjs +3 -3
- package/eslint-rules/{paper-typography.mjs → paper-typography.ts} +134 -56
- package/eslint-rules/papers.harness.mjs +2 -2
- package/eslint-rules/{papers.mjs → papers.ts} +7 -4
- package/eslint-rules/pdf-last-page-balance.harness.mjs +3 -3
- package/eslint-rules/{pdf-last-page-balance.mjs → pdf-last-page-balance.ts} +89 -32
- package/eslint-rules/review-frontmatter.test.mjs +2 -2
- package/eslint-rules/{review-frontmatter.mjs → review-frontmatter.ts} +38 -16
- package/eslint-rules/rule-context.ts +62 -0
- package/eslint-rules/{sibling-frontmatter.mjs → sibling-frontmatter.ts} +1 -1
- package/eslint-rules/temp-root-realpath.harness.mjs +1 -1
- package/eslint-rules/{temp-root-realpath.mjs → temp-root-realpath.ts} +20 -7
- package/eslint-rules/tex-build.harness.mjs +2 -2
- package/eslint-rules/{tex-build.mjs → tex-build.ts} +6 -5
- package/eslint.config.mjs +146 -30
- 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 +2 -2
- package/fixtures/real-markdown-paper/{baseline.mjs → baseline.ts} +31 -10
- package/hooks/{hooks.harness.mjs → hooks.harness.ts} +51 -34
- package/hooks/paper-edit-guard.hook.mjs +1 -1
- package/hooks/{paper-edit-guard.test.mjs → paper-edit-guard.test.ts} +3 -7
- package/hooks/paper-skills-nudge.hook.mjs +1 -1
- package/hooks/{paper-skills-nudge.test.mjs → paper-skills-nudge.test.ts} +2 -6
- package/hooks/paper-status-gates.sh +9 -19
- package/hooks/{paper-status-gates.test.mjs → paper-status-gates.test.ts} +2 -6
- package/lib/agent-cli-version.harness.mjs +1 -1
- package/lib/{agent-cli-version.mjs → agent-cli-version.ts} +32 -18
- package/lib/check.test.ts +14 -9
- package/lib/{check.mjs → check.ts} +22 -9
- package/lib/markdown.test.mjs +7 -7
- package/lib/{markdown.mjs → markdown.ts} +68 -29
- package/lib/paper-config.harness.mjs +9 -9
- package/lib/{paper-config.mjs → paper-config.ts} +20 -19
- package/lib/skill-checks.mjs +24 -381
- package/lib/skill-checks.test.mjs +64 -116
- package/lib/skill-corpus.mjs +10 -251
- package/lib/skill-corpus.test.mjs +2 -13
- package/lib/skill-eval-kit.mjs +1 -1
- package/lib/skill-trigger-cases.mjs +1 -1
- package/package.json +42 -10
- package/scripts/{check.harness.mjs → check.harness.ts} +111 -10
- package/scripts/{check.test.mjs → check.test.ts} +13 -10
- package/scripts/{check.mjs → check.ts} +116 -42
- package/scripts/coverage-config.test.ts +80 -38
- package/scripts/eslint-report-guard.test.mjs +1 -1
- package/scripts/harness-api.frozen.json +14 -16
- package/scripts/harness-api.test.ts +8 -5
- package/scripts/layer-legacy-frozen.test.ts +17 -13
- package/scripts/{layer-legacy-frozen.mjs → layer-legacy-frozen.ts} +128 -39
- package/scripts/release-config.test.ts +12 -4
- package/scripts/rules-see-files.test.mjs +1 -1
- package/scripts/test.test.ts +78 -0
- package/scripts/test.ts +87 -0
- 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 +3 -4
- package/skills/grade-paper-writing/{prose-lint.test.mjs → prose-lint.test.ts} +3 -3
- 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.mjs → 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/SKILL.md +2 -28
- package/skills/paper-pipeline/SKILL.md.spec.ts +1 -27
- package/skills/paper-pipeline/description-language.eval.mjs +1 -1
- package/skills/paper-pipeline/framing-vs-vocabulary.eval.mjs +1 -1
- package/skills/paper-pipeline/grade-paper-writing-ablation.eval.mjs +2 -2
- package/skills/paper-pipeline/paper-pipeline.harness.mjs +25 -40
- package/skills/paper-pipeline/pipeline-firing.eval.mjs +1 -1
- package/skills/paper-pipeline/pipeline-language.eval.mjs +1 -1
- 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/artifact-coverage.mjs +1 -1
- package/skills/paper-pipeline/scripts/{artifact-coverage.test.mjs → artifact-coverage.test.ts} +2 -2
- package/skills/paper-pipeline/scripts/consumer.harness.mjs +6 -104
- package/skills/paper-pipeline/scripts/consumer.mjs +24 -109
- package/skills/paper-pipeline/scripts/consumer.test.mjs +1 -1
- package/skills/paper-pipeline/scripts/extract-ref-facts.harness.mjs +1 -1
- package/skills/paper-pipeline/scripts/extract-ref-facts.mjs +1 -4
- package/skills/paper-pipeline/scripts/extract-ref-facts.test.mjs +1 -1
- package/skills/paper-pipeline/scripts/generated-code.harness.mjs +2 -3
- package/skills/paper-pipeline/scripts/{generated-code.test.mjs → generated-code.test.ts} +3 -3
- package/skills/paper-pipeline/scripts/population-map.harness.mjs +3 -3
- package/skills/paper-pipeline/scripts/population-map.mjs +1 -4
- package/skills/paper-pipeline/scripts/population-map.test.mjs +1 -1
- package/skills/paper-pipeline/scripts/{round-diff.harness.mjs → round-diff.harness.ts} +45 -18
- package/skills/paper-pipeline/scripts/round-diff.mjs +1 -1
- package/skills/paper-pipeline/scripts/round-diff.test.mjs +1 -1
- 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/plan-paper-timeline/fixtures/{fake-google-calendar.mjs → fake-google-calendar.ts} +101 -70
- package/skills/plan-paper-timeline/plan-paper-timeline.effects.harness.mjs +3 -3
- package/skills/render-paper/SKILL.md +1 -37
- package/skills/render-paper/SKILL.md.spec.ts +0 -36
- package/skills/render-paper/{extract-pdf-facts.harness.mjs → extract-pdf-facts.harness.ts} +34 -9
- package/skills/render-paper/render-paper.harness.mjs +2 -2
- 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 +5 -12
- package/skills/tighten-paper/{structure.test.mjs → structure.test.ts} +4 -3
- package/skills/verify-citations/SKILL.md +2 -38
- package/skills/verify-citations/SKILL.md.spec.ts +1 -37
- package/skills/verify-citations/scripts/{bib-authors.cli.test.mjs → bib-authors.cli.test.ts} +14 -4
- package/skills/verify-citations/scripts/bib-authors.d.mts +63 -0
- package/skills/verify-citations/scripts/bib-authors.mjs +1 -4
- package/skills/verify-citations/scripts/{verify-cites.cli.test.mjs → verify-cites.cli.test.ts} +32 -9
- package/skills/verify-citations/scripts/verify-cites.d.mts +58 -0
- package/src/CLAUDE.md +1 -1
- package/src/adapters/banal/failure.ts +9 -10
- package/src/adapters/banal/index.test.ts +5 -8
- package/src/adapters/banal/index.ts +8 -7
- package/src/adapters/banal/install.ts +9 -4
- package/src/adapters/banal/invocation.test.ts +23 -9
- 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 +5 -5
- 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 +8 -6
- package/src/adapters/memory/index.ts +4 -3
- package/src/adapters/node/files.io.ts +2 -2
- package/src/adapters/node/files.test.ts +8 -4
- package/src/adapters/node/process.io.ts +13 -5
- package/src/adapters/node/process.test.ts +5 -5
- package/src/adapters/node/workspace.io.ts +9 -4
- package/src/adapters/node/workspace.test.ts +3 -1
- package/src/adapters/pdfjs/fill.test.ts +2 -2
- package/src/adapters/pdfjs/fill.ts +14 -7
- package/src/adapters/references/index.test.ts +31 -38
- package/src/adapters/references/index.ts +33 -60
- package/src/adapters/references/pool.ts +3 -5
- package/src/adapters/references/reach.io.ts +2 -1
- package/src/build-engine.harness.mjs +1 -1
- package/src/build-engine.test.ts +13 -8
- package/src/build-engine.ts +12 -7
- package/src/build.harness.mjs +2 -2
- package/src/build.test.ts +24 -24
- package/src/build.ts +34 -26
- package/src/caller-path.ts +13 -0
- package/src/cli.harness.mjs +6 -8
- package/src/cli.test.ts +31 -13
- package/src/cli.ts +117 -58
- package/src/doctor.harness.mjs +3 -3
- package/src/doctor.test.ts +1 -1
- package/src/doctor.ts +17 -7
- package/src/domain/lookup-cache.ts +21 -11
- 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/result.test.ts +2 -2
- package/src/domain/sha256.ts +5 -3
- package/src/domain/text.test.ts +32 -0
- package/src/domain/text.ts +23 -0
- package/src/engine.harness.mjs +2 -2
- package/src/engine.ts +34 -5
- package/src/facts-file.test.ts +9 -7
- package/src/facts-file.ts +45 -29
- package/src/{hooks-settings.harness.mjs → hooks-settings.harness.ts} +56 -20
- package/src/hooks-settings.ts +48 -13
- package/src/init.test.ts +37 -14
- package/src/init.ts +48 -24
- package/src/{latex-log.harness.mjs → latex-log.harness.ts} +14 -14
- package/src/{latex-loop.harness.mjs → latex-loop.harness.ts} +64 -36
- package/src/latex-loop.ts +3 -3
- package/src/{link-skills.harness.mjs → link-skills.harness.ts} +19 -19
- package/src/link-skills.test.ts +3 -3
- package/src/link-skills.ts +5 -4
- package/src/lint-fix.test.ts +4 -6
- package/src/linted-set.test.ts +2 -2
- package/src/new-paper.harness.mjs +9 -25
- package/src/new-paper.test.ts +8 -10
- package/src/new-paper.ts +9 -5
- package/src/paper-files.test.ts +25 -24
- package/src/paper-files.ts +16 -27
- package/src/paper-settings-commands.test.ts +5 -4
- package/src/paper-settings.test.ts +7 -7
- package/src/paper-settings.ts +13 -14
- package/src/{pdf-facts.harness.mjs → pdf-facts.harness.ts} +9 -8
- package/src/pdf-facts.test.ts +25 -15
- package/src/pdf-facts.ts +81 -23
- package/src/{pdf-geometry.harness.mjs → pdf-geometry.harness.ts} +19 -12
- package/src/pdf-geometry.ts +8 -4
- package/src/ports/tool-installer.ts +2 -17
- package/src/presets.ts +31 -16
- package/src/references.test.ts +76 -62
- package/src/references.ts +18 -12
- package/src/{rules-config.harness.mjs → rules-config.harness.ts} +7 -13
- package/src/rules-config.ts +46 -14
- package/src/rules-off.test.ts +6 -4
- package/src/{structure.harness.mjs → structure.harness.ts} +26 -15
- package/src/structure.ts +1 -1
- package/src/{tex-requirements.harness.mjs → tex-requirements.harness.ts} +50 -22
- package/src/tex-requirements.test.ts +21 -0
- package/src/tex-requirements.ts +37 -11
- package/src/toolchain.harness.mjs +2 -2
- package/src/toolchain.test.ts +10 -6
- package/src/toolchain.ts +31 -17
- package/src/types.ts +5 -3
- package/src/venue-rules.test.ts +70 -68
- package/src/venue-rules.ts +22 -12
- package/tsconfig.json +6 -3
- package/tsconfig.pkg.json +10 -0
- package/lib/check.d.mts +0 -17
- package/lib/paper-config.d.mts +0 -31
- package/scripts/layer-legacy-frozen.d.mts +0 -36
- package/scripts/rules-are-content-only.harness.mjs +0 -105
- package/scripts/rules-are-content-only.mjs +0 -148
- package/scripts/rules-are-content-only.test.mjs +0 -61
- package/scripts/test.mjs +0 -44
- package/scripts/test.test.mjs +0 -52
- 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.mjs +0 -71
- 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 -753
- 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/bin/{paperlint.test.mjs → paperlint.test.ts} +0 -0
- /package/lib/{skill-eval-fixture.mjs → skill-eval-fixture.ts} +0 -0
- /package/skills/grade-paper-writing/__snapshots__/{prose-lint.test.mjs.snap → prose-lint.test.ts.snap} +0 -0
- /package/skills/tighten-paper/__snapshots__/{structure.test.mjs.snap → structure.test.ts.snap} +0 -0
|
@@ -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);
|
|
@@ -3,7 +3,7 @@ import { test } from "vitest";
|
|
|
3
3
|
import { andThen, err, map, match, ok, type Result } from "./result.ts";
|
|
4
4
|
|
|
5
5
|
const half = (n: number): Result<number, string> =>
|
|
6
|
-
n % 2 === 0 ? ok(n / 2) : err(`${n} is odd`);
|
|
6
|
+
n % 2 === 0 ? ok(n / 2) : err(`${String(n)} is odd`);
|
|
7
7
|
|
|
8
8
|
test("map and andThen run on ok and pass an error through untouched", () => {
|
|
9
9
|
assert.deepEqual(
|
|
@@ -24,7 +24,7 @@ test("map and andThen run on ok and pass an error through untouched", () => {
|
|
|
24
24
|
|
|
25
25
|
test("match picks the branch by `ok`", () => {
|
|
26
26
|
const say = (r: Result<number, string>) =>
|
|
27
|
-
match(r, { ok: (n) => `got ${n}`, err: (e) => `no: ${e}` });
|
|
27
|
+
match(r, { ok: (n) => `got ${String(n)}`, err: (e) => `no: ${e}` });
|
|
28
28
|
assert.equal(say(half(4)), "got 2");
|
|
29
29
|
assert.equal(say(half(5)), "no: 5 is odd");
|
|
30
30
|
});
|
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,32 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { test } from "vitest";
|
|
3
|
+
import { codeOf, firstLine, messageOf, printed } from "./text.ts";
|
|
4
|
+
|
|
5
|
+
test("messageOf: an Error's message, and anything else thrown as its text", () => {
|
|
6
|
+
assert.equal(messageOf(new Error("boom")), "boom");
|
|
7
|
+
// `throw "x"` and `throw 3` are legal JavaScript; `(e as Error).message` read them as undefined.
|
|
8
|
+
assert.equal(messageOf("plain string"), "plain string");
|
|
9
|
+
assert.equal(messageOf(3), "3");
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
test("codeOf: a string `code` only", () => {
|
|
13
|
+
assert.equal(
|
|
14
|
+
codeOf(Object.assign(new Error("x"), { code: "ENOENT" })),
|
|
15
|
+
"ENOENT",
|
|
16
|
+
);
|
|
17
|
+
assert.equal(codeOf(new Error("no code")), undefined);
|
|
18
|
+
assert.equal(codeOf({ code: 3 }), undefined);
|
|
19
|
+
assert.equal(codeOf(null), undefined);
|
|
20
|
+
assert.equal(codeOf("ENOENT"), undefined);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test("printed: a stream a failed spawn left undefined reads as empty text", () => {
|
|
24
|
+
assert.equal(printed(undefined), "");
|
|
25
|
+
assert.equal(printed(null), "");
|
|
26
|
+
assert.equal(printed("out"), "out");
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test("firstLine: the first non-blank line, trimmed", () => {
|
|
30
|
+
assert.equal(firstLine("\n \n first \nsecond"), "first");
|
|
31
|
+
assert.equal(firstLine(""), "");
|
|
32
|
+
});
|
package/src/domain/text.ts
CHANGED
|
@@ -9,3 +9,26 @@ export const firstLine = (s: string): string =>
|
|
|
9
9
|
.split("\n")
|
|
10
10
|
.map((l) => l.trim())
|
|
11
11
|
.find(Boolean) ?? "";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* What a process printed on one stream, as text. Node types `spawnSync(…, { encoding })`'s
|
|
15
|
+
* `stdout`/`stderr` as `string`, but both are `undefined` when the spawn itself failed (measured:
|
|
16
|
+
* `spawnSync("no-such-program", [], { encoding: "utf8" }).stdout` is `undefined`). The parameter
|
|
17
|
+
* takes the stream as it really arrives, so the fallback guards a real case instead of reading
|
|
18
|
+
* as dead code against a type that lies.
|
|
19
|
+
*/
|
|
20
|
+
export const printed = (stream: string | null | undefined): string =>
|
|
21
|
+
stream ?? "";
|
|
22
|
+
|
|
23
|
+
/** What a caught value says: an `Error`'s message, anything else as text (`catch` gets `unknown`). */
|
|
24
|
+
export const messageOf = (e: unknown): string =>
|
|
25
|
+
e instanceof Error ? e.message : String(e);
|
|
26
|
+
|
|
27
|
+
/** The `code` a Node error carries (`ENOENT`, `EACCES`, …), when it carries a string one. */
|
|
28
|
+
export const codeOf = (e: unknown): string | undefined =>
|
|
29
|
+
typeof e === "object" &&
|
|
30
|
+
e !== null &&
|
|
31
|
+
"code" in e &&
|
|
32
|
+
typeof e.code === "string"
|
|
33
|
+
? e.code
|
|
34
|
+
: undefined;
|
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.
|
|
8
|
+
* All pure or port-driven: no TeX needed. The real-TeX half is `test/e2e/build.ts`.
|
|
9
9
|
*/
|
|
10
10
|
import {
|
|
11
11
|
chmodSync,
|
|
@@ -18,7 +18,7 @@ import {
|
|
|
18
18
|
import { tmpdir } from "node:os";
|
|
19
19
|
import { dirname, join } from "node:path";
|
|
20
20
|
import { fileURLToPath } from "node:url";
|
|
21
|
-
import { createChecker } from "../lib/check.
|
|
21
|
+
import { createChecker } from "../lib/check.ts";
|
|
22
22
|
|
|
23
23
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
24
24
|
const {
|
package/src/engine.ts
CHANGED
|
@@ -24,9 +24,38 @@ import { spawnSync } from "node:child_process";
|
|
|
24
24
|
import { accessSync, constants, statSync } from "node:fs";
|
|
25
25
|
import { basename, delimiter, join } from "node:path";
|
|
26
26
|
import type { TexRequirements } from "./tex-requirements.ts";
|
|
27
|
+
import { printed } from "./domain/text.ts";
|
|
27
28
|
|
|
28
|
-
/**
|
|
29
|
-
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;
|
|
30
59
|
|
|
31
60
|
/** One TeX Live and what it lacks for the papers at hand. */
|
|
32
61
|
export interface TreeProbe {
|
|
@@ -161,7 +190,7 @@ export function probeTree(
|
|
|
161
190
|
stdio: ["ignore", "pipe", "pipe"],
|
|
162
191
|
});
|
|
163
192
|
if (r.error) return Object.keys(packages).sort();
|
|
164
|
-
return missingPackages(packages,
|
|
193
|
+
return missingPackages(packages, printed(r.stdout));
|
|
165
194
|
}
|
|
166
195
|
|
|
167
196
|
/**
|
|
@@ -181,7 +210,7 @@ export function missingDependencies(
|
|
|
181
210
|
stdio: ["ignore", "pipe", "pipe"],
|
|
182
211
|
});
|
|
183
212
|
if (r.error) return [];
|
|
184
|
-
const lines =
|
|
213
|
+
const lines = printed(r.stdout).split("\n");
|
|
185
214
|
const start = lines.findIndex(
|
|
186
215
|
(l) => l.trim() === "DEPENDS WITHOUT PACKAGES:",
|
|
187
216
|
);
|
|
@@ -193,7 +222,7 @@ export function missingDependencies(
|
|
|
193
222
|
return [
|
|
194
223
|
...new Set(
|
|
195
224
|
(end < 0 ? section : section.slice(0, end)).map((l) =>
|
|
196
|
-
l.
|
|
225
|
+
l.replace(/ in: [\s\S]*/, "").trim(),
|
|
197
226
|
),
|
|
198
227
|
),
|
|
199
228
|
].sort();
|
package/src/facts-file.test.ts
CHANGED
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
* The document is assembled from hand-made measurements (pure), and `measurePaper` runs on
|
|
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
|
-
* `pdf-facts.harness.
|
|
8
|
-
* real banal is `test/e2e/banal.
|
|
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`.
|
|
9
9
|
*/
|
|
10
10
|
import assert from "node:assert/strict";
|
|
11
11
|
import { readdirSync, readFileSync } from "node:fs";
|
|
@@ -147,10 +147,11 @@ test("🔴 geometry_source and the nine columns come from ONE branch", () => {
|
|
|
147
147
|
const PAPER = "/r/papers/p";
|
|
148
148
|
const PDF = `${PAPER}/paper.pdf`;
|
|
149
149
|
const PDF_BYTES = "%PDF-pretend";
|
|
150
|
-
const good: PdfReader =
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
}
|
|
150
|
+
const good: PdfReader = () =>
|
|
151
|
+
Promise.resolve({
|
|
152
|
+
ok: true,
|
|
153
|
+
facts: { pages: 2, fonts: FONTS, last: lastPage(60, 60), layout: [] },
|
|
154
|
+
});
|
|
154
155
|
/** The shipped presets at their real paths: `extends` resolves through the same Files port. */
|
|
155
156
|
const SHIPPED_PRESETS = Object.fromEntries(
|
|
156
157
|
readdirSync(packageVenuesDir())
|
|
@@ -232,7 +233,8 @@ test("a failed read is the one error line, and nothing is written", async () =>
|
|
|
232
233
|
const before = files.map.size;
|
|
233
234
|
const m = await measurePaper(PAPER, PDF, {
|
|
234
235
|
...o,
|
|
235
|
-
readPdf:
|
|
236
|
+
readPdf: () =>
|
|
237
|
+
Promise.resolve({ ok: false, reason: "unreadable", detail: "boom" }),
|
|
236
238
|
});
|
|
237
239
|
assert.match(!m.ok ? m.error : "", /pdf\.js \(unreadable\): boom/);
|
|
238
240
|
assert.equal(files.map.size, before);
|
package/src/facts-file.ts
CHANGED
|
@@ -45,11 +45,12 @@ 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
52
|
import { packageVenuesDir } from "../skills/paper-pipeline/scripts/consumer.mjs";
|
|
53
|
+
import { messageOf } from "./domain/text.ts";
|
|
53
54
|
|
|
54
55
|
export const FACTS_SCHEMA = 2;
|
|
55
56
|
export const FACTS_DIR = "_build";
|
|
@@ -59,12 +60,6 @@ export const FACTS_FILE = "paper.facts.json";
|
|
|
59
60
|
export const factsPath = (paperDir: string): string =>
|
|
60
61
|
join(paperDir, FACTS_DIR, FACTS_FILE);
|
|
61
62
|
|
|
62
|
-
/**
|
|
63
|
-
* A path as the caller gave it. The `Files` adapter resolves a relative one against the process's
|
|
64
|
-
* cwd, which is what these callers have always meant; the brand is not a claim this code checked it.
|
|
65
|
-
*/
|
|
66
|
-
const at = (p: string): AbsolutePath => p as AbsolutePath;
|
|
67
|
-
|
|
68
63
|
/** What the facts need from a paper's `paperlint.json`: its venue's label, its kind, where its PDF is. */
|
|
69
64
|
export interface VenueDecl {
|
|
70
65
|
/** The venue preset's display label (`agenticdev`), or null when the paper extends none. */
|
|
@@ -212,7 +207,7 @@ export async function measurePaper(
|
|
|
212
207
|
): Promise<Result<Measured, string>> {
|
|
213
208
|
const r = await o.readPdf(pdf);
|
|
214
209
|
if (!r.ok) return err(describeFailure(r, pdf));
|
|
215
|
-
const bytes = o.files.readBytes(
|
|
210
|
+
const bytes = o.files.readBytes(callerPath(pdf));
|
|
216
211
|
if (bytes === null) return err(`${pdf}: gone after pdf.js read it`);
|
|
217
212
|
const geometry = o.measure.measure(r.facts.layout);
|
|
218
213
|
const decl = declaredVenue(o.files, paperDir);
|
|
@@ -238,7 +233,7 @@ export function writeFactsFile(
|
|
|
238
233
|
): string {
|
|
239
234
|
const out = factsPath(paperDir);
|
|
240
235
|
files.writeAtomic(
|
|
241
|
-
|
|
236
|
+
callerPath(out),
|
|
242
237
|
new TextEncoder().encode(`${JSON.stringify(facts, null, 2)}\n`),
|
|
243
238
|
);
|
|
244
239
|
return out;
|
|
@@ -287,6 +282,34 @@ const MAYBE_NUMBERS = [
|
|
|
287
282
|
] as const;
|
|
288
283
|
const NUMBERS = ["body_pages", "ref_pages", "appendix_pages"] as const;
|
|
289
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
|
+
|
|
290
313
|
/** The geometry columns when a measurer ran; the shape error otherwise. */
|
|
291
314
|
function geometryOf(
|
|
292
315
|
d: Readonly<Record<string, unknown>>,
|
|
@@ -294,24 +317,17 @@ function geometryOf(
|
|
|
294
317
|
if (d["geometry_source"] === null) return ok(null);
|
|
295
318
|
if (typeof d["geometry_source"] !== "string")
|
|
296
319
|
return err("`geometry_source` is neither a measurer's name nor null");
|
|
297
|
-
|
|
298
|
-
MAYBE_NUMBERS.find((k) => !numOrNull(d[k])) ??
|
|
299
|
-
NUMBERS.find((k) => !isNum(d[k]));
|
|
300
|
-
if (bad) return err(`\`${bad}\` is not a number`);
|
|
301
|
-
const pagesByType = d["pages_by_type"];
|
|
302
|
-
if (!isRecord(pagesByType) || !Object.values(pagesByType).every(isNum))
|
|
303
|
-
return err("`pages_by_type` is not an object of page counts");
|
|
304
|
-
const n = (k: (typeof MAYBE_NUMBERS)[number]) => d[k] as number | null;
|
|
320
|
+
if (!hasGeometryColumns(d)) return err(geometryError(d));
|
|
305
321
|
return ok({
|
|
306
|
-
page_w_in:
|
|
307
|
-
page_h_in:
|
|
308
|
-
columns:
|
|
309
|
-
body_pt:
|
|
310
|
-
ref_pt:
|
|
311
|
-
body_pages: d
|
|
312
|
-
ref_pages: d
|
|
313
|
-
appendix_pages: d
|
|
314
|
-
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,
|
|
315
331
|
});
|
|
316
332
|
}
|
|
317
333
|
|
|
@@ -338,7 +354,7 @@ export function parseFactsText(text: string): Result<ReadFacts, FactsProblem> {
|
|
|
338
354
|
try {
|
|
339
355
|
d = JSON.parse(text);
|
|
340
356
|
} catch (e) {
|
|
341
|
-
return err({ kind: "broken", why: `not JSON (${(e
|
|
357
|
+
return err({ kind: "broken", why: `not JSON (${messageOf(e)})` });
|
|
342
358
|
}
|
|
343
359
|
if (!isRecord(d)) return err({ kind: "broken", why: "not a JSON object" });
|
|
344
360
|
if (d["schema"] !== FACTS_SCHEMA)
|
|
@@ -347,7 +363,7 @@ export function parseFactsText(text: string): Result<ReadFacts, FactsProblem> {
|
|
|
347
363
|
if (!artifact.ok) return err({ kind: "broken", why: artifact.error });
|
|
348
364
|
const rawFonts = d["fonts"];
|
|
349
365
|
const fonts = Array.isArray(rawFonts) ? rawFonts.map(fontOf) : null;
|
|
350
|
-
if (fonts === null || fonts.
|
|
366
|
+
if (fonts === null || !fonts.every((f) => f !== null))
|
|
351
367
|
return err({
|
|
352
368
|
kind: "broken",
|
|
353
369
|
why: "`fonts` is not a list of { name, type, embedded, program }",
|
|
@@ -356,7 +372,7 @@ export function parseFactsText(text: string): Result<ReadFacts, FactsProblem> {
|
|
|
356
372
|
if (!geometry.ok) return err({ kind: "broken", why: geometry.error });
|
|
357
373
|
return ok({
|
|
358
374
|
...artifact.value,
|
|
359
|
-
fonts
|
|
375
|
+
fonts,
|
|
360
376
|
geometry: geometry.value,
|
|
361
377
|
});
|
|
362
378
|
}
|
|
@@ -25,11 +25,21 @@ import {
|
|
|
25
25
|
writeFileSync,
|
|
26
26
|
} from "node:fs";
|
|
27
27
|
import { tmpdir } from "node:os";
|
|
28
|
-
import {
|
|
29
|
-
import {
|
|
30
|
-
import {
|
|
28
|
+
import { join } from "node:path";
|
|
29
|
+
import { createChecker } from "../lib/check.ts";
|
|
30
|
+
import { isDeepStrictEqual } from "node:util";
|
|
31
|
+
import { z } from "zod";
|
|
32
|
+
import type { Merge, Settings } from "./hooks-settings.ts";
|
|
33
|
+
|
|
34
|
+
/** Settings with a PreToolUse entry — the shape the "wired twice" case edits. */
|
|
35
|
+
const PreToolUseSettings = z.looseObject({
|
|
36
|
+
hooks: z.looseObject({
|
|
37
|
+
PreToolUse: z.array(
|
|
38
|
+
z.looseObject({ hooks: z.array(z.looseObject({ command: z.string() })) }),
|
|
39
|
+
),
|
|
40
|
+
}),
|
|
41
|
+
});
|
|
31
42
|
|
|
32
|
-
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
33
43
|
const {
|
|
34
44
|
hookRun,
|
|
35
45
|
shippedWiring,
|
|
@@ -39,16 +49,17 @@ const {
|
|
|
39
49
|
readSettings,
|
|
40
50
|
MANAGED_BY,
|
|
41
51
|
SETTINGS_PATH,
|
|
42
|
-
} = await import(
|
|
52
|
+
} = await import("./hooks-settings.ts");
|
|
43
53
|
const { claudeCodeHookProtocol } = await import("vigiles/claude-code");
|
|
44
|
-
const merge = (e, c, m) =>
|
|
54
|
+
const merge: Merge = (e, c, m) =>
|
|
55
|
+
claudeCodeHookProtocol.mergeRegistrations(e, c, m);
|
|
45
56
|
|
|
46
57
|
const check = createChecker();
|
|
47
58
|
|
|
48
59
|
// ── the contract with vigiles, checked before anything relies on it ──────────────────────
|
|
49
60
|
check(
|
|
50
61
|
"🔴 vigiles/claude-code exports claudeCodeHookProtocol.mergeRegistrations — the merge init calls",
|
|
51
|
-
typeof claudeCodeHookProtocol
|
|
62
|
+
typeof claudeCodeHookProtocol.mergeRegistrations === "function",
|
|
52
63
|
);
|
|
53
64
|
|
|
54
65
|
// ── the one source: plugin/hooks/hooks.json ─────────────────────────────────────────────
|
|
@@ -82,7 +93,7 @@ check(
|
|
|
82
93
|
try {
|
|
83
94
|
shippedWiring(file);
|
|
84
95
|
} catch (e) {
|
|
85
|
-
thrown = e.message;
|
|
96
|
+
thrown = e instanceof Error ? e.message : String(e);
|
|
86
97
|
}
|
|
87
98
|
rmSync(file);
|
|
88
99
|
check(
|
|
@@ -107,9 +118,26 @@ try {
|
|
|
107
118
|
threw,
|
|
108
119
|
);
|
|
109
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
|
+
|
|
110
138
|
// ── which spelling runs which hook ─────────────────────────────────────────────────────
|
|
111
139
|
const ours = `node "\${CLAUDE_PROJECT_DIR}/${MANAGED_BY}" hook paper-edit-guard`;
|
|
112
|
-
const cases = [
|
|
140
|
+
const cases: [string, { name: string; ours: boolean } | null][] = [
|
|
113
141
|
[ours, { name: "paper-edit-guard", ours: true }],
|
|
114
142
|
[
|
|
115
143
|
`node "$CLAUDE_PROJECT_DIR/${MANAGED_BY}" hook paper-status-gates`,
|
|
@@ -140,7 +168,7 @@ try {
|
|
|
140
168
|
JSON.stringify(hookRun(cmd)) === JSON.stringify(want),
|
|
141
169
|
);
|
|
142
170
|
|
|
143
|
-
const project = (name, settings) => {
|
|
171
|
+
const project = (name: string, settings?: Settings | string) => {
|
|
144
172
|
const dir = join(work, name);
|
|
145
173
|
mkdirSync(join(dir, ".claude"), { recursive: true });
|
|
146
174
|
if (settings !== undefined)
|
|
@@ -152,14 +180,19 @@ try {
|
|
|
152
180
|
);
|
|
153
181
|
return dir;
|
|
154
182
|
};
|
|
155
|
-
const text = (dir) => readFileSync(join(dir, SETTINGS_PATH), "utf8");
|
|
183
|
+
const text = (dir: string) => readFileSync(join(dir, SETTINGS_PATH), "utf8");
|
|
184
|
+
const settingsIn = (dir: string): Settings => {
|
|
185
|
+
const r = readSettings(dir);
|
|
186
|
+
if (r.status === "unparsable") throw new Error(r.reason);
|
|
187
|
+
return r.settings;
|
|
188
|
+
};
|
|
156
189
|
|
|
157
190
|
// ── a fresh project ────────────────────────────────────────────────────────────────────
|
|
158
191
|
{
|
|
159
192
|
const dir = join(work, "fresh");
|
|
160
193
|
mkdirSync(dir);
|
|
161
194
|
const r = wireHooks(dir, merge, wiring);
|
|
162
|
-
const s =
|
|
195
|
+
const s = settingsIn(dir);
|
|
163
196
|
check(
|
|
164
197
|
"no settings file → it is created with the three hooks",
|
|
165
198
|
r.status === "written" &&
|
|
@@ -189,14 +222,14 @@ try {
|
|
|
189
222
|
},
|
|
190
223
|
});
|
|
191
224
|
wireHooks(dir, merge, wiring);
|
|
192
|
-
const s =
|
|
225
|
+
const s = settingsIn(dir);
|
|
193
226
|
check(
|
|
194
227
|
"🔴 the user's own hook in the SAME matcher survives",
|
|
195
228
|
JSON.stringify(s).includes("node my-own-lint.mjs"),
|
|
196
229
|
);
|
|
197
230
|
check(
|
|
198
231
|
"and every other key is kept (permissions)",
|
|
199
|
-
s
|
|
232
|
+
isDeepStrictEqual(s["permissions"], { allow: ["Bash(ls:*)"] }),
|
|
200
233
|
);
|
|
201
234
|
check(
|
|
202
235
|
"and ours are there, once each",
|
|
@@ -243,11 +276,14 @@ try {
|
|
|
243
276
|
);
|
|
244
277
|
check(
|
|
245
278
|
"the report names the hook and the command it found",
|
|
246
|
-
r.
|
|
279
|
+
r.status === "foreign" &&
|
|
280
|
+
r.found.length === 1 &&
|
|
281
|
+
r.found[0]?.name === "paper-edit-guard",
|
|
247
282
|
);
|
|
248
283
|
check(
|
|
249
284
|
"and names the hooks that are not wired in ANY form",
|
|
250
|
-
r.
|
|
285
|
+
r.status === "foreign" &&
|
|
286
|
+
r.missing.join() === "paper-skills-nudge,paper-status-gates",
|
|
251
287
|
);
|
|
252
288
|
// The control: without the check, vigiles' merge WOULD add a second copy. This is the
|
|
253
289
|
// reason the check exists, measured rather than asserted.
|
|
@@ -257,7 +293,7 @@ try {
|
|
|
257
293
|
);
|
|
258
294
|
check(
|
|
259
295
|
"(control) vigiles' merge alone would wire paper-edit-guard TWICE here",
|
|
260
|
-
c
|
|
296
|
+
c?.ours === 1 && c.other === 1,
|
|
261
297
|
);
|
|
262
298
|
}
|
|
263
299
|
|
|
@@ -276,7 +312,7 @@ try {
|
|
|
276
312
|
}
|
|
277
313
|
|
|
278
314
|
// ── doctor's section ───────────────────────────────────────────────────────────────────
|
|
279
|
-
const doc = (settings) => {
|
|
315
|
+
const doc = (settings?: Settings | string) => {
|
|
280
316
|
const dir = project(`doc-${String(check.count)}`, settings);
|
|
281
317
|
return doctorHooks(dir, wiring).join("\n");
|
|
282
318
|
};
|
|
@@ -291,8 +327,8 @@ try {
|
|
|
291
327
|
"nothing wired → ⚠ not wired, with the remedy",
|
|
292
328
|
/⚠ not wired — none of the 3 hooks/.test(doc({})),
|
|
293
329
|
);
|
|
294
|
-
const twice =
|
|
295
|
-
twice.hooks.PreToolUse[0]
|
|
330
|
+
const twice = PreToolUseSettings.parse(structuredClone(wiredOnce));
|
|
331
|
+
twice.hooks.PreToolUse[0]?.hooks.push({
|
|
296
332
|
type: "command",
|
|
297
333
|
command: "npx paperlint hook paper-edit-guard",
|
|
298
334
|
});
|
package/src/hooks-settings.ts
CHANGED
|
@@ -30,6 +30,8 @@ import {
|
|
|
30
30
|
BIN_FILE,
|
|
31
31
|
PACKAGE_NAME,
|
|
32
32
|
} from "../skills/paper-pipeline/scripts/consumer.mjs";
|
|
33
|
+
import { messageOf } from "./domain/text.ts";
|
|
34
|
+
import { fieldOf, isRecord } from "./domain/record.ts";
|
|
33
35
|
|
|
34
36
|
/** The committed, shared settings file — husky's `.husky/` analogue, except git carries it. */
|
|
35
37
|
export const SETTINGS_PATH = join(".claude", "settings.json");
|
|
@@ -133,17 +135,42 @@ export function hookRun(command: string): {
|
|
|
133
135
|
export function commandsIn(
|
|
134
136
|
settings: Settings,
|
|
135
137
|
): { readonly event: string; readonly command: string }[] {
|
|
136
|
-
const hooks = settings["hooks"];
|
|
137
|
-
if (!hooks || typeof hooks !== "object") return [];
|
|
138
138
|
const out: { event: string; command: string }[] = [];
|
|
139
|
-
for (const [event, entries] of Object.entries(hooks
|
|
140
|
-
for (const entry of
|
|
141
|
-
for (const h of
|
|
142
|
-
|
|
143
|
-
|
|
139
|
+
for (const [event, entries] of Object.entries(prop(settings["hooks"]) ?? {}))
|
|
140
|
+
for (const entry of listOf(entries))
|
|
141
|
+
for (const h of listOf(prop(entry)?.["hooks"])) {
|
|
142
|
+
const command = prop(h)?.["command"];
|
|
143
|
+
if (typeof command === "string") out.push({ event, command });
|
|
144
|
+
}
|
|
144
145
|
return out;
|
|
145
146
|
}
|
|
146
147
|
|
|
148
|
+
/** The settings file is the user's: any level may be missing or of the wrong kind. */
|
|
149
|
+
const listOf = (v: unknown): readonly unknown[] => (Array.isArray(v) ? v : []);
|
|
150
|
+
const prop = (v: unknown): Readonly<Record<string, unknown>> | undefined =>
|
|
151
|
+
typeof v === "object" && v !== null
|
|
152
|
+
? Object.fromEntries(Object.entries(v))
|
|
153
|
+
: undefined;
|
|
154
|
+
|
|
155
|
+
const isHookCommand = (v: unknown): v is HookCommand =>
|
|
156
|
+
typeof fieldOf(v, "type") === "string" &&
|
|
157
|
+
typeof fieldOf(v, "command") === "string";
|
|
158
|
+
|
|
159
|
+
function isHookEntry(v: unknown): v is HookEntry {
|
|
160
|
+
const matcher = fieldOf(v, "matcher");
|
|
161
|
+
const hooks = fieldOf(v, "hooks");
|
|
162
|
+
return (
|
|
163
|
+
["undefined", "string"].includes(typeof matcher) &&
|
|
164
|
+
Array.isArray(hooks) &&
|
|
165
|
+
hooks.every(isHookCommand)
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** A `hooks` map of the shape Claude Code reads: event → entries → commands. */
|
|
170
|
+
const isHooksMap = (v: unknown): v is HooksMap =>
|
|
171
|
+
isRecord(v) &&
|
|
172
|
+
Object.values(v).every((es) => Array.isArray(es) && es.every(isHookEntry));
|
|
173
|
+
|
|
147
174
|
export interface Wiring {
|
|
148
175
|
readonly compiled: HooksMap;
|
|
149
176
|
/** Derived from the commands, in order. Never empty — an empty set throws. */
|
|
@@ -155,8 +182,12 @@ export interface Wiring {
|
|
|
155
182
|
* check that silently wired nothing would report "✓ wired" over an empty set.
|
|
156
183
|
*/
|
|
157
184
|
export function shippedWiring(file: string = WIRING_FILE): Wiring {
|
|
158
|
-
const json = JSON.parse(readFileSync(file, "utf8"))
|
|
159
|
-
const compiled = json
|
|
185
|
+
const json: unknown = JSON.parse(readFileSync(file, "utf8"));
|
|
186
|
+
const compiled = fieldOf(json, "hooks") ?? {};
|
|
187
|
+
if (!isHooksMap(compiled))
|
|
188
|
+
throw new Error(
|
|
189
|
+
`${file}: "hooks" is not a map of event → [{ matcher?, hooks: [{ type, command }] }]`,
|
|
190
|
+
);
|
|
160
191
|
const names = commandsIn({ hooks: compiled })
|
|
161
192
|
.map((c) => hookRun(c.command)?.name)
|
|
162
193
|
.filter((n): n is string => typeof n === "string");
|
|
@@ -195,7 +226,11 @@ export function wiredCounts(
|
|
|
195
226
|
const total = (c: HookCount): number => c.ours + c.other;
|
|
196
227
|
|
|
197
228
|
export type SettingsRead =
|
|
198
|
-
| {
|
|
229
|
+
| {
|
|
230
|
+
readonly status: "absent";
|
|
231
|
+
readonly path: string;
|
|
232
|
+
readonly settings: Settings;
|
|
233
|
+
}
|
|
199
234
|
| {
|
|
200
235
|
readonly status: "read";
|
|
201
236
|
readonly path: string;
|
|
@@ -214,11 +249,11 @@ export function readSettings(root: string): SettingsRead {
|
|
|
214
249
|
const raw = readFileSync(path, "utf8");
|
|
215
250
|
try {
|
|
216
251
|
const parsed: unknown = JSON.parse(raw);
|
|
217
|
-
if (!
|
|
252
|
+
if (!isRecord(parsed))
|
|
218
253
|
return { status: "unparsable", path, reason: "not a JSON object" };
|
|
219
|
-
return { status: "read", path, settings: parsed
|
|
254
|
+
return { status: "read", path, settings: parsed, raw };
|
|
220
255
|
} catch (e) {
|
|
221
|
-
return { status: "unparsable", path, reason: (e
|
|
256
|
+
return { status: "unparsable", path, reason: messageOf(e) };
|
|
222
257
|
}
|
|
223
258
|
}
|
|
224
259
|
|