paperlint 3.1.2 → 3.2.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 +3 -3
- package/CONTRIBUTING.md +34 -5
- package/dist/adapters/banal/index.d.ts.map +1 -1
- package/dist/adapters/banal/index.js +9 -5
- package/dist/adapters/banal/index.js.map +1 -1
- package/dist/adapters/banal/invocation.d.ts +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 +0 -5
- 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/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 +4 -5
- 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.js +2 -2
- package/dist/build-engine.js.map +1 -1
- package/dist/build.d.ts.map +1 -1
- package/dist/build.js +14 -11
- package/dist/build.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +31 -34
- package/dist/cli.js.map +1 -1
- package/dist/doctor.d.ts.map +1 -1
- package/dist/doctor.js +3 -2
- 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/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.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 +2 -1
- 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 +14 -9
- package/dist/hooks-settings.js.map +1 -1
- package/dist/init.d.ts +5 -5
- package/dist/init.d.ts.map +1 -1
- package/dist/init.js +15 -7
- 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 +142 -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 +2 -2
- package/dist/new-paper.js.map +1 -1
- package/dist/paper-settings.d.ts.map +1 -1
- package/dist/paper-settings.js +5 -4
- package/dist/paper-settings.js.map +1 -1
- package/dist/pdf-facts.d.ts.map +1 -1
- package/dist/pdf-facts.js +2 -1
- package/dist/pdf-facts.js.map +1 -1
- package/dist/pdf-geometry.d.ts.map +1 -1
- package/dist/pdf-geometry.js +1 -1
- package/dist/pdf-geometry.js.map +1 -1
- package/dist/presets.d.ts.map +1 -1
- package/dist/presets.js +3 -2
- package/dist/presets.js.map +1 -1
- package/dist/references.d.ts.map +1 -1
- package/dist/references.js +11 -6
- package/dist/references.js.map +1 -1
- package/dist/rules-config.d.ts.map +1 -1
- package/dist/rules-config.js +6 -3
- 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.map +1 -1
- package/dist/tex-requirements.js +8 -3
- 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/venue-rules.js +1 -1
- package/dist/venue-rules.js.map +1 -1
- 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} +4 -3
- package/eslint-rules/install-path-literals.harness.mjs +1 -1
- package/eslint-rules/{install-path-literals.mjs → install-path-literals.ts} +16 -8
- 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} +34 -15
- 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} +74 -29
- package/eslint-rules/paper-typography.test.mjs +3 -3
- package/eslint-rules/{paper-typography.mjs → paper-typography.ts} +132 -54
- 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} +88 -31
- package/eslint-rules/review-frontmatter.test.mjs +2 -2
- package/eslint-rules/{review-frontmatter.mjs → review-frontmatter.ts} +37 -15
- 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} +19 -6
- package/eslint-rules/tex-build.harness.mjs +2 -2
- package/eslint-rules/{tex-build.mjs → tex-build.ts} +4 -3
- package/eslint.config.mjs +139 -30
- 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.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 +8 -8
- package/lib/{paper-config.mjs → paper-config.ts} +19 -14
- package/lib/skill-checks.mjs +1 -1
- package/lib/skill-checks.test.mjs +1 -1
- package/lib/skill-corpus.mjs +1 -1
- package/lib/skill-eval-kit.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 -37
- package/scripts/eslint-report-guard.test.mjs +1 -1
- package/scripts/harness-api.frozen.json +14 -15
- 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/grade-paper-writing/prose-lint.mjs +1 -1
- package/skills/grade-paper-writing/{prose-lint.test.mjs → prose-lint.test.ts} +2 -2
- 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 +1 -1
- package/skills/paper-pipeline/pipeline-firing.eval.mjs +1 -1
- package/skills/paper-pipeline/pipeline-language.eval.mjs +1 -1
- package/skills/paper-pipeline/scripts/README.md +1 -1
- package/skills/paper-pipeline/scripts/announce.test.mjs +1 -1
- 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.mjs +1 -5
- 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/fixtures/{scorecard.mjs → scorecard.ts} +36 -7
- package/skills/paper-pipeline/scripts/{generated-code.test.mjs → generated-code.test.ts} +3 -3
- package/skills/paper-pipeline/scripts/ledger.cli.test.mjs +1 -1
- package/skills/paper-pipeline/scripts/pipeline-check.cli.test.mjs +2 -2
- package/skills/paper-pipeline/scripts/pipeline-check.harness.mjs +2 -2
- package/skills/paper-pipeline/scripts/pipeline-check.mjs +1 -4
- 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-pipeline/scripts/run-mechanical.test.mjs +1 -1
- package/skills/paper-pipeline/scripts/status.test.mjs +1 -1
- 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/{extract-pdf-facts.harness.mjs → extract-pdf-facts.harness.ts} +34 -9
- package/skills/render-paper/render-paper.harness.mjs +2 -2
- package/skills/tighten-paper/structure.mjs +1 -1
- package/skills/tighten-paper/{structure.test.mjs → structure.test.ts} +4 -3
- 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/skills/verify-citations/scripts/verify-cites.mjs +11 -1
- package/skills/verify-citations/scripts/verify-cites.net.test.mjs +35 -0
- package/skills/verify-citations/scripts/verify-cites.test.mjs +14 -0
- package/src/CLAUDE.md +1 -1
- package/src/adapters/banal/index.test.ts +3 -6
- package/src/adapters/banal/index.ts +7 -4
- package/src/adapters/banal/invocation.test.ts +3 -1
- package/src/adapters/banal/invocation.ts +1 -1
- package/src/adapters/banal/output.test.ts +1 -1
- package/src/adapters/banal/run.test.ts +3 -3
- package/src/adapters/banal/xml.test.ts +1 -1
- package/src/adapters/curl/download.test.ts +4 -2
- package/src/adapters/node/files.io.ts +2 -2
- package/src/adapters/node/files.test.ts +3 -1
- package/src/adapters/node/process.io.ts +4 -3
- package/src/adapters/node/process.test.ts +1 -1
- 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 +26 -58
- 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 +2 -2
- package/src/build-engine.ts +2 -2
- package/src/build.harness.mjs +2 -2
- package/src/build.test.ts +15 -14
- package/src/build.ts +28 -19
- package/src/cli.harness.mjs +6 -6
- package/src/cli.test.ts +13 -6
- package/src/cli.ts +39 -37
- package/src/doctor.harness.mjs +3 -3
- package/src/doctor.ts +3 -2
- package/src/domain/lookup-cache.ts +21 -11
- package/src/domain/result.test.ts +2 -2
- 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 +4 -3
- package/src/facts-file.test.ts +9 -7
- package/src/facts-file.ts +2 -1
- package/src/{hooks-settings.harness.mjs → hooks-settings.harness.ts} +39 -20
- package/src/hooks-settings.ts +20 -9
- package/src/init.test.ts +12 -9
- package/src/init.ts +22 -12
- 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 +2 -2
- package/src/new-paper.test.ts +8 -10
- package/src/new-paper.ts +2 -2
- package/src/paper-files.test.ts +3 -5
- package/src/paper-settings-commands.test.ts +5 -4
- package/src/paper-settings.test.ts +5 -5
- package/src/paper-settings.ts +8 -9
- package/src/{pdf-facts.harness.mjs → pdf-facts.harness.ts} +9 -8
- package/src/pdf-facts.test.ts +10 -11
- package/src/pdf-facts.ts +2 -1
- package/src/{pdf-geometry.harness.mjs → pdf-geometry.harness.ts} +19 -12
- package/src/pdf-geometry.ts +3 -1
- package/src/presets.ts +3 -2
- package/src/references.test.ts +51 -43
- package/src/references.ts +14 -7
- package/src/{rules-config.harness.mjs → rules-config.harness.ts} +7 -13
- package/src/rules-config.ts +7 -4
- 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.ts +8 -5
- package/src/toolchain.harness.mjs +2 -2
- package/src/toolchain.test.ts +1 -1
- package/src/toolchain.ts +31 -17
- package/src/venue-rules.test.ts +62 -64
- package/src/venue-rules.ts +1 -1
- 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/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/osf-artifact-upload/{osf-artifact-upload.harness.mjs → osf-artifact-upload.harness.ts} +0 -0
- /package/skills/tighten-paper/__snapshots__/{structure.test.mjs.snap → structure.test.ts.snap} +0 -0
|
@@ -23,24 +23,26 @@ import { spawnSync } from "node:child_process";
|
|
|
23
23
|
import { globSync, readFileSync } from "node:fs";
|
|
24
24
|
import { dirname, join, matchesGlob, resolve } from "node:path";
|
|
25
25
|
import { fileURLToPath } from "node:url";
|
|
26
|
-
// @ts-expect-error — js-yaml ships no types, and the package does not depend on @types/js-yaml
|
|
27
26
|
import yaml from "js-yaml";
|
|
28
27
|
import ts from "typescript";
|
|
29
28
|
import { test } from "vitest";
|
|
29
|
+
import { z } from "zod";
|
|
30
30
|
|
|
31
31
|
const ROOT = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
32
|
-
const CONFIG =
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
32
|
+
const CONFIG = z
|
|
33
|
+
.object({
|
|
34
|
+
lines: z.number(),
|
|
35
|
+
statements: z.number(),
|
|
36
|
+
functions: z.number(),
|
|
37
|
+
branches: z.number(),
|
|
38
|
+
excludeAfterRemap: z.boolean(),
|
|
39
|
+
include: z.array(z.string()),
|
|
40
|
+
exclude: z.array(z.string()),
|
|
41
|
+
extension: z.array(z.string()),
|
|
42
|
+
typeOnly: z.array(z.string()),
|
|
43
|
+
evalOnly: z.array(z.string()),
|
|
44
|
+
})
|
|
45
|
+
.parse(JSON.parse(readFileSync(join(ROOT, ".c8rc.json"), "utf8")));
|
|
44
46
|
|
|
45
47
|
const expand = (patterns: readonly string[]): string[] =>
|
|
46
48
|
patterns.flatMap((p) => globSync(p, { cwd: ROOT })).sort();
|
|
@@ -53,13 +55,45 @@ export function runtimeStatements(fileName: string, source: string): string[] {
|
|
|
53
55
|
(s) =>
|
|
54
56
|
!ts.isInterfaceDeclaration(s) &&
|
|
55
57
|
!ts.isTypeAliasDeclaration(s) &&
|
|
56
|
-
!(
|
|
58
|
+
!(
|
|
59
|
+
ts.isImportDeclaration(s) &&
|
|
60
|
+
s.importClause?.phaseModifier === ts.SyntaxKind.TypeKeyword
|
|
61
|
+
) &&
|
|
57
62
|
!(ts.isExportDeclaration(s) && s.isTypeOnly),
|
|
58
63
|
)
|
|
59
64
|
.map((s) => ts.SyntaxKind[s.kind]);
|
|
60
65
|
}
|
|
61
66
|
|
|
62
67
|
/** Every relative module specifier a source imports, statically or with `import("…")`. */
|
|
68
|
+
/**
|
|
69
|
+
* The package's subpath imports (`#lib/*`, `#eslint-rules/*`), each as its prefix and the source
|
|
70
|
+
* file pattern its `paperlint-source` condition names — so an import written `#lib/x` is followed
|
|
71
|
+
* to `lib/x.ts` like a relative one, instead of being skipped as a bare package name.
|
|
72
|
+
*/
|
|
73
|
+
const SUBPATHS = Object.entries(
|
|
74
|
+
z
|
|
75
|
+
.object({
|
|
76
|
+
imports: z.record(
|
|
77
|
+
z.string().endsWith("/*"),
|
|
78
|
+
z.object({ "paperlint-source": z.string() }),
|
|
79
|
+
),
|
|
80
|
+
})
|
|
81
|
+
.parse(JSON.parse(readFileSync(join(ROOT, "package.json"), "utf8")))
|
|
82
|
+
.imports,
|
|
83
|
+
).map(([key, t]) => ({
|
|
84
|
+
prefix: key.slice(0, -"*".length),
|
|
85
|
+
target: t["paperlint-source"],
|
|
86
|
+
}));
|
|
87
|
+
|
|
88
|
+
/** Where a specifier points inside the repository: a relative path, or a subpath import's source. */
|
|
89
|
+
function resolvedSpecifier(fileName: string, spec: string): string[] {
|
|
90
|
+
if (spec.startsWith(".")) return [resolve(dirname(fileName), spec)];
|
|
91
|
+
return SUBPATHS.filter(({ prefix }) => spec.startsWith(prefix)).map(
|
|
92
|
+
({ prefix, target }) =>
|
|
93
|
+
resolve(ROOT, target.replace("*", spec.slice(prefix.length))),
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
63
97
|
export function importedPaths(fileName: string, source: string): string[] {
|
|
64
98
|
const sf = ts.createSourceFile(fileName, source, ts.ScriptTarget.Latest);
|
|
65
99
|
const found: string[] = [];
|
|
@@ -70,8 +104,8 @@ export function importedPaths(fileName: string, source: string): string[] {
|
|
|
70
104
|
node.expression.kind === ts.SyntaxKind.ImportKeyword
|
|
71
105
|
? node.arguments[0]
|
|
72
106
|
: undefined;
|
|
73
|
-
if (spec && ts.isStringLiteral(spec)
|
|
74
|
-
found.push(
|
|
107
|
+
if (spec && ts.isStringLiteral(spec))
|
|
108
|
+
found.push(...resolvedSpecifier(fileName, spec.text));
|
|
75
109
|
ts.forEachChild(node, visit);
|
|
76
110
|
};
|
|
77
111
|
visit(sf);
|
|
@@ -179,7 +213,10 @@ export function comments(fileName: string, source: string): string[] {
|
|
|
179
213
|
const visit = (node: ts.Node): void => {
|
|
180
214
|
if (ts.isJSDoc(node)) return; // its text is the comment already collected before its node
|
|
181
215
|
const kids = node.getChildren(sf);
|
|
182
|
-
if (kids.length > 0)
|
|
216
|
+
if (kids.length > 0) {
|
|
217
|
+
kids.forEach(visit);
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
183
220
|
for (const r of [
|
|
184
221
|
...(ts.getLeadingCommentRanges(source, node.pos) ?? []),
|
|
185
222
|
...(ts.getTrailingCommentRanges(source, node.end) ?? []),
|
|
@@ -234,32 +271,36 @@ test("the floor is 100 on all four measures, and the script that checks it canno
|
|
|
234
271
|
[CONFIG.lines, CONFIG.statements, CONFIG.functions, CONFIG.branches],
|
|
235
272
|
[100, 100, 100, 100],
|
|
236
273
|
);
|
|
237
|
-
const pkg =
|
|
238
|
-
scripts:
|
|
239
|
-
|
|
274
|
+
const pkg = z
|
|
275
|
+
.object({ scripts: z.record(z.string(), z.string()) })
|
|
276
|
+
.parse(JSON.parse(readFileSync(join(ROOT, "package.json"), "utf8")));
|
|
240
277
|
// Pinned whole: c8's command-line flags override .c8rc.json, so `--lines 50` or `--exclude "**"`
|
|
241
278
|
// added here would bypass every threshold and the pinned exclude list below.
|
|
242
279
|
assert.equal(
|
|
243
280
|
pkg.scripts["coverage"],
|
|
244
|
-
'NODE_OPTIONS="--import=./test/coverage-src.
|
|
281
|
+
'NODE_OPTIONS="--import=./test/coverage-src.ts $NODE_OPTIONS" c8 --check-coverage npm test --',
|
|
245
282
|
);
|
|
246
283
|
});
|
|
247
284
|
|
|
248
285
|
/** The gates job and the one step of it that runs coverage, read from the parsed workflow. */
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
}
|
|
286
|
+
const StepSchema = z.looseObject({
|
|
287
|
+
run: z.string().optional(),
|
|
288
|
+
if: z.unknown().optional(),
|
|
289
|
+
"continue-on-error": z.unknown().optional(),
|
|
290
|
+
});
|
|
291
|
+
const JobSchema = z.looseObject({
|
|
292
|
+
if: z.unknown().optional(),
|
|
293
|
+
"continue-on-error": z.unknown().optional(),
|
|
294
|
+
steps: z.array(StepSchema).optional(),
|
|
295
|
+
});
|
|
296
|
+
type Step = z.infer<typeof StepSchema>;
|
|
297
|
+
type Job = z.infer<typeof JobSchema>;
|
|
259
298
|
export function coverageSteps(
|
|
260
299
|
workflow: string,
|
|
261
300
|
): { job: string; definition: Job; step: Step }[] {
|
|
262
|
-
const ci =
|
|
301
|
+
const ci = z
|
|
302
|
+
.looseObject({ jobs: z.record(z.string(), JobSchema) })
|
|
303
|
+
.parse(yaml.load(workflow));
|
|
263
304
|
return Object.entries(ci.jobs).flatMap(([job, definition]) =>
|
|
264
305
|
(definition.steps ?? [])
|
|
265
306
|
.filter((step) => (step.run ?? "").trim().startsWith("npm run coverage"))
|
|
@@ -273,11 +314,12 @@ const DRAFT_GATE =
|
|
|
273
314
|
/** Why CI's coverage step would not fail the run on a coverage drop — empty when it would. */
|
|
274
315
|
export function coverageStepHoles(workflow: string): string[] {
|
|
275
316
|
const found = coverageSteps(workflow);
|
|
276
|
-
|
|
317
|
+
const [only, ...more] = found;
|
|
318
|
+
if (only === undefined || more.length > 0)
|
|
277
319
|
return [
|
|
278
320
|
`expected one step running \`npm run coverage\`, found ${String(found.length)}`,
|
|
279
321
|
];
|
|
280
|
-
const
|
|
322
|
+
const { job, definition, step } = only;
|
|
281
323
|
return [
|
|
282
324
|
...(job === "gates" ? [] : [`the step is in job ${job}, not gates`]),
|
|
283
325
|
...(definition.if === DRAFT_GATE
|
|
@@ -345,7 +387,7 @@ test("dist/ is excluded BEFORE source maps: a direct load is not a second copy o
|
|
|
345
387
|
test("the exclude list is exactly the justified set", () => {
|
|
346
388
|
assert.deepEqual(CONFIG.exclude, [
|
|
347
389
|
// tests themselves
|
|
348
|
-
"**/*.harness
|
|
390
|
+
"**/*.harness.*",
|
|
349
391
|
"**/*.test.*",
|
|
350
392
|
// PAID LLM evaluations, never run by `npm test`
|
|
351
393
|
"**/*.eval.mjs",
|
|
@@ -356,7 +398,7 @@ test("the exclude list is exactly the justified set", () => {
|
|
|
356
398
|
"**/*.d.mts",
|
|
357
399
|
// inputs the tests read
|
|
358
400
|
"**/fixtures/**",
|
|
359
|
-
// build output (the coverage run resolves it to src/ — test/coverage-src.
|
|
401
|
+
// build output (the coverage run resolves it to src/ — test/coverage-src.ts)
|
|
360
402
|
"dist/**",
|
|
361
403
|
// test support
|
|
362
404
|
"test/**",
|
|
@@ -389,10 +431,11 @@ test("the exclude list is exactly the justified set", () => {
|
|
|
389
431
|
"src/ports/*.ts",
|
|
390
432
|
"src/domain/page-layout.ts",
|
|
391
433
|
"src/domain/paths.ts",
|
|
434
|
+
"eslint-rules/rule-context.ts",
|
|
392
435
|
]);
|
|
393
436
|
assert.deepEqual(CONFIG.evalOnly, [
|
|
394
437
|
"lib/skill-eval-kit.mjs",
|
|
395
|
-
"lib/skill-eval-fixture.
|
|
438
|
+
"lib/skill-eval-fixture.ts",
|
|
396
439
|
"lib/trigger-ledger.mjs",
|
|
397
440
|
]);
|
|
398
441
|
});
|
|
@@ -7,7 +7,7 @@ import assert from "node:assert/strict";
|
|
|
7
7
|
import { writeFileSync } from "node:fs";
|
|
8
8
|
import { join } from "node:path";
|
|
9
9
|
import { test } from "vitest";
|
|
10
|
-
import { useTempDir } from "../test/support.
|
|
10
|
+
import { useTempDir } from "../test/support.ts";
|
|
11
11
|
import { guard } from "./eslint-report-guard.mjs";
|
|
12
12
|
|
|
13
13
|
test("warnings and rule-less messages are listed; a file with no messages counts as linted", () => {
|
|
@@ -11,12 +11,11 @@
|
|
|
11
11
|
"eslint-rules/pdf-last-page-balance.harness.mjs",
|
|
12
12
|
"eslint-rules/temp-root-realpath.harness.mjs",
|
|
13
13
|
"eslint-rules/tex-build.harness.mjs",
|
|
14
|
-
"fixtures/real-markdown-paper/real-markdown-paper.harness.
|
|
14
|
+
"fixtures/real-markdown-paper/real-markdown-paper.harness.ts",
|
|
15
15
|
"lib/agent-cli-version.harness.mjs",
|
|
16
16
|
"lib/paper-config.harness.mjs",
|
|
17
17
|
"lib/skill-trigger-cases.harness.mjs",
|
|
18
|
-
"scripts/check.harness.
|
|
19
|
-
"scripts/rules-are-content-only.harness.mjs",
|
|
18
|
+
"scripts/check.harness.ts",
|
|
20
19
|
"scripts/rules-see-files.harness.mjs",
|
|
21
20
|
"skills/analyze-sibling-paper/analyze-sibling-paper.harness.mjs",
|
|
22
21
|
"skills/argument-arc/argument-arc.harness.mjs",
|
|
@@ -29,7 +28,7 @@
|
|
|
29
28
|
"skills/grade-paper-writing/grade-paper-writing.harness.mjs",
|
|
30
29
|
"skills/harden-paper/harden-paper.harness.mjs",
|
|
31
30
|
"skills/map-prior-work/map-prior-work.harness.mjs",
|
|
32
|
-
"skills/osf-artifact-upload/osf-artifact-upload.harness.
|
|
31
|
+
"skills/osf-artifact-upload/osf-artifact-upload.harness.ts",
|
|
33
32
|
"skills/paper-adversarial-review/paper-adversarial-review.harness.mjs",
|
|
34
33
|
"skills/paper-pipeline/paper-pipeline.harness.mjs",
|
|
35
34
|
"skills/paper-pipeline/scripts/artifact-coverage.harness.mjs",
|
|
@@ -39,11 +38,11 @@
|
|
|
39
38
|
"skills/paper-pipeline/scripts/pipeline-check.harness.mjs",
|
|
40
39
|
"skills/paper-pipeline/scripts/population-map.harness.mjs",
|
|
41
40
|
"skills/paper-pipeline/scripts/provenance.harness.mjs",
|
|
42
|
-
"skills/paper-pipeline/scripts/round-diff.harness.
|
|
41
|
+
"skills/paper-pipeline/scripts/round-diff.harness.ts",
|
|
43
42
|
"skills/paper-status/paper-status.harness.mjs",
|
|
44
43
|
"skills/pc-panel-review/pc-panel-review.harness.mjs",
|
|
45
44
|
"skills/plan-paper-timeline/plan-paper-timeline.harness.mjs",
|
|
46
|
-
"skills/render-paper/extract-pdf-facts.harness.
|
|
45
|
+
"skills/render-paper/extract-pdf-facts.harness.ts",
|
|
47
46
|
"skills/render-paper/render-paper.harness.mjs",
|
|
48
47
|
"skills/research-ideate/research-ideate.harness.mjs",
|
|
49
48
|
"skills/study-accepted-papers/study-accepted-papers.harness.mjs",
|
|
@@ -56,16 +55,16 @@
|
|
|
56
55
|
"src/cli.harness.mjs",
|
|
57
56
|
"src/doctor.harness.mjs",
|
|
58
57
|
"src/engine.harness.mjs",
|
|
59
|
-
"src/hooks-settings.harness.
|
|
60
|
-
"src/latex-log.harness.
|
|
61
|
-
"src/latex-loop.harness.
|
|
62
|
-
"src/link-skills.harness.
|
|
58
|
+
"src/hooks-settings.harness.ts",
|
|
59
|
+
"src/latex-log.harness.ts",
|
|
60
|
+
"src/latex-loop.harness.ts",
|
|
61
|
+
"src/link-skills.harness.ts",
|
|
63
62
|
"src/new-paper.harness.mjs",
|
|
64
|
-
"src/pdf-facts.harness.
|
|
65
|
-
"src/pdf-geometry.harness.
|
|
66
|
-
"src/rules-config.harness.
|
|
67
|
-
"src/structure.harness.
|
|
68
|
-
"src/tex-requirements.harness.
|
|
63
|
+
"src/pdf-facts.harness.ts",
|
|
64
|
+
"src/pdf-geometry.harness.ts",
|
|
65
|
+
"src/rules-config.harness.ts",
|
|
66
|
+
"src/structure.harness.ts",
|
|
67
|
+
"src/tex-requirements.harness.ts",
|
|
69
68
|
"src/toolchain.harness.mjs"
|
|
70
69
|
]
|
|
71
70
|
}
|
|
@@ -18,13 +18,16 @@ import { basename, dirname, join } from "node:path";
|
|
|
18
18
|
import { test } from "vitest";
|
|
19
19
|
import { fileURLToPath } from "node:url";
|
|
20
20
|
import ts from "typescript";
|
|
21
|
+
import { z } from "zod";
|
|
21
22
|
|
|
22
23
|
const ROOT = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
23
|
-
const FROZEN: readonly string[] =
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
)
|
|
24
|
+
const FROZEN: readonly string[] = z
|
|
25
|
+
.object({ harnesses: z.array(z.string()) })
|
|
26
|
+
.parse(
|
|
27
|
+
JSON.parse(
|
|
28
|
+
readFileSync(join(ROOT, "scripts", "harness-api.frozen.json"), "utf8"),
|
|
29
|
+
),
|
|
30
|
+
).harnesses;
|
|
28
31
|
|
|
29
32
|
/** vigiles' agent-testing API — what a harness exists to call. */
|
|
30
33
|
export const AGENT_API = new Set(["runHook", "runHarnessTest", "runEval"]);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The legacy-exemption ratchet (`layer-legacy-frozen.
|
|
2
|
+
* The legacy-exemption ratchet (`layer-legacy-frozen.ts`): each of its refusals fires on a planted
|
|
3
3
|
* case and stays silent on the matching clean one. The inputs are ESLint's own result shape, so the
|
|
4
4
|
* tally is tested on what the linter reports, not on source text.
|
|
5
5
|
*/
|
|
@@ -14,8 +14,8 @@ import {
|
|
|
14
14
|
main,
|
|
15
15
|
tally,
|
|
16
16
|
type Suppressed,
|
|
17
|
-
} from "./layer-legacy-frozen.
|
|
18
|
-
import { useTempDir, writeTree } from "../test/support.
|
|
17
|
+
} from "./layer-legacy-frozen.ts";
|
|
18
|
+
import { useTempDir, writeTree } from "../test/support.ts";
|
|
19
19
|
|
|
20
20
|
const IO = "legacy I/O, moves behind a port in #76";
|
|
21
21
|
const LAYER = "legacy layer, moves behind a port in #76";
|
|
@@ -131,7 +131,7 @@ test("main: problems are listed and fail; a clean count passes", async () => {
|
|
|
131
131
|
}) => {
|
|
132
132
|
const out: string[] = [];
|
|
133
133
|
const code = await main({
|
|
134
|
-
check:
|
|
134
|
+
check: () => Promise.resolve({ ...r, frozen: {} }),
|
|
135
135
|
log: (s: string) => out.push(s),
|
|
136
136
|
err: (s: string) => out.push(`E ${s}`),
|
|
137
137
|
});
|
|
@@ -191,11 +191,14 @@ test("checkFrozen: a lint that returns no result reports that nothing was counte
|
|
|
191
191
|
writeTree(root, {
|
|
192
192
|
[FROZEN_FILE]: JSON.stringify({ files: { "src/a.ts": { x: 1 } } }),
|
|
193
193
|
});
|
|
194
|
-
assert.deepEqual(
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
194
|
+
assert.deepEqual(
|
|
195
|
+
await checkFrozen(root, { lint: () => Promise.resolve([]) }),
|
|
196
|
+
{
|
|
197
|
+
problems: ["src/**/*.ts matched no file — nothing was counted."],
|
|
198
|
+
frozen: { "src/a.ts": { x: 1 } },
|
|
199
|
+
counts: {},
|
|
200
|
+
},
|
|
201
|
+
);
|
|
199
202
|
});
|
|
200
203
|
|
|
201
204
|
test("tally: optional parts of ESLint's result shape are read as absent", () => {
|
|
@@ -221,9 +224,10 @@ test("checkFrozen: a frozen file with no `files` freezes nothing", async () => {
|
|
|
221
224
|
const root = useTempDir("layer-frozen-nofiles-");
|
|
222
225
|
writeTree(root, { [FROZEN_FILE]: "{}\n" });
|
|
223
226
|
const r = await checkFrozen(root, {
|
|
224
|
-
lint:
|
|
225
|
-
|
|
226
|
-
|
|
227
|
+
lint: () =>
|
|
228
|
+
Promise.resolve([
|
|
229
|
+
{ filePath: `${root}/src/a.ts`, suppressedMessages: [] },
|
|
230
|
+
]),
|
|
227
231
|
});
|
|
228
232
|
assert.deepEqual(r, { problems: [], frozen: {}, counts: {} });
|
|
229
233
|
});
|
|
@@ -245,7 +249,7 @@ test("run as a program: the gate's verdict is the exit code", () => {
|
|
|
245
249
|
[
|
|
246
250
|
"--import",
|
|
247
251
|
preload,
|
|
248
|
-
fileURLToPath(new URL("./layer-legacy-frozen.
|
|
252
|
+
fileURLToPath(new URL("./layer-legacy-frozen.ts", import.meta.url)),
|
|
249
253
|
],
|
|
250
254
|
{ encoding: "utf8" },
|
|
251
255
|
);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* layer-legacy-frozen.
|
|
3
|
+
* layer-legacy-frozen.ts — the legacy exemptions from `src/`'s layer rules may only SHRINK.
|
|
4
4
|
*
|
|
5
5
|
* `src/` is hexagonal on two axes (src/CLAUDE.md): who may know whom, and where effects may be
|
|
6
6
|
* written. The flat `src/*.ts` files predate both. Each site in them that still breaks a rule
|
|
@@ -32,22 +32,55 @@
|
|
|
32
32
|
* counted by construction — and a range directive (`/* eslint-disable … *\/` around a multi-line
|
|
33
33
|
* import) counts exactly what it silences.
|
|
34
34
|
*
|
|
35
|
-
* Run: `node scripts/layer-legacy-frozen.
|
|
35
|
+
* Run: `node scripts/layer-legacy-frozen.ts` (also part of `npm run check`)
|
|
36
36
|
*/
|
|
37
37
|
import { isMain } from "../skills/paper-pipeline/scripts/consumer.mjs";
|
|
38
38
|
import { readFileSync } from "node:fs";
|
|
39
39
|
import { dirname, join, relative, sep } from "node:path";
|
|
40
40
|
import { fileURLToPath } from "node:url";
|
|
41
41
|
import { ESLint } from "eslint";
|
|
42
|
+
import { z } from "zod";
|
|
43
|
+
|
|
44
|
+
/** One file's result, in the shape ESLint's `LintResult` reports suppressions. */
|
|
45
|
+
export interface Suppressed {
|
|
46
|
+
readonly filePath: string;
|
|
47
|
+
readonly suppressedMessages?: readonly {
|
|
48
|
+
readonly ruleId: string | null;
|
|
49
|
+
readonly line: number;
|
|
50
|
+
readonly suppressions?: readonly { readonly justification?: string }[];
|
|
51
|
+
}[];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Per file, per rule: a count of suppressed findings. */
|
|
55
|
+
export type Counts = Record<string, Record<string, number>>;
|
|
56
|
+
|
|
57
|
+
/** What `checkFrozen` reports: the problems, and both sides of the comparison. */
|
|
58
|
+
export interface FrozenCheck {
|
|
59
|
+
problems: string[];
|
|
60
|
+
frozen: Counts;
|
|
61
|
+
counts: Counts;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** The frozen list on disk. A file with no `files` key freezes nothing. */
|
|
65
|
+
const FrozenFile = z.object({
|
|
66
|
+
files: z.record(z.string(), z.record(z.string(), z.number())).optional(),
|
|
67
|
+
});
|
|
42
68
|
|
|
43
69
|
const ROOT = join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
44
70
|
export const FROZEN_FILE = join("scripts", "layer-legacy.frozen.json");
|
|
45
71
|
|
|
46
72
|
/** The rules that enforce the layers. A suppression of any other rule is not this check's business. */
|
|
47
|
-
export const LAYER_RULES = [
|
|
73
|
+
export const LAYER_RULES: readonly string[] = [
|
|
74
|
+
"boundaries/dependencies",
|
|
75
|
+
"no-restricted-globals",
|
|
76
|
+
];
|
|
48
77
|
|
|
49
78
|
/** Where a legacy suppression may never appear: the folders written under the rules. */
|
|
50
|
-
export const NO_LEGACY_UNDER
|
|
79
|
+
export const NO_LEGACY_UNDER: readonly string[] = [
|
|
80
|
+
"src/domain/",
|
|
81
|
+
"src/ports/",
|
|
82
|
+
"src/adapters/",
|
|
83
|
+
];
|
|
51
84
|
|
|
52
85
|
/** The justification every legacy directive carries — `I/O` or `layer`, then the issue. */
|
|
53
86
|
export const LEGACY = /^legacy (I\/O|layer), moves behind a port in #76$/;
|
|
@@ -57,12 +90,16 @@ export const LEGACY = /^legacy (I\/O|layer), moves behind a port in #76$/;
|
|
|
57
90
|
* results (`filePath` relative to the repository, POSIX separators) and returns the tally plus
|
|
58
91
|
* every layer suppression that is not a legacy one.
|
|
59
92
|
*/
|
|
60
|
-
export function tally(results) {
|
|
61
|
-
|
|
62
|
-
|
|
93
|
+
export function tally(results: readonly Suppressed[]): {
|
|
94
|
+
counts: Counts;
|
|
95
|
+
unexplained: string[];
|
|
96
|
+
} {
|
|
97
|
+
const counts: Counts = {};
|
|
98
|
+
const unexplained: string[] = [];
|
|
63
99
|
for (const r of results)
|
|
64
100
|
for (const m of r.suppressedMessages ?? []) {
|
|
65
|
-
|
|
101
|
+
const rule = m.ruleId ?? "";
|
|
102
|
+
if (!LAYER_RULES.includes(rule)) continue;
|
|
66
103
|
const why = (m.suppressions ?? []).map((s) => s.justification ?? "");
|
|
67
104
|
if (!why.some((j) => LEGACY.test(j.trim()))) {
|
|
68
105
|
unexplained.push(
|
|
@@ -71,39 +108,64 @@ export function tally(results) {
|
|
|
71
108
|
continue;
|
|
72
109
|
}
|
|
73
110
|
const file = (counts[r.filePath] ??= {});
|
|
74
|
-
file[
|
|
111
|
+
file[rule] = (file[rule] ?? 0) + 1;
|
|
75
112
|
}
|
|
76
113
|
return { counts, unexplained };
|
|
77
114
|
}
|
|
78
115
|
|
|
116
|
+
/** A file carrying a legacy exemption where none may be, or one the list does not know. */
|
|
117
|
+
function misplaced(f: string, frozen: Counts): string[] {
|
|
118
|
+
if (NO_LEGACY_UNDER.some((d) => f.startsWith(d)))
|
|
119
|
+
return [
|
|
120
|
+
`${f}: carries a legacy layer exemption, and nothing under ${NO_LEGACY_UNDER.join(", ")} may — those folders are written under the rules. Fix the site (src/CLAUDE.md).`,
|
|
121
|
+
];
|
|
122
|
+
if (!(f in frozen))
|
|
123
|
+
return [
|
|
124
|
+
`${f}: a new legacy layer exemption is not accepted (#76). A module that needs the world is a new *.io.ts in the adapter of the program it talks to; an app file takes a port.`,
|
|
125
|
+
];
|
|
126
|
+
return [];
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** One rule's count in one file against its frozen number: grown, new, or shrunk without a record. */
|
|
130
|
+
function compareCount(
|
|
131
|
+
f: string,
|
|
132
|
+
rule: string,
|
|
133
|
+
got: number,
|
|
134
|
+
frozen: Counts,
|
|
135
|
+
): string[] {
|
|
136
|
+
const want = frozen[f]?.[rule];
|
|
137
|
+
if (want === undefined)
|
|
138
|
+
return f in frozen
|
|
139
|
+
? [
|
|
140
|
+
`${f}: ${String(got)} legacy ${rule} suppression(s), frozen at 0 — no new exemptions in existing files (#76).`,
|
|
141
|
+
]
|
|
142
|
+
: [];
|
|
143
|
+
if (got > want)
|
|
144
|
+
return [
|
|
145
|
+
`${f}: ${String(got)} legacy ${rule} suppressions, frozen at ${String(want)} — no new exemptions in existing files (#76).`,
|
|
146
|
+
];
|
|
147
|
+
if (got < want)
|
|
148
|
+
return [
|
|
149
|
+
`${f}: ${String(got)} legacy ${rule} suppressions, frozen at ${String(want)} — lower it to ${String(got)} in ${FROZEN_FILE} (the list must shrink with the code).`,
|
|
150
|
+
];
|
|
151
|
+
return [];
|
|
152
|
+
}
|
|
153
|
+
|
|
79
154
|
/** Compare the tally with the frozen list. Pure. `frozen` is `{ path: { rule: count } }`. */
|
|
80
|
-
export function judge({
|
|
155
|
+
export function judge({
|
|
156
|
+
counts,
|
|
157
|
+
unexplained,
|
|
158
|
+
frozen,
|
|
159
|
+
}: {
|
|
160
|
+
counts: Counts;
|
|
161
|
+
unexplained: readonly string[];
|
|
162
|
+
frozen: Counts;
|
|
163
|
+
}): string[] {
|
|
81
164
|
const problems = [...unexplained];
|
|
82
165
|
for (const [f, rules] of Object.entries(counts)) {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
);
|
|
87
|
-
else if (!(f in frozen))
|
|
88
|
-
problems.push(
|
|
89
|
-
`${f}: a new legacy layer exemption is not accepted (#76). A module that needs the world is a new *.io.ts in the adapter of the program it talks to; an app file takes a port.`,
|
|
90
|
-
);
|
|
91
|
-
for (const [rule, got] of Object.entries(rules)) {
|
|
92
|
-
const want = frozen[f]?.[rule];
|
|
93
|
-
if (want === undefined) {
|
|
94
|
-
if (f in frozen)
|
|
95
|
-
problems.push(
|
|
96
|
-
`${f}: ${String(got)} legacy ${rule} suppression(s), frozen at 0 — no new exemptions in existing files (#76).`,
|
|
97
|
-
);
|
|
98
|
-
} else if (got > want)
|
|
99
|
-
problems.push(
|
|
100
|
-
`${f}: ${String(got)} legacy ${rule} suppressions, frozen at ${String(want)} — no new exemptions in existing files (#76).`,
|
|
101
|
-
);
|
|
102
|
-
else if (got < want)
|
|
103
|
-
problems.push(
|
|
104
|
-
`${f}: ${String(got)} legacy ${rule} suppressions, frozen at ${String(want)} — lower it to ${String(got)} in ${FROZEN_FILE} (the list must shrink with the code).`,
|
|
105
|
-
);
|
|
106
|
-
}
|
|
166
|
+
problems.push(...misplaced(f, frozen));
|
|
167
|
+
for (const [rule, got] of Object.entries(rules))
|
|
168
|
+
problems.push(...compareCount(f, rule, got, frozen));
|
|
107
169
|
}
|
|
108
170
|
for (const [f, rules] of Object.entries(frozen))
|
|
109
171
|
for (const [rule, want] of Object.entries(rules))
|
|
@@ -114,12 +176,35 @@ export function judge({ counts, unexplained, frozen }) {
|
|
|
114
176
|
return problems;
|
|
115
177
|
}
|
|
116
178
|
|
|
179
|
+
/**
|
|
180
|
+
* The repository's configuration, narrowed to what this check counts: `LAYER_RULES` and their
|
|
181
|
+
* suppressions, with no type information. The configuration is
|
|
182
|
+
* type-aware, and a type-aware lint builds the whole TypeScript program first — 33 s on a CI runner
|
|
183
|
+
* under coverage, for rules that read only import paths. Suppressions of the filtered rules are
|
|
184
|
+
* still reported, which is what `tally` reads.
|
|
185
|
+
*/
|
|
186
|
+
function layerLint(cwd: string): ESLint {
|
|
187
|
+
return new ESLint({
|
|
188
|
+
cwd,
|
|
189
|
+
ruleFilter: ({ ruleId }) => LAYER_RULES.includes(ruleId),
|
|
190
|
+
overrideConfig: {
|
|
191
|
+
languageOptions: {
|
|
192
|
+
parserOptions: { project: false, projectService: false },
|
|
193
|
+
},
|
|
194
|
+
},
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
|
|
117
198
|
/** Lint `src/` with the repository's configuration and run the whole check. */
|
|
118
199
|
export async function checkFrozen(
|
|
119
|
-
root = ROOT,
|
|
120
|
-
{
|
|
121
|
-
)
|
|
122
|
-
|
|
200
|
+
root: string = ROOT,
|
|
201
|
+
{
|
|
202
|
+
lint = (cwd: string) => layerLint(cwd).lintFiles(["src/**/*.ts"]),
|
|
203
|
+
}: { lint?: (cwd: string) => Promise<readonly Suppressed[]> } = {},
|
|
204
|
+
): Promise<FrozenCheck> {
|
|
205
|
+
const data = FrozenFile.parse(
|
|
206
|
+
JSON.parse(readFileSync(join(root, FROZEN_FILE), "utf8")),
|
|
207
|
+
);
|
|
123
208
|
const frozen = data.files ?? {};
|
|
124
209
|
// ESLint itself THROWS when the glob matches nothing ("No files matching 'src/**/*.ts' were
|
|
125
210
|
// found") or only ignored files — measured. An empty result from any other linter (or a
|
|
@@ -147,7 +232,11 @@ export async function main({
|
|
|
147
232
|
check = () => checkFrozen(ROOT),
|
|
148
233
|
log = console.log,
|
|
149
234
|
err = console.error,
|
|
150
|
-
}
|
|
235
|
+
}: {
|
|
236
|
+
check?: () => Promise<FrozenCheck>;
|
|
237
|
+
log?: (line: string) => void;
|
|
238
|
+
err?: (line: string) => void;
|
|
239
|
+
} = {}): Promise<number> {
|
|
151
240
|
const { problems, counts } = await check();
|
|
152
241
|
if (problems.length) {
|
|
153
242
|
err(
|
|
@@ -15,11 +15,19 @@ import { describe, expect, test } from "vitest";
|
|
|
15
15
|
import { readFileSync } from "node:fs";
|
|
16
16
|
import { analyzeCommits } from "@semantic-release/commit-analyzer";
|
|
17
17
|
import { generateNotes } from "@semantic-release/release-notes-generator";
|
|
18
|
+
import { z } from "zod";
|
|
18
19
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
)
|
|
20
|
+
const PluginEntry = z.union([
|
|
21
|
+
z.string(),
|
|
22
|
+
z.tuple([z.string(), z.record(z.string(), z.unknown())]),
|
|
23
|
+
]);
|
|
24
|
+
const releaserc = z
|
|
25
|
+
.object({ plugins: z.array(PluginEntry) })
|
|
26
|
+
.parse(
|
|
27
|
+
JSON.parse(
|
|
28
|
+
readFileSync(new URL("../.releaserc.json", import.meta.url), "utf8"),
|
|
29
|
+
),
|
|
30
|
+
);
|
|
23
31
|
|
|
24
32
|
/** The options `.releaserc.json` gives one plugin, exactly as semantic-release would pass them. */
|
|
25
33
|
function optionsFor(name: string): Record<string, unknown> {
|
|
@@ -5,7 +5,7 @@ import assert from "node:assert/strict";
|
|
|
5
5
|
import { spawnSync } from "node:child_process";
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
7
|
import { test } from "vitest";
|
|
8
|
-
import { useTempDir, writeTree } from "../test/support.
|
|
8
|
+
import { useTempDir, writeTree } from "../test/support.ts";
|
|
9
9
|
import { main as seeMain } from "./rules-see-files.mjs";
|
|
10
10
|
|
|
11
11
|
const capture = () => {
|