paperlint 3.1.3 → 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/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
|
@@ -38,23 +38,44 @@
|
|
|
38
38
|
* author wrote correctly.
|
|
39
39
|
*/
|
|
40
40
|
import Ajv from "ajv";
|
|
41
|
+
import type { ErrorObject } from "ajv";
|
|
41
42
|
import { CORE_SCHEMA, load } from "js-yaml";
|
|
43
|
+
import assert from "node:assert/strict";
|
|
42
44
|
import { readFileSync } from "node:fs";
|
|
45
|
+
import { z } from "zod";
|
|
46
|
+
import type { RuleContext } from "./rule-context.ts";
|
|
47
|
+
|
|
48
|
+
/** A compiled schema, as far as `errorsOf` uses one. Ajv's `ValidateFunction` is one. */
|
|
49
|
+
export interface Validate {
|
|
50
|
+
(data: unknown): unknown;
|
|
51
|
+
readonly errors?: readonly ErrorObject[] | null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* What a parser failure says about itself. js-yaml's `YAMLException` carries a `reason`; anything
|
|
56
|
+
* else thrown may carry a `message`, or be a bare value that says nothing.
|
|
57
|
+
*/
|
|
58
|
+
const Thrown = z
|
|
59
|
+
.object({
|
|
60
|
+
reason: z.string().optional().catch(undefined),
|
|
61
|
+
message: z.string().optional().catch(undefined),
|
|
62
|
+
})
|
|
63
|
+
.catch({});
|
|
43
64
|
|
|
44
65
|
/** One ajv error as a line a person can act on. `if` errors only repeat their `then`. */
|
|
45
|
-
function describe(e) {
|
|
66
|
+
function describe(e: ErrorObject): string {
|
|
46
67
|
const where = e.dataPath
|
|
47
68
|
? `\`${e.dataPath.replace(/^\./, "")}\``
|
|
48
69
|
: "the frontmatter";
|
|
49
|
-
if (e.keyword === "enum")
|
|
70
|
+
if (e.keyword === "enum" && "allowedValues" in e.params)
|
|
50
71
|
return `${where} must be one of: ${e.params.allowedValues.join(", ")}`;
|
|
51
|
-
if (e.keyword === "additionalProperties")
|
|
72
|
+
if (e.keyword === "additionalProperties" && "additionalProperty" in e.params)
|
|
52
73
|
return `${where} has an unknown field \`${e.params.additionalProperty}\``;
|
|
53
|
-
return `${where} ${e.message}`;
|
|
74
|
+
return `${where} ${String(e.message)}`;
|
|
54
75
|
}
|
|
55
76
|
|
|
56
77
|
/** The schema's verdict on `data` as messages. Ajv types `errors` as possibly null; that is tolerated. */
|
|
57
|
-
export const errorsOf = (validate, data) =>
|
|
78
|
+
export const errorsOf = (validate: Validate, data: unknown): string[] =>
|
|
58
79
|
validate(data)
|
|
59
80
|
? []
|
|
60
81
|
: (validate.errors ?? []).filter((e) => e.keyword !== "if").map(describe);
|
|
@@ -63,10 +84,10 @@ export const errorsOf = (validate, data) =>
|
|
|
63
84
|
* A rule that validates a markdown file's YAML frontmatter against the JSON Schema in `schemaUrl`
|
|
64
85
|
* (a file shipped beside the rule). A file with no frontmatter is validated as `{}`.
|
|
65
86
|
*/
|
|
66
|
-
export function frontmatterRule(schemaUrl, description) {
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
);
|
|
87
|
+
export function frontmatterRule(schemaUrl: URL | string, description: string) {
|
|
88
|
+
const schema: unknown = JSON.parse(readFileSync(schemaUrl, "utf8"));
|
|
89
|
+
assert.ok(typeof schema === "object" && schema !== null, "not a schema");
|
|
90
|
+
const validate = new Ajv({ allErrors: true }).compile(schema);
|
|
70
91
|
return {
|
|
71
92
|
meta: {
|
|
72
93
|
type: "problem",
|
|
@@ -78,24 +99,25 @@ export function frontmatterRule(schemaUrl, description) {
|
|
|
78
99
|
invalid: "{{problem}}",
|
|
79
100
|
},
|
|
80
101
|
},
|
|
81
|
-
create(context) {
|
|
82
|
-
let frontmatter = null;
|
|
83
|
-
let data = {};
|
|
102
|
+
create(context: RuleContext) {
|
|
103
|
+
let frontmatter: object | null = null;
|
|
104
|
+
let data: unknown = {};
|
|
84
105
|
return {
|
|
85
|
-
yaml(node) {
|
|
106
|
+
yaml(node: { readonly value?: string }) {
|
|
86
107
|
frontmatter = node;
|
|
87
108
|
try {
|
|
88
109
|
data = load(node.value ?? "", { schema: CORE_SCHEMA }) ?? {};
|
|
89
110
|
} catch (e) {
|
|
90
111
|
data = null;
|
|
112
|
+
const { reason, message } = Thrown.parse(e);
|
|
91
113
|
context.report({
|
|
92
114
|
node,
|
|
93
115
|
messageId: "malformed",
|
|
94
|
-
data: { why:
|
|
116
|
+
data: { why: reason ?? message ?? "unparseable" },
|
|
95
117
|
});
|
|
96
118
|
}
|
|
97
119
|
},
|
|
98
|
-
"root:exit"(root) {
|
|
120
|
+
"root:exit"(root: object) {
|
|
99
121
|
if (data === null) return;
|
|
100
122
|
for (const problem of errorsOf(validate, data))
|
|
101
123
|
context.report({
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The slice of ESLint's rule API the rules in this directory use.
|
|
3
|
+
*
|
|
4
|
+
* These rules run on three languages (JavaScript, markdown and the `.tex` language next door),
|
|
5
|
+
* and ESLint's own `Rule.RuleContext` is the JavaScript one. So the rules are typed against what
|
|
6
|
+
* they actually touch, the way `src/venue-rules.ts` and `src/reference-rules.ts` already are.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/** A line and a 1-based column, as ESLint reports them. */
|
|
10
|
+
export interface Loc {
|
|
11
|
+
readonly line: number;
|
|
12
|
+
readonly column: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
import type { Root } from "mdast";
|
|
16
|
+
|
|
17
|
+
/** The source code a rule reads. `raw` exists on the `.tex` language's source code only. */
|
|
18
|
+
export interface RuleSourceCode {
|
|
19
|
+
readonly text: string;
|
|
20
|
+
readonly raw?: string;
|
|
21
|
+
/** mdast: the markdown language's tree, or the `.tex` language's projection of one. */
|
|
22
|
+
readonly ast?: Root;
|
|
23
|
+
getLocFromIndex(index: number): Loc;
|
|
24
|
+
getText(node?: object): string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** What a fix produces: replace `range` of the file with `text`. */
|
|
28
|
+
export interface RuleFix {
|
|
29
|
+
readonly range: readonly [number, number];
|
|
30
|
+
readonly text: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface RuleFixer {
|
|
34
|
+
replaceTextRange(range: readonly [number, number], text: string): RuleFix;
|
|
35
|
+
insertTextBeforeRange(
|
|
36
|
+
range: readonly [number, number],
|
|
37
|
+
text: string,
|
|
38
|
+
): RuleFix;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface RuleSuggestion {
|
|
42
|
+
readonly messageId: string;
|
|
43
|
+
readonly data?: Readonly<Record<string, string | number>>;
|
|
44
|
+
fix(fixer: RuleFixer): RuleFix;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface RuleReport {
|
|
48
|
+
readonly node?: object;
|
|
49
|
+
readonly loc?: { readonly start: Loc; readonly end: Loc };
|
|
50
|
+
readonly messageId: string;
|
|
51
|
+
readonly data?: Readonly<Record<string, string | number>>;
|
|
52
|
+
readonly fix?: (fixer: RuleFixer) => RuleFix;
|
|
53
|
+
readonly suggest?: readonly RuleSuggestion[];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface RuleContext {
|
|
57
|
+
readonly filename: string;
|
|
58
|
+
readonly options: readonly unknown[];
|
|
59
|
+
readonly settings: Readonly<Record<string, unknown>>;
|
|
60
|
+
readonly sourceCode: RuleSourceCode;
|
|
61
|
+
report(descriptor: RuleReport): void;
|
|
62
|
+
}
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* The package writes the cards, so it ships the schema: `sibling-frontmatter.schema.json`. Same
|
|
8
8
|
* mechanism as `review/frontmatter`.
|
|
9
9
|
*/
|
|
10
|
-
import { frontmatterRule } from "./review-frontmatter.
|
|
10
|
+
import { frontmatterRule } from "./review-frontmatter.ts";
|
|
11
11
|
|
|
12
12
|
export default {
|
|
13
13
|
rules: {
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
import assert from "node:assert/strict";
|
|
21
21
|
import { ESLint } from "eslint";
|
|
22
22
|
import { recordCheck } from "vigiles";
|
|
23
|
-
import localRules from "./temp-root-realpath.
|
|
23
|
+
import localRules from "./temp-root-realpath.ts";
|
|
24
24
|
|
|
25
25
|
const eslint = new ESLint({
|
|
26
26
|
overrideConfigFile: true,
|
|
@@ -45,8 +45,18 @@
|
|
|
45
45
|
* assertion was about their sum.
|
|
46
46
|
*/
|
|
47
47
|
|
|
48
|
+
import type { CallExpression, Node } from "estree";
|
|
49
|
+
import type { RuleContext } from "./rule-context.ts";
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Whether `value` is an ESTree node: an object whose `type` is a string. The walk below reaches
|
|
53
|
+
* every property of a node, and most of them (`regex`, `value`, arrays of flags) are not nodes.
|
|
54
|
+
*/
|
|
55
|
+
const isNode = (value: object): value is Node =>
|
|
56
|
+
"type" in value && typeof value.type === "string";
|
|
57
|
+
|
|
48
58
|
/** The called function's name: `f()` and `mod.f()` count as the same name for our purposes. */
|
|
49
|
-
const calleeName = (node) => {
|
|
59
|
+
const calleeName = (node: Node | null | undefined): string => {
|
|
50
60
|
if (node?.type !== "CallExpression") return "";
|
|
51
61
|
const c = node.callee;
|
|
52
62
|
if (c.type === "Identifier") return c.name;
|
|
@@ -65,10 +75,10 @@ const calleeName = (node) => {
|
|
|
65
75
|
* text-based guard would be forced to exclude itself — exactly the class of check string-based
|
|
66
76
|
* checks are banned for in this repository.
|
|
67
77
|
*/
|
|
68
|
-
const mentionsTmpdir = (node) => {
|
|
78
|
+
const mentionsTmpdir = (node: unknown): boolean => {
|
|
69
79
|
if (node === null || typeof node !== "object") return false;
|
|
70
80
|
if (Array.isArray(node)) return node.some(mentionsTmpdir);
|
|
71
|
-
if (
|
|
81
|
+
if (!isNode(node)) return false;
|
|
72
82
|
if (calleeName(node) === "tmpdir") return true;
|
|
73
83
|
for (const [key, value] of Object.entries(node)) {
|
|
74
84
|
if (key === "parent" || key === "loc" || key === "range") continue;
|
|
@@ -79,7 +89,9 @@ const mentionsTmpdir = (node) => {
|
|
|
79
89
|
};
|
|
80
90
|
|
|
81
91
|
/** `realpathSync(x)` and `realpathSync.native(x)` — both resolve the path, both count. */
|
|
82
|
-
const isRealpathCall = (
|
|
92
|
+
const isRealpathCall = (
|
|
93
|
+
node: Node | null | undefined,
|
|
94
|
+
): node is CallExpression => {
|
|
83
95
|
if (node?.type !== "CallExpression") return false;
|
|
84
96
|
const name = calleeName(node);
|
|
85
97
|
if (name === "realpathSync") return true;
|
|
@@ -91,6 +103,7 @@ const isRealpathCall = (node) => {
|
|
|
91
103
|
return (
|
|
92
104
|
obj?.type === "MemberExpression" &&
|
|
93
105
|
!obj.computed &&
|
|
106
|
+
"name" in obj.property &&
|
|
94
107
|
obj.property.name === "realpathSync"
|
|
95
108
|
);
|
|
96
109
|
};
|
|
@@ -113,9 +126,9 @@ export default {
|
|
|
113
126
|
"directory under two names, and every path comparison built on it silently compares the two.",
|
|
114
127
|
},
|
|
115
128
|
},
|
|
116
|
-
create(context) {
|
|
129
|
+
create(context: RuleContext) {
|
|
117
130
|
return {
|
|
118
|
-
CallExpression(node) {
|
|
131
|
+
CallExpression(node: CallExpression & { readonly parent?: Node }) {
|
|
119
132
|
if (calleeName(node) !== "mkdtempSync") return;
|
|
120
133
|
if (!mentionsTmpdir(node.arguments)) return;
|
|
121
134
|
const parent = node.parent;
|
|
@@ -70,7 +70,7 @@ import { fileURLToPath } from "node:url";
|
|
|
70
70
|
|
|
71
71
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
72
72
|
const ROOT = join(HERE, ".."); // eslint-rules → repository root
|
|
73
|
-
const RULES = join(HERE, "tex-build.
|
|
73
|
+
const RULES = join(HERE, "tex-build.ts");
|
|
74
74
|
assert.ok(existsSync(RULES), `no entry point for the test: ${RULES}`);
|
|
75
75
|
|
|
76
76
|
const TMP = realpathSync(mkdtempSync(join(tmpdir(), "tex-build-")));
|
|
@@ -78,7 +78,7 @@ const TMP = realpathSync(mkdtempSync(join(tmpdir(), "tex-build-")));
|
|
|
78
78
|
// `rmSync` at the bottom never runs in exactly the runs that are red.
|
|
79
79
|
process.on("exit", () => rmSync(TMP, { recursive: true, force: true }));
|
|
80
80
|
|
|
81
|
-
const { texLanguage } = await import(join(HERE, "latex-language.
|
|
81
|
+
const { texLanguage } = await import(join(HERE, "latex-language.ts"));
|
|
82
82
|
const texBuild = (await import(RULES)).default;
|
|
83
83
|
|
|
84
84
|
// A new rule with no declared severity must not slip through unnoticed.
|
|
@@ -50,6 +50,7 @@
|
|
|
50
50
|
* on `readdirSync` order and on how many drafts happen to lie next to the paper.
|
|
51
51
|
*/
|
|
52
52
|
import { readFileSync } from "node:fs";
|
|
53
|
+
import type { RuleContext } from "./rule-context.ts";
|
|
53
54
|
|
|
54
55
|
/**
|
|
55
56
|
* A copy rather than an import, for the same reason the prose lexicons are copies: a rule may
|
|
@@ -152,7 +153,7 @@ const futurePromise = {
|
|
|
152
153
|
"through paper.tex",
|
|
153
154
|
},
|
|
154
155
|
},
|
|
155
|
-
create(context) {
|
|
156
|
+
create(context: RuleContext) {
|
|
156
157
|
return {
|
|
157
158
|
root() {
|
|
158
159
|
// 🔴 ONE GUARD, NOT THREE. The first draft had `if (!file || !existsSync(file))`
|
|
@@ -177,7 +178,7 @@ const futurePromise = {
|
|
|
177
178
|
.split("\n")
|
|
178
179
|
.map((l) => l.replace(/(^|[^\\])%.*$/, "$1"));
|
|
179
180
|
lines.forEach((line, i) => {
|
|
180
|
-
const m =
|
|
181
|
+
const m = FUTURE_PROMISE_RE.exec(line);
|
|
181
182
|
if (!m) return;
|
|
182
183
|
const column = m.index + 1;
|
|
183
184
|
context.report({
|
|
@@ -259,7 +260,7 @@ const acmFrontmatterOverride = {
|
|
|
259
260
|
"on its own",
|
|
260
261
|
},
|
|
261
262
|
},
|
|
262
|
-
create(context) {
|
|
263
|
+
create(context: RuleContext) {
|
|
263
264
|
return {
|
|
264
265
|
root() {
|
|
265
266
|
// Same single guard as the sibling rule, and for the same measured reason: a separate
|
package/eslint.config.mjs
CHANGED
|
@@ -11,11 +11,11 @@
|
|
|
11
11
|
* is why `scripts/rules-see-files.mjs` exists and why it runs as part of the test suite:
|
|
12
12
|
* every rule declared below must be enabled for at least one file that is actually on disk.
|
|
13
13
|
*/
|
|
14
|
-
import { texLanguage } from "
|
|
15
|
-
import texBuild from "
|
|
14
|
+
import { texLanguage } from "#eslint-rules/latex-language";
|
|
15
|
+
import texBuild from "#eslint-rules/tex-build";
|
|
16
16
|
import markdown from "@eslint/markdown";
|
|
17
|
-
import localRules from "
|
|
18
|
-
import portRules from "
|
|
17
|
+
import localRules from "#eslint-rules/temp-root-realpath";
|
|
18
|
+
import portRules from "#eslint-rules/install-path-literals";
|
|
19
19
|
import n from "eslint-plugin-n";
|
|
20
20
|
import tseslint from "typescript-eslint";
|
|
21
21
|
import boundaries from "eslint-plugin-boundaries";
|
|
@@ -25,6 +25,35 @@ import { fileURLToPath } from "node:url";
|
|
|
25
25
|
// The complexity set, shared by the TypeScript block and the ratchet below. Every function
|
|
26
26
|
// measured over these limits on 2026-09-24 was either refactored under them (the #59 build code)
|
|
27
27
|
// or pinned at its current maximum in RATCHET.
|
|
28
|
+
const TS_FILES = ["**/*.ts", "**/*.mts"];
|
|
29
|
+
|
|
30
|
+
/** `x as unknown as T` tells the checker to look away (#76). Shared: a block REPLACES a rule's options. */
|
|
31
|
+
const AS_UNKNOWN_AS = {
|
|
32
|
+
selector:
|
|
33
|
+
"TSAsExpression > TSAsExpression[typeAnnotation.type='TSUnknownKeyword']",
|
|
34
|
+
message:
|
|
35
|
+
"`as unknown as` switches the type checker off. Convert with a function, or fix the type.",
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* A RULE MAY NOT ASK GIT. `paper/source` once bound a paper's stage to a commit sha and checked it
|
|
40
|
+
* with `git cat-file -e`; of four recorded shas, one still resolved ninety minutes later, because
|
|
41
|
+
* squash-merge and `gc` delete commits as routine. And `actions/checkout` fetches one commit, so
|
|
42
|
+
* under CI even a live sha does not resolve: the rule is red where it must hold and green locally.
|
|
43
|
+
* A rule reads the files ESLint gives it. Matched on the AST: a spawner called with the program
|
|
44
|
+
* `git` (or a shell line starting with `git `). Two things pass, deliberately: opening `.git/` with
|
|
45
|
+
* `fs`, and a program name assembled at runtime.
|
|
46
|
+
*/
|
|
47
|
+
const SPAWNERS = "exec|execSync|execFile|execFileSync|spawn|spawnSync|fork";
|
|
48
|
+
const GIT_IN_A_RULE = [
|
|
49
|
+
`CallExpression[callee.name=/^(${SPAWNERS})$/][arguments.0.value=/^git( |$)/]`,
|
|
50
|
+
`CallExpression[callee.property.name=/^(${SPAWNERS})$/][arguments.0.value=/^git( |$)/]`,
|
|
51
|
+
].map((selector) => ({
|
|
52
|
+
selector,
|
|
53
|
+
message:
|
|
54
|
+
"A rule may not run git: a sha is deleted by routine maintenance and absent from a shallow CI checkout. Read the file content ESLint passes the rule.",
|
|
55
|
+
}));
|
|
56
|
+
|
|
28
57
|
const MAX_LINES = { max: 60, skipComments: true, skipBlankLines: true };
|
|
29
58
|
|
|
30
59
|
// 🔴 A CEILING, NOT A PERMISSION: each number is the file's measured maximum on 2026-09-24, so a
|
|
@@ -38,6 +67,20 @@ const RATCHET = {
|
|
|
38
67
|
"src/hooks-settings.ts": { complexity: 12, "max-depth": 4 },
|
|
39
68
|
"src/structure.ts": { complexity: 12, "max-depth": 4 },
|
|
40
69
|
"src/new-paper.ts": { complexity: 11 },
|
|
70
|
+
// Moved from JavaScript on 2026-09-27 (#78) and measured that day: the move changed their
|
|
71
|
+
// language, not their shape, so their debt is pinned here like the rest rather than paid in it.
|
|
72
|
+
"eslint-rules/latex-language.ts": {
|
|
73
|
+
complexity: 68,
|
|
74
|
+
"max-lines-per-function": 314,
|
|
75
|
+
"max-depth": 4,
|
|
76
|
+
},
|
|
77
|
+
"eslint-rules/paper-typography.ts": { complexity: 29, "max-depth": 5 },
|
|
78
|
+
"eslint-rules/paper-stages.ts": {
|
|
79
|
+
complexity: 15,
|
|
80
|
+
"max-lines-per-function": 105,
|
|
81
|
+
},
|
|
82
|
+
"eslint-rules/temp-root-realpath.ts": { complexity: 13 },
|
|
83
|
+
"lib/markdown.ts": { complexity: 12 },
|
|
41
84
|
};
|
|
42
85
|
|
|
43
86
|
// ── Hexagonal layers (src/CLAUDE.md, issue #76) ─────────────────────────────────────────
|
|
@@ -121,6 +164,15 @@ export const layerBoundaries = (root) => ({
|
|
|
121
164
|
plugins: { boundaries },
|
|
122
165
|
settings: {
|
|
123
166
|
"boundaries/root-path": root,
|
|
167
|
+
// `#lib/*` and `#eslint-rules/*` are package.json subpath imports; the Node resolver the plugin
|
|
168
|
+
// defaults to does not read that map and leaves them unresolved, i.e. unknown. The TypeScript
|
|
169
|
+
// resolver reads it, and with the repository's source condition lands on the `.ts` file, which
|
|
170
|
+
// is classified as the js-module element it is.
|
|
171
|
+
"import/resolver": {
|
|
172
|
+
typescript: {
|
|
173
|
+
conditionNames: ["paperlint-source", "types", "import", "node"],
|
|
174
|
+
},
|
|
175
|
+
},
|
|
124
176
|
"boundaries/elements": [
|
|
125
177
|
{ type: "port", pattern: "src/ports", partialMatch: false },
|
|
126
178
|
{ type: "domain", pattern: "src/domain", partialMatch: false },
|
|
@@ -138,7 +190,7 @@ export const layerBoundaries = (root) => ({
|
|
|
138
190
|
partialMatch: false,
|
|
139
191
|
capture: ["kind"],
|
|
140
192
|
},
|
|
141
|
-
// test/support.
|
|
193
|
+
// test/support.ts — shared helpers for tests. Only the `test` category may import it: the
|
|
142
194
|
// policies below allow tests any element, and allow no other file this one.
|
|
143
195
|
{ type: "test-support", pattern: "test", partialMatch: false },
|
|
144
196
|
],
|
|
@@ -147,7 +199,8 @@ export const layerBoundaries = (root) => ({
|
|
|
147
199
|
{ category: "root", pattern: "src/cli.ts", exclusive: true },
|
|
148
200
|
// Every other file at the top of src/ is app. A new one gets the full app rules.
|
|
149
201
|
{ category: "app", pattern: "src/*.ts" },
|
|
150
|
-
|
|
202
|
+
// A harness is a test too: it drives the code through vigiles instead of vitest.
|
|
203
|
+
{ category: "test", pattern: "src/**/*.{test,harness}.ts" },
|
|
151
204
|
// AXIS B: purity is a category, orthogonal to the element.
|
|
152
205
|
{ category: "io", pattern: "src/adapters/*/*.io.ts" },
|
|
153
206
|
],
|
|
@@ -243,12 +296,17 @@ export const layerBoundaries = (root) => ({
|
|
|
243
296
|
* Files that did I/O before the layers existed carry an `eslint-disable-next-line` naming #76 above
|
|
244
297
|
* each such import or use; `reportUnusedDisableDirectives: "error"` turns a disable that suppresses
|
|
245
298
|
* nothing into a finding, so an exemption leaves the moment its I/O does, and
|
|
246
|
-
* `scripts/layer-legacy-frozen.
|
|
299
|
+
* `scripts/layer-legacy-frozen.ts` freezes how many each file may carry. A NEW module that needs
|
|
247
300
|
* the outside world is a new `*.io.ts` in the adapter of the program it talks to — not a disable.
|
|
248
301
|
*/
|
|
249
302
|
export const IO_GLOBALS = {
|
|
250
303
|
files: ["src/**/*.ts"],
|
|
251
|
-
ignores: [
|
|
304
|
+
ignores: [
|
|
305
|
+
"src/cli.ts",
|
|
306
|
+
"src/adapters/*/*.io.ts",
|
|
307
|
+
"src/**/*.test.ts",
|
|
308
|
+
"src/**/*.harness.ts",
|
|
309
|
+
],
|
|
252
310
|
linterOptions: { reportUnusedDisableDirectives: "error" },
|
|
253
311
|
rules: {
|
|
254
312
|
"no-restricted-globals": [
|
|
@@ -271,6 +329,27 @@ const ceiling = (rule, n) =>
|
|
|
271
329
|
? ["error", { ...MAX_LINES, max: n }]
|
|
272
330
|
: ["error", n];
|
|
273
331
|
|
|
332
|
+
/**
|
|
333
|
+
* New code is TypeScript (#78). A JavaScript file (`.js`, `.mjs`, `.cjs`) under any of these
|
|
334
|
+
* directories is an error, whatever its name — the check is on the file's AST root, so a name with
|
|
335
|
+
* any number of dots and an empty file are both caught. The directories are the ones already moved
|
|
336
|
+
* to TypeScript; each later step of #78 adds the directories it converts, and the last one replaces
|
|
337
|
+
* the list with every JavaScript file in the repository.
|
|
338
|
+
*/
|
|
339
|
+
const TYPESCRIPT_ONLY = [
|
|
340
|
+
"src/adapters/**/*.{js,mjs,cjs}",
|
|
341
|
+
"src/domain/**/*.{js,mjs,cjs}",
|
|
342
|
+
"src/ports/**/*.{js,mjs,cjs}",
|
|
343
|
+
// test/ itself and test/e2e — not test/fixtures/layers/eslint-rules, whose `rule.mjs` stands for
|
|
344
|
+
// the JavaScript module it is linted as.
|
|
345
|
+
"test/*.{js,mjs,cjs}",
|
|
346
|
+
"test/e2e/**/*.{js,mjs,cjs}",
|
|
347
|
+
"test/fixtures/layers/src/**/*.{js,mjs,cjs}",
|
|
348
|
+
"fixtures/real-markdown-paper/**/*.{js,mjs,cjs}",
|
|
349
|
+
"skills/paper-pipeline/scripts/fixtures/**/*.{js,mjs,cjs}",
|
|
350
|
+
"skills/plan-paper-timeline/fixtures/**/*.{js,mjs,cjs}",
|
|
351
|
+
];
|
|
352
|
+
|
|
274
353
|
export default [
|
|
275
354
|
// 🔴 TRANSIENT DIRECTORIES ARE NOT THE CORPUS, and leaving them in is a RACE, not sloppiness.
|
|
276
355
|
// `paper-stages.harness.mjs` creates a temp tree under fixtures and removes it when done,
|
|
@@ -297,37 +376,51 @@ export default [
|
|
|
297
376
|
// 🔴 THE PACKAGE'S TYPESCRIPT, and before 2026-09-24 no block matched it — the same silent
|
|
298
377
|
// ignore the `.mjs` block below was written against, recurring for `.ts` (#49). `src/` is the
|
|
299
378
|
// CLI and the build; the skill specs compile into the SKILL.md files the package ships; the
|
|
300
|
-
//
|
|
379
|
+
// hand-written `.d.mts` files type the `.mjs` modules `src/` imports. All tracked TypeScript.
|
|
301
380
|
//
|
|
302
381
|
// Measured on the first run (41727cd): 44 findings in 9 of 39 files, all in `src/`, plus one
|
|
303
382
|
// `no-useless-escape` in a spec that had dropped a backslash from its compiled SKILL.md. The
|
|
304
383
|
// #59 build files were refactored clean; the older CLI files sit in RATCHET, one block below.
|
|
305
384
|
//
|
|
306
|
-
//
|
|
307
|
-
//
|
|
385
|
+
// TYPE-AWARE since 2026-09-27: typescript-eslint's `strictTypeChecked` preset, fed by
|
|
386
|
+
// `tsconfig.test.json`, which now includes EVERY tracked `.ts`/`.mts` (the 24 skill specs and
|
|
387
|
+
// `vitest.config.ts` had been in no tsconfig, so neither tsc nor this block type-checked them).
|
|
388
|
+
// `parserOptions.project` rather than `projectService`: the service looks for the nearest
|
|
389
|
+
// `tsconfig.json`, and the root one covers only the shipped `src/`, so every test, script and
|
|
390
|
+
// spec would fall back to a default project. First run: 374 findings in 64 files, all fixed in
|
|
391
|
+
// the same PR, none suppressed.
|
|
308
392
|
//
|
|
309
393
|
// NOT HERE: `port/js-install-path` (43 findings, 41 of them the specs' `.claude/skills/`
|
|
310
394
|
// literals — issue #19's debt, `warn` for `.mjs` for the same reason; 2 in hooks-settings.ts,
|
|
311
395
|
// which writes the consumer's settings and must name the install) and `n/no-missing-import`
|
|
312
|
-
// (tsc already fails on an unresolved import
|
|
396
|
+
// (tsc already fails on an unresolved import).
|
|
397
|
+
...tseslint.configs.strictTypeChecked.map((config) => ({
|
|
398
|
+
...config,
|
|
399
|
+
files: TS_FILES,
|
|
400
|
+
})),
|
|
313
401
|
{
|
|
314
|
-
files:
|
|
315
|
-
languageOptions: {
|
|
402
|
+
files: TS_FILES,
|
|
403
|
+
languageOptions: {
|
|
404
|
+
parser: tseslint.parser,
|
|
405
|
+
parserOptions: {
|
|
406
|
+
project: "./tsconfig.test.json",
|
|
407
|
+
tsconfigRootDir: dirname(fileURLToPath(import.meta.url)),
|
|
408
|
+
},
|
|
409
|
+
},
|
|
316
410
|
plugins: { "@typescript-eslint": tseslint.plugin, local: localRules },
|
|
317
411
|
rules: {
|
|
318
412
|
// The same macOS-only defect as in the `.mjs` block; clean here, so it opens at `error`.
|
|
319
413
|
"local/temp-root-realpath": "error",
|
|
320
|
-
//
|
|
321
|
-
//
|
|
322
|
-
"@typescript-eslint/no-unused-vars": "error",
|
|
323
|
-
// `any` switches the checker off for everything it flows into — the gate that makes `src/`
|
|
324
|
-
// TypeScript at all. Five older sites are pinned inline and named in #49.
|
|
414
|
+
// `any` switches the checker off for everything it flows into. The preset has it too; named
|
|
415
|
+
// here so that dropping the preset cannot drop it.
|
|
325
416
|
"@typescript-eslint/no-explicit-any": "error",
|
|
326
417
|
// The behaviour rules of the `.mjs` block, for the same reasons: each changes what the code
|
|
327
418
|
// DOES, not how it looks, and a regex that escapes the wrong thing searches for the wrong thing.
|
|
328
419
|
"no-empty": "error",
|
|
329
420
|
"no-constant-condition": "error",
|
|
330
|
-
|
|
421
|
+
// `no-unreachable` is switched off by the preset's `eslint-recommended` layer on the claim
|
|
422
|
+
// that tsc reports it; tsc only greys unreachable code unless `allowUnreachableCode` is
|
|
423
|
+
// false, so it is switched back on. (`no-dupe-keys` is dropped: tsc does fail on it.)
|
|
331
424
|
"no-unreachable": "error",
|
|
332
425
|
"no-fallthrough": "error",
|
|
333
426
|
"no-useless-escape": "error",
|
|
@@ -339,15 +432,7 @@ export default [
|
|
|
339
432
|
complexity: ["error", 10],
|
|
340
433
|
// `x as unknown as T` tells the checker to look away: two spellings of one port were once
|
|
341
434
|
// reconciled that way (#76). A real conversion is a function; a real subset needs no cast.
|
|
342
|
-
"no-restricted-syntax": [
|
|
343
|
-
"error",
|
|
344
|
-
{
|
|
345
|
-
selector:
|
|
346
|
-
"TSAsExpression > TSAsExpression[typeAnnotation.type='TSUnknownKeyword']",
|
|
347
|
-
message:
|
|
348
|
-
"`as unknown as` switches the type checker off. Convert with a function, or fix the type.",
|
|
349
|
-
},
|
|
350
|
-
],
|
|
435
|
+
"no-restricted-syntax": ["error", AS_UNKNOWN_AS],
|
|
351
436
|
// Nesting beyond three blocks is where a step belongs in its own named function.
|
|
352
437
|
"max-depth": ["error", 3],
|
|
353
438
|
// Five positional parameters are a record without field names; pass an object instead.
|
|
@@ -423,7 +508,9 @@ export default [
|
|
|
423
508
|
// unreachable code, fallthrough in `case`.
|
|
424
509
|
"no-empty": "error",
|
|
425
510
|
"no-constant-condition": "error",
|
|
426
|
-
|
|
511
|
+
// `no-unreachable` is switched off by the preset's `eslint-recommended` layer on the claim
|
|
512
|
+
// that tsc reports it; tsc only greys unreachable code unless `allowUnreachableCode` is
|
|
513
|
+
// false, so it is switched back on. (`no-dupe-keys` is dropped: tsc does fail on it.)
|
|
427
514
|
"no-unreachable": "error",
|
|
428
515
|
"no-fallthrough": "error",
|
|
429
516
|
// Regexes: unnecessary escaping slash and control character in class — both findings about
|
|
@@ -507,4 +594,26 @@ export default [
|
|
|
507
594
|
// one whose firing path nothing exercises, which is rule 4 wearing a different hat. The
|
|
508
595
|
// pass/fail signal lives in `npm test`, not in the warning count of `npm run lint`.
|
|
509
596
|
},
|
|
597
|
+
// Rule sources, whatever their extension; tests and harnesses plant git calls on purpose.
|
|
598
|
+
{
|
|
599
|
+
files: ["eslint-rules/*.ts", "eslint-rules/*.mjs"],
|
|
600
|
+
ignores: ["eslint-rules/*.test.*", "eslint-rules/*.harness.*"],
|
|
601
|
+
rules: {
|
|
602
|
+
"no-restricted-syntax": ["error", AS_UNKNOWN_AS, ...GIT_IN_A_RULE],
|
|
603
|
+
},
|
|
604
|
+
},
|
|
605
|
+
// LAST, so no block above can replace its `no-restricted-syntax` options for these files (none
|
|
606
|
+
// sets that rule for JavaScript today; being last keeps it that way).
|
|
607
|
+
{
|
|
608
|
+
files: TYPESCRIPT_ONLY,
|
|
609
|
+
rules: {
|
|
610
|
+
"no-restricted-syntax": [
|
|
611
|
+
"error",
|
|
612
|
+
{
|
|
613
|
+
selector: "Program",
|
|
614
|
+
message: "New code is TypeScript (#78): write this file as .ts.",
|
|
615
|
+
},
|
|
616
|
+
],
|
|
617
|
+
},
|
|
618
|
+
},
|
|
510
619
|
];
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# PDF fixtures for the pdf.js reader
|
|
2
2
|
|
|
3
|
-
Real PDFs that `src/pdf-facts.harness.
|
|
3
|
+
Real PDFs that `src/pdf-facts.harness.ts` and `skills/render-paper/extract-pdf-facts.harness.ts`
|
|
4
4
|
read. They were measured with poppler and with pdf.js on 2026-09-24 (issue #61); the harnesses pin
|
|
5
5
|
poppler's numbers, so a pass means agreement with the tool pdf.js replaced.
|
|
6
6
|
|
|
@@ -13,7 +13,7 @@ poppler's numbers, so a pass means agreement with the tool pdf.js replaced.
|
|
|
13
13
|
| `encrypted.pdf` | `t3-all.pdf` saved with an AES-256 user password (PyMuPDF) | pdf.js refuses it with `PasswordException`; the reader reports `encrypted` |
|
|
14
14
|
| `hidden-text.pdf` | `hidden-text.tex`, pdflatex | two-column body text beside near-white, rotated and render-mode-3 text; page 2 holds only hidden text, so banal calls it blank |
|
|
15
15
|
|
|
16
|
-
`test/e2e/banal.
|
|
16
|
+
`test/e2e/banal.ts` also reads these files, with the real banal: it pins what banal 1.2 printed for
|
|
17
17
|
each one on poppler's `pdftohtml` 24.02.0 (2026-09-25), so a pass there means paperlint's pdf.js-written
|
|
18
18
|
XML measures the same as poppler's. `hidden-text.pdf` exists for that test: each kind of hidden text
|
|
19
19
|
changes banal's answer if it is counted.
|
|
@@ -1,18 +1,31 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* ONE reader of `baseline.json`, shared by the two runs that compare against it: the harness
|
|
3
|
-
* beside this file (the repository's own `bin/paperlint.mjs`) and `test/e2e/install.
|
|
3
|
+
* beside this file (the repository's own `bin/paperlint.mjs`) and `test/e2e/install.ts` (the binary
|
|
4
4
|
* a consumer actually got). Two copies of "growth fails, a drop never does" would drift the first
|
|
5
5
|
* time one of them is tightened, and the two runs would then disagree about the same article.
|
|
6
6
|
*/
|
|
7
7
|
import { readFileSync } from "node:fs";
|
|
8
8
|
import { dirname, join } from "node:path";
|
|
9
9
|
import { fileURLToPath } from "node:url";
|
|
10
|
+
import { z } from "zod";
|
|
11
|
+
|
|
12
|
+
/** Findings per rule id. */
|
|
13
|
+
export type Counts = Record<string, number>;
|
|
14
|
+
|
|
15
|
+
const Baseline = z.object({ findings: z.record(z.string(), z.number()) });
|
|
16
|
+
const LintOutput = z.array(
|
|
17
|
+
z.object({
|
|
18
|
+
messages: z.array(z.object({ ruleId: z.string().nullable() })).nullish(),
|
|
19
|
+
}),
|
|
20
|
+
);
|
|
10
21
|
|
|
11
22
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
12
23
|
|
|
13
24
|
/** The recorded counts, `{ruleId: n}`. */
|
|
14
|
-
export function recordedFindings() {
|
|
15
|
-
return
|
|
25
|
+
export function recordedFindings(): Counts {
|
|
26
|
+
return Baseline.parse(
|
|
27
|
+
JSON.parse(readFileSync(join(HERE, "baseline.json"), "utf8")),
|
|
28
|
+
).findings;
|
|
16
29
|
}
|
|
17
30
|
|
|
18
31
|
/**
|
|
@@ -20,11 +33,13 @@ export function recordedFindings() {
|
|
|
20
33
|
* 🔴 Output that does not parse THROWS. An empty set read as "clean" is how this repository's
|
|
21
34
|
* checks have gone hollow before.
|
|
22
35
|
*/
|
|
23
|
-
export function countByRule(stdout) {
|
|
24
|
-
const out = {};
|
|
25
|
-
for (const file of JSON.parse(stdout))
|
|
26
|
-
for (const m of file.messages ?? [])
|
|
27
|
-
|
|
36
|
+
export function countByRule(stdout: string): Counts {
|
|
37
|
+
const out: Counts = {};
|
|
38
|
+
for (const file of LintOutput.parse(JSON.parse(stdout)))
|
|
39
|
+
for (const m of file.messages ?? []) {
|
|
40
|
+
const rule = String(m.ruleId);
|
|
41
|
+
out[rule] = (out[rule] ?? 0) + 1;
|
|
42
|
+
}
|
|
28
43
|
return out;
|
|
29
44
|
}
|
|
30
45
|
|
|
@@ -37,12 +52,18 @@ export function countByRule(stdout) {
|
|
|
37
52
|
* A partial drop is neither: it is what a fix looks like (paperlint#44 is expected to take typography
|
|
38
53
|
* to zero, and then the recording is updated in the same change).
|
|
39
54
|
*/
|
|
40
|
-
export function compareToBaseline(
|
|
55
|
+
export function compareToBaseline(
|
|
56
|
+
found: Readonly<Counts>,
|
|
57
|
+
recorded: Readonly<Counts> = recordedFindings(),
|
|
58
|
+
): {
|
|
59
|
+
grew: { rule: string; now: number; recorded: number }[];
|
|
60
|
+
vanished: string[];
|
|
61
|
+
} {
|
|
41
62
|
const grew = Object.entries(found)
|
|
42
63
|
.filter(([rule, n]) => n > (recorded[rule] ?? 0))
|
|
43
64
|
.map(([rule, n]) => ({ rule, now: n, recorded: recorded[rule] ?? 0 }));
|
|
44
65
|
const vanished = Object.keys(recorded).filter(
|
|
45
|
-
(r) => recorded[r] > 0 && !(r in found),
|
|
66
|
+
(r) => (recorded[r] ?? 0) > 0 && !(r in found),
|
|
46
67
|
);
|
|
47
68
|
return { grew, vanished };
|
|
48
69
|
}
|