paperlint 3.0.0 → 3.1.1
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 +28 -41
- package/.github/workflows/dependabot-automerge.yml +1 -1
- package/CLAUDE.md +51 -56
- package/CONTRIBUTING.md +30 -18
- package/README.md +179 -180
- package/bin/paperlint.test.mjs +45 -0
- package/dist/adapters/node/files.io.d.ts +2 -0
- package/dist/adapters/node/files.io.d.ts.map +1 -1
- package/dist/adapters/node/files.io.js +3 -2
- package/dist/adapters/node/files.io.js.map +1 -1
- package/dist/adapters/pdfjs/fill.d.ts +15 -4
- package/dist/adapters/pdfjs/fill.d.ts.map +1 -1
- package/dist/adapters/pdfjs/fill.js +31 -21
- 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 +14 -5
- package/dist/adapters/references/index.js.map +1 -1
- package/dist/adapters/references/pool.d.ts +8 -0
- package/dist/adapters/references/pool.d.ts.map +1 -0
- package/dist/adapters/references/pool.js +19 -0
- package/dist/adapters/references/pool.js.map +1 -0
- package/dist/build-engine.d.ts.map +1 -1
- package/dist/build-engine.js +6 -2
- package/dist/build-engine.js.map +1 -1
- package/dist/build.d.ts +10 -2
- package/dist/build.d.ts.map +1 -1
- package/dist/build.js +29 -11
- package/dist/build.js.map +1 -1
- package/dist/cli.d.ts +93 -6
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +296 -80
- package/dist/cli.js.map +1 -1
- package/dist/doctor.d.ts +27 -1
- package/dist/doctor.d.ts.map +1 -1
- package/dist/doctor.js +4 -3
- package/dist/doctor.js.map +1 -1
- package/dist/domain/result.d.ts +0 -2
- package/dist/domain/result.d.ts.map +1 -1
- package/dist/domain/result.js +0 -4
- package/dist/domain/result.js.map +1 -1
- package/dist/engine.d.ts +9 -0
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +27 -0
- package/dist/engine.js.map +1 -1
- package/dist/hooks-settings.d.ts +8 -5
- package/dist/hooks-settings.d.ts.map +1 -1
- package/dist/hooks-settings.js +32 -32
- package/dist/hooks-settings.js.map +1 -1
- package/dist/init.d.ts +8 -0
- package/dist/init.d.ts.map +1 -1
- package/dist/init.js +4 -4
- package/dist/init.js.map +1 -1
- package/dist/latex-log.d.ts +0 -2
- package/dist/latex-log.d.ts.map +1 -1
- package/dist/latex-log.js +7 -13
- package/dist/latex-log.js.map +1 -1
- package/dist/link-skills.d.ts +15 -2
- package/dist/link-skills.d.ts.map +1 -1
- package/dist/link-skills.js +36 -20
- package/dist/link-skills.js.map +1 -1
- package/dist/new-paper.d.ts +8 -1
- package/dist/new-paper.d.ts.map +1 -1
- package/dist/new-paper.js +50 -15
- package/dist/new-paper.js.map +1 -1
- package/dist/paper-files.d.ts +30 -8
- package/dist/paper-files.d.ts.map +1 -1
- package/dist/paper-files.js +59 -30
- package/dist/paper-files.js.map +1 -1
- package/dist/pdf-facts.d.ts +11 -0
- package/dist/pdf-facts.d.ts.map +1 -1
- package/dist/pdf-facts.js +16 -18
- package/dist/pdf-facts.js.map +1 -1
- package/dist/pdf-geometry.js +1 -1
- package/dist/pdf-geometry.js.map +1 -1
- package/dist/presets.d.ts +2 -0
- package/dist/presets.d.ts.map +1 -1
- package/dist/presets.js +3 -1
- package/dist/presets.js.map +1 -1
- package/dist/references.js +2 -2
- package/dist/references.js.map +1 -1
- package/dist/structure.js +1 -1
- package/dist/structure.js.map +1 -1
- package/dist/tex-requirements.d.ts +8 -0
- package/dist/tex-requirements.d.ts.map +1 -1
- package/dist/tex-requirements.js +6 -5
- package/dist/tex-requirements.js.map +1 -1
- package/dist/toolchain.d.ts +8 -3
- package/dist/toolchain.d.ts.map +1 -1
- package/dist/toolchain.js +26 -10
- package/dist/toolchain.js.map +1 -1
- package/dist/types.d.ts +6 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/venue-rules.d.ts.map +1 -1
- package/dist/venue-rules.js +12 -10
- package/dist/venue-rules.js.map +1 -1
- package/docs/configuration.md +48 -4
- package/docs/prior-art/README.md +1 -1
- package/docs/prior-art/test-tooling.md +7 -30
- package/docs/rules.md +29 -25
- package/docs/skills.md +80 -0
- package/eslint-rules/install-path-literals.harness.mjs +17 -1
- package/eslint-rules/latex-language.harness.mjs +3 -3
- package/eslint-rules/latex-language.mjs +12 -9
- package/eslint-rules/latex-language.test.mjs +200 -0
- package/eslint-rules/paper-research-question.harness.mjs +73 -7
- package/eslint-rules/paper-stages.harness.mjs +9 -14
- package/eslint-rules/paper-stages.test.mjs +135 -0
- package/eslint-rules/paper-typography.mjs +56 -9
- package/eslint-rules/paper-typography.test.mjs +186 -1
- package/eslint-rules/pdf-last-page-balance.harness.mjs +63 -7
- package/eslint-rules/review-frontmatter.mjs +2 -1
- package/eslint-rules/review-frontmatter.schema.json +1 -1
- package/eslint-rules/review-frontmatter.test.mjs +37 -1
- package/eslint-rules/temp-root-realpath.harness.mjs +49 -0
- package/eslint-rules/tex-build.mjs +13 -13
- package/eslint.config.mjs +6 -2
- package/fixtures/toolchain-mirror/catalog.txt +1 -0
- package/fixtures/toolchain-mirror/install-tl +4 -1
- package/fixtures/toolchain-mirror/stub-fmtutil-sys +4 -0
- package/fixtures/toolchain-mirror/stub-tlmgr +15 -0
- package/hooks/hooks.harness.mjs +6 -11
- package/hooks/paper-edit-guard.hook.mjs +2 -2
- package/hooks/paper-edit-guard.test.mjs +61 -0
- package/hooks/paper-skills-nudge.hook.mjs +2 -2
- package/hooks/paper-skills-nudge.test.mjs +38 -0
- package/hooks/paper-status-gates.test.mjs +42 -0
- package/lib/agent-cli-version.harness.mjs +7 -0
- package/lib/check.d.mts +17 -0
- package/lib/check.mjs +50 -0
- package/lib/check.test.ts +67 -0
- package/lib/markdown.mjs +16 -69
- package/lib/markdown.test.mjs +246 -0
- package/lib/paper-config.harness.mjs +28 -9
- package/lib/paper-config.mjs +7 -2
- package/lib/skill-checks.mjs +18 -46
- package/lib/skill-checks.test.mjs +297 -0
- package/lib/skill-corpus.mjs +11 -13
- package/lib/skill-corpus.test.mjs +54 -0
- package/lib/skill-trigger-cases.harness.mjs +57 -4
- package/package.json +6 -5
- package/scripts/check.harness.mjs +4 -10
- package/scripts/check.mjs +45 -42
- package/scripts/check.test.mjs +104 -0
- package/scripts/coverage-config.test.ts +406 -0
- package/scripts/eslint-report-guard.test.mjs +48 -0
- package/scripts/harness-api.frozen.json +0 -2
- package/scripts/layer-legacy-frozen.d.mts +9 -1
- package/scripts/layer-legacy-frozen.mjs +28 -11
- package/scripts/layer-legacy-frozen.test.ts +143 -1
- package/scripts/rules-are-content-only.harness.mjs +3 -11
- package/scripts/rules-are-content-only.mjs +29 -19
- package/scripts/rules-are-content-only.test.mjs +61 -0
- package/scripts/rules-see-files.mjs +11 -9
- package/scripts/rules-see-files.test.mjs +71 -0
- package/scripts/test.mjs +44 -0
- package/scripts/test.test.mjs +52 -0
- package/skills/grade-paper-writing/__snapshots__/prose-lint.test.mjs.snap +57 -0
- package/skills/grade-paper-writing/prose-lint.mjs +31 -12
- package/skills/grade-paper-writing/prose-lint.test.mjs +153 -0
- package/skills/paper-pipeline/grade-paper-writing-ablation.eval.mjs +3 -3
- package/skills/paper-pipeline/scripts/README.md +13 -132
- package/skills/paper-pipeline/scripts/announce.test.mjs +102 -0
- package/skills/paper-pipeline/scripts/artifact-coverage.test.mjs +86 -0
- package/skills/paper-pipeline/scripts/consumer.d.mts +8 -1
- package/skills/paper-pipeline/scripts/consumer.harness.mjs +3 -3
- package/skills/paper-pipeline/scripts/consumer.mjs +13 -25
- package/skills/paper-pipeline/scripts/consumer.test.mjs +113 -0
- package/skills/paper-pipeline/scripts/extract-ref-facts.mjs +9 -4
- package/skills/paper-pipeline/scripts/extract-ref-facts.test.mjs +380 -0
- package/skills/paper-pipeline/scripts/fixtures/scorecard.mjs +71 -0
- package/skills/paper-pipeline/scripts/generated-code.harness.mjs +4 -4
- package/skills/paper-pipeline/scripts/generated-code.mjs +3 -2
- package/skills/paper-pipeline/scripts/generated-code.test.mjs +62 -0
- package/skills/paper-pipeline/scripts/ledger.cli.test.mjs +302 -0
- package/skills/paper-pipeline/scripts/ledger.mjs +1 -1
- package/skills/paper-pipeline/scripts/{ledger.selftest.mjs → ledger.test.mjs} +44 -76
- package/skills/paper-pipeline/scripts/pipeline-check.cli.test.mjs +474 -0
- package/skills/paper-pipeline/scripts/pipeline-check.harness.mjs +2 -63
- package/skills/paper-pipeline/scripts/pipeline-check.mjs +37 -21
- package/skills/paper-pipeline/scripts/pipeline-edges.mjs +2 -2
- package/skills/paper-pipeline/scripts/pipeline-edges.test.mjs +73 -0
- package/skills/paper-pipeline/scripts/population-map.harness.mjs +4 -47
- package/skills/paper-pipeline/scripts/population-map.mjs +12 -2
- package/skills/paper-pipeline/scripts/population-map.test.mjs +154 -0
- package/skills/paper-pipeline/scripts/provenance.harness.mjs +26 -0
- package/skills/paper-pipeline/scripts/round-diff.harness.mjs +1 -2
- package/skills/paper-pipeline/scripts/round-diff.mjs +13 -6
- package/skills/paper-pipeline/scripts/round-diff.test.mjs +159 -0
- package/skills/paper-pipeline/scripts/run-mechanical.mjs +30 -6
- package/skills/paper-pipeline/scripts/run-mechanical.test.mjs +468 -0
- package/skills/paper-pipeline/scripts/status.mjs +2 -2
- package/skills/paper-pipeline/scripts/status.test.mjs +232 -0
- package/skills/plan-paper-timeline/plan-paper-timeline.effects.harness.mjs +2 -2
- package/skills/render-paper/extract-pdf-facts.harness.mjs +56 -7
- package/skills/tighten-paper/__snapshots__/structure.test.mjs.snap +133 -0
- package/skills/tighten-paper/structure.test.mjs +116 -0
- package/skills/verify-citations/SKILL.md +5 -5
- package/skills/verify-citations/SKILL.md.spec.ts +4 -4
- package/skills/verify-citations/scripts/__snapshots__/verify-cites.net.test.mjs.snap +235 -0
- package/skills/verify-citations/scripts/bib-authors.check.test.mjs +274 -0
- package/skills/verify-citations/scripts/bib-authors.cli.test.mjs +178 -0
- package/skills/verify-citations/scripts/bib-authors.mjs +2 -1
- package/skills/verify-citations/scripts/verify-cites.cli.test.mjs +160 -0
- package/skills/verify-citations/scripts/verify-cites.mjs +96 -96
- package/skills/verify-citations/scripts/verify-cites.net.test.mjs +287 -0
- package/skills/verify-citations/scripts/verify-cites.test.mjs +149 -5
- package/src/adapters/banal/failure.test.ts +70 -1
- package/src/adapters/banal/index.test.ts +16 -0
- package/src/adapters/banal/output.test.ts +5 -0
- package/src/adapters/curl/download.test.ts +43 -1
- package/src/adapters/memory/index.test.ts +25 -0
- package/src/adapters/node/files.io.ts +3 -2
- package/src/adapters/node/files.test.ts +13 -1
- package/src/adapters/node/process.test.ts +38 -0
- package/src/adapters/pdfjs/fill.test.ts +34 -0
- package/src/adapters/pdfjs/fill.ts +44 -25
- package/src/adapters/references/index.test.ts +228 -0
- package/src/adapters/references/index.ts +21 -6
- package/src/adapters/references/pool.test.ts +31 -0
- package/src/adapters/references/pool.ts +24 -0
- package/src/build-engine.harness.mjs +3 -7
- package/src/build-engine.test.ts +137 -0
- package/src/build-engine.ts +6 -1
- package/src/build.harness.mjs +51 -11
- package/src/build.test.ts +362 -0
- package/src/build.ts +40 -14
- package/src/cli.harness.mjs +12 -19
- package/src/cli.test.ts +584 -0
- package/src/cli.ts +409 -94
- package/src/doctor.harness.mjs +3 -8
- package/src/doctor.test.ts +64 -0
- package/src/doctor.ts +12 -4
- package/src/domain/result.test.ts +4 -0
- package/src/domain/result.ts +0 -5
- package/src/engine.harness.mjs +77 -7
- package/src/engine.ts +35 -0
- package/src/facts-file.test.ts +110 -0
- package/src/hooks-settings.harness.mjs +43 -13
- package/src/hooks-settings.ts +41 -35
- package/src/init.test.ts +308 -5
- package/src/init.ts +4 -4
- package/src/latex-log.harness.mjs +32 -7
- package/src/latex-log.ts +6 -13
- package/src/latex-loop.harness.mjs +8 -7
- package/src/link-skills.harness.mjs +3 -8
- package/src/link-skills.test.ts +105 -0
- package/src/link-skills.ts +75 -21
- package/src/linted-set.test.ts +118 -0
- package/src/new-paper.harness.mjs +5 -7
- package/src/new-paper.test.ts +203 -2
- package/src/new-paper.ts +84 -17
- package/src/paper-files.test.ts +122 -5
- package/src/paper-files.ts +99 -39
- package/src/paper-settings-commands.test.ts +41 -5
- package/src/paper-settings.test.ts +20 -0
- package/src/parse-args.test.ts +48 -0
- package/src/pdf-facts.harness.mjs +3 -6
- package/src/pdf-facts.test.ts +144 -0
- package/src/pdf-facts.ts +17 -19
- package/src/pdf-geometry.harness.mjs +3 -7
- package/src/pdf-geometry.ts +1 -1
- package/src/presets.test.ts +2 -0
- package/src/presets.ts +7 -1
- package/src/references.test.ts +97 -0
- package/src/references.ts +2 -2
- package/src/rules-config.harness.mjs +10 -8
- package/src/rules-off.test.ts +111 -0
- package/src/structure.harness.mjs +5 -9
- package/src/structure.ts +1 -1
- package/src/tex-requirements.harness.mjs +34 -7
- package/src/tex-requirements.ts +12 -6
- package/src/toolchain.harness.mjs +44 -13
- package/src/toolchain.test.ts +280 -0
- package/src/toolchain.ts +37 -12
- package/src/types.ts +6 -0
- package/src/venue-rules.test.ts +63 -0
- package/src/venue-rules.ts +14 -12
- package/templates/paper/paperlint.json +1 -1
- package/action.mutations.mjs +0 -162
- package/eslint-rules/install-path-literals.mutations.mjs +0 -62
- package/eslint-rules/latex-language.mutations.mjs +0 -196
- package/eslint-rules/paper-research-question.mutations.mjs +0 -127
- package/eslint-rules/paper-stages.mutations.mjs +0 -81
- package/eslint-rules/papers.mutations.mjs +0 -186
- package/eslint-rules/temp-root-realpath.mutations.mjs +0 -99
- package/eslint-rules/tex-build.mutations.mjs +0 -258
- package/hooks/hooks.mutations.mjs +0 -337
- package/lib/agent-cli-version.mutations.mjs +0 -109
- package/lib/mutation-driver.harness.mjs +0 -227
- package/lib/mutation-driver.mjs +0 -397
- package/lib/mutation-driver.mutations.mjs +0 -68
- package/lib/paper-config.mutations.mjs +0 -143
- package/lib/skill-trigger-cases.mutations.mjs +0 -65
- package/scripts/check.mutations.mjs +0 -110
- package/scripts/exclusive.mjs +0 -138
- package/scripts/mutation-batteries-frozen.harness.mjs +0 -204
- package/scripts/mutation-batteries-frozen.mjs +0 -238
- package/scripts/mutation-batteries.frozen.json +0 -108
- package/scripts/rules-are-content-only.mutations.mjs +0 -81
- package/scripts/rules-see-files.mutations.mjs +0 -131
- package/scripts/run-mutations.mjs +0 -100
- package/skills/paper-pipeline/scripts/artifact-coverage.mutations.mjs +0 -218
- package/skills/paper-pipeline/scripts/consumer.mutations.mjs +0 -190
- package/skills/paper-pipeline/scripts/extract-ref-facts.mutations.mjs +0 -54
- package/skills/paper-pipeline/scripts/generated-code.mutations.mjs +0 -254
- package/skills/paper-pipeline/scripts/pipeline-check.mutations.mjs +0 -54
- package/skills/paper-pipeline/scripts/population-map.mutations.mjs +0 -65
- package/skills/paper-pipeline/scripts/population-map.selftest.mjs +0 -122
- package/skills/paper-pipeline/scripts/provenance.mutations.mjs +0 -59
- package/skills/paper-pipeline/scripts/round-diff.mutations.mjs +0 -276
- package/skills/plan-paper-timeline/plan-paper-timeline.effects.mutations.mjs +0 -65
- package/skills/skill-contract.mutations.mjs +0 -179
- package/src/build.mutations.mjs +0 -196
- package/src/cli.mutations.mjs +0 -414
- package/src/doctor.mutations.mjs +0 -175
- package/src/hooks-settings.mutations.mjs +0 -99
- package/src/link-skills.mutations.mjs +0 -140
- package/src/new-paper.mutations.mjs +0 -79
- package/src/structure.mutations.mjs +0 -83
package/src/cli.ts
CHANGED
|
@@ -56,6 +56,7 @@ import {
|
|
|
56
56
|
} from "./paper-settings.ts";
|
|
57
57
|
import { referenceRules, REFERENCE_RULE_LEVELS } from "./reference-rules.ts";
|
|
58
58
|
import { onlineReferences } from "./adapters/references/index.ts";
|
|
59
|
+
import type { CheckReferences } from "./ports/check-references.ts";
|
|
59
60
|
import {
|
|
60
61
|
narrowToOwners,
|
|
61
62
|
ownedPatterns,
|
|
@@ -64,7 +65,11 @@ import {
|
|
|
64
65
|
} from "./paper-files.ts";
|
|
65
66
|
import {
|
|
66
67
|
paperPreset,
|
|
67
|
-
|
|
68
|
+
settingsProblemLine,
|
|
69
|
+
presetProblemText,
|
|
70
|
+
resolvePreset,
|
|
71
|
+
shippedPresets,
|
|
72
|
+
SHIPPED_PREFIX,
|
|
68
73
|
type PaperPreset,
|
|
69
74
|
} from "./presets.ts";
|
|
70
75
|
import type { ToolInstaller } from "./ports/tool-installer.ts";
|
|
@@ -76,7 +81,12 @@ import {
|
|
|
76
81
|
type TexRequirements,
|
|
77
82
|
} from "./tex-requirements.ts";
|
|
78
83
|
import { doctor } from "./doctor.ts";
|
|
79
|
-
import {
|
|
84
|
+
import {
|
|
85
|
+
askOnTerminal,
|
|
86
|
+
askOrDefault,
|
|
87
|
+
init,
|
|
88
|
+
processInteractivity,
|
|
89
|
+
} from "./init.ts";
|
|
80
90
|
import {
|
|
81
91
|
DEFAULT_FORMAT,
|
|
82
92
|
FORMATS,
|
|
@@ -84,6 +94,7 @@ import {
|
|
|
84
94
|
newPaper,
|
|
85
95
|
reportNewPaper,
|
|
86
96
|
type PaperFormat,
|
|
97
|
+
type VenueSetting,
|
|
87
98
|
} from "./new-paper.ts";
|
|
88
99
|
// The one source for the consumer's config key lives in the .mjs half of the package (the ESLint
|
|
89
100
|
// rules and the skill scripts import it too); its types are in lib/paper-config.d.mts.
|
|
@@ -123,12 +134,15 @@ import siblingFrontmatter from "../eslint-rules/sibling-frontmatter.mjs";
|
|
|
123
134
|
// @ts-expect-error — an ESLint rule in .mjs, it has no types
|
|
124
135
|
import pdfRules from "../eslint-rules/pdf-last-page-balance.mjs";
|
|
125
136
|
|
|
137
|
+
/** The shipped venue presets, read from the package's venues directory — the one list. */
|
|
138
|
+
const SHIPPED_VENUES = (): string[] => shippedPresets(packageVenuesDir());
|
|
139
|
+
|
|
126
140
|
const USAGE = `paperlint — machine-checkable gates for a paper kept in git
|
|
127
141
|
|
|
128
142
|
npx paperlint init [dir] set the project up: detect the papers directory, declare it
|
|
129
143
|
in package.json, link the skills, wire the hooks into
|
|
130
144
|
.claude/settings.json, offer the CI step, report what is missing
|
|
131
|
-
npx paperlint new <name> [--format tex|md]
|
|
145
|
+
npx paperlint new <name> [--venue <preset>] [--kind <kind>] [--format tex|md]
|
|
132
146
|
create <papers>/<name>/ from the template; never overwrites,
|
|
133
147
|
on an existing folder adds only the missing files, then lints it
|
|
134
148
|
npx paperlint lint [paths…] run every rule over your papers
|
|
@@ -156,6 +170,15 @@ init:
|
|
|
156
170
|
--paper <name> create this paper too (without a human, the only way init creates one)
|
|
157
171
|
--format tex|md the new paper's source format; default tex
|
|
158
172
|
|
|
173
|
+
new:
|
|
174
|
+
--venue <preset> the venue preset, written as "extends" into the paper's paperlint.json:
|
|
175
|
+
a shipped one (${SHIPPED_VENUES().join(", ")}), or a path to your
|
|
176
|
+
own preset starting with ./ or ../, relative to where you run the command.
|
|
177
|
+
On a terminal without --venue, new asks; "none" leaves it unset
|
|
178
|
+
--kind <kind> the paper's kind at that venue (its page limit), e.g. short — one of the
|
|
179
|
+
preset's kinds; needs --venue
|
|
180
|
+
--format tex|md the paper's source format; default tex
|
|
181
|
+
|
|
159
182
|
lint:
|
|
160
183
|
npx paperlint lint [paths…] [--fix] [--config <file.json>] [--json]
|
|
161
184
|
|
|
@@ -188,7 +211,7 @@ settings — paperlint.json, at two levels, one schema. Both are optional.
|
|
|
188
211
|
|
|
189
212
|
{ "extends": "paperlint:aisec", "kind": "research", "rules": { "pdf/last-page-balance": "error" } }
|
|
190
213
|
|
|
191
|
-
"extends" names a venue preset: paperlint:<name> (shipped:
|
|
214
|
+
"extends" names a venue preset: paperlint:<name> (shipped: ${SHIPPED_VENUES().join(", ")})
|
|
192
215
|
or ./path.jsonc, relative to the paperlint.json. npm presets are not supported yet.
|
|
193
216
|
|
|
194
217
|
"rules" is { "<rule>": "<severity>" } for every paper file in scope, or ESLint flat-config
|
|
@@ -322,6 +345,64 @@ export const SHIPPED_RULES: ReadonlySet<string> = shippedRuleIds(
|
|
|
322
345
|
[markdown],
|
|
323
346
|
);
|
|
324
347
|
|
|
348
|
+
/** A plugin as `rulePlugins` gives it: its rules, and for `tex` the LaTeX language when given. */
|
|
349
|
+
export interface RulePlugin {
|
|
350
|
+
readonly rules?: Readonly<Record<string, unknown>>;
|
|
351
|
+
readonly languages?: Readonly<Record<string, unknown>>;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Every plugin paperlint's config registers, ONE object per name, read off that config. In it one
|
|
356
|
+
* name is bound to different objects in different blocks (`paper` beside `PIPELINE-STATUS.md` is
|
|
357
|
+
* not `paper` beside `paper.tex`); here their rules are merged, so a config that lints any paper
|
|
358
|
+
* file knows every rule id. `@eslint/markdown` is a dependency's plugin and is left out. The LaTeX
|
|
359
|
+
* language rides on `tex` when `texLanguage` is given, and that is how a consumer registers it: a
|
|
360
|
+
* second `tex` plugin of its own is refused by ESLint ("Cannot redefine plugin "tex"", measured
|
|
361
|
+
* with and without the language here). Without it, `tex` carries rules only — for a config that
|
|
362
|
+
* lints markdown papers alone.
|
|
363
|
+
*/
|
|
364
|
+
export function rulePlugins(texLanguage?: unknown): Record<string, RulePlugin> {
|
|
365
|
+
const out: Record<string, { rules: Record<string, unknown> }> = {};
|
|
366
|
+
for (const block of buildConfig({}, texLanguage ?? { sentinel: "tex" }))
|
|
367
|
+
for (const [name, plugin] of Object.entries(
|
|
368
|
+
(block as { plugins?: Record<string, RulePlugin> }).plugins ?? {},
|
|
369
|
+
))
|
|
370
|
+
if (plugin !== markdown)
|
|
371
|
+
out[name] = { rules: { ...out[name]?.rules, ...plugin.rules } };
|
|
372
|
+
if (out["tex"] && texLanguage !== undefined)
|
|
373
|
+
return {
|
|
374
|
+
...out,
|
|
375
|
+
tex: { ...out["tex"], languages: { latex: texLanguage } },
|
|
376
|
+
};
|
|
377
|
+
return out;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* A flat-config fragment for a consumer's OWN ESLint run over paper files (#104): every rule
|
|
382
|
+
* paperlint ships, registered and turned off. Without it a `% eslint-disable-next-line
|
|
383
|
+
* paper/leading-zero -- why` — the documented escape hatch — fails that run with "Definition for
|
|
384
|
+
* rule 'paper/leading-zero' was not found". Off, because `paperlint lint` is where they run.
|
|
385
|
+
*
|
|
386
|
+
* The second block is not optional, and it is measured: a directive naming a rule that is off
|
|
387
|
+
* suppresses nothing, so ESLint reports it as an unused directive (a warning by default, an error
|
|
388
|
+
* under `reportUnusedDisableDirectives: "error"`). On the files paperlint lints that report is
|
|
389
|
+
* switched off in THIS run — `paperlint lint` still reports a directive that silences nothing.
|
|
390
|
+
*/
|
|
391
|
+
export function rulesOff(texLanguage?: unknown): Linter.Config[] {
|
|
392
|
+
return [
|
|
393
|
+
{
|
|
394
|
+
name: "paperlint/rules-off",
|
|
395
|
+
plugins: rulePlugins(texLanguage) as Linter.Config["plugins"],
|
|
396
|
+
rules: Object.fromEntries([...SHIPPED_RULES].map((id) => [id, "off"])),
|
|
397
|
+
},
|
|
398
|
+
{
|
|
399
|
+
name: "paperlint/rules-off/directives",
|
|
400
|
+
files: PAPER_FILE_PATTERNS,
|
|
401
|
+
linterOptions: { reportUnusedDisableDirectives: "off" },
|
|
402
|
+
},
|
|
403
|
+
];
|
|
404
|
+
}
|
|
405
|
+
|
|
325
406
|
/** Whether a rule entry (`"error"`, `2`, `["warn", {…}]`) turns the rule on. */
|
|
326
407
|
const isOn = (entry: unknown): boolean => {
|
|
327
408
|
const sev = Array.isArray(entry) ? entry[0] : entry;
|
|
@@ -345,12 +426,17 @@ export const OPTIONAL_RULES: ReadonlySet<string> = new Set(
|
|
|
345
426
|
* 🔴 AN OPTIONAL RULE THAT IS ON AND REACHES NO PAPER IS A GREEN ZERO. A `files` glob that matches
|
|
346
427
|
* nothing — a typo, a path relative to the wrong directory — leaves the rule never invoked, and a
|
|
347
428
|
* rule that never runs reports exactly like a rule that passed. So for every optional rule the
|
|
348
|
-
* consumer turned on, some
|
|
429
|
+
* consumer turned on, some `paper.tex` must actually have it enabled; the ones none has are
|
|
349
430
|
* returned, for the caller to refuse.
|
|
431
|
+
*
|
|
432
|
+
* `files` is every `paper.tex` the guard may count: the ones this run linted AND every paper of the
|
|
433
|
+
* project. Judged against the run alone, `paperlint lint papers/c` failed on a block written for
|
|
434
|
+
* papers a and b (#103) — a block that reaches a paper outside this run is not dead. A glob that
|
|
435
|
+
* reaches no paper of the project at all still fails, from a subset as from the whole.
|
|
350
436
|
*/
|
|
351
437
|
export async function silentOptionalRules(
|
|
352
438
|
eslint: ESLint,
|
|
353
|
-
|
|
439
|
+
files: readonly string[],
|
|
354
440
|
opts: PaperlintConfig,
|
|
355
441
|
): Promise<string[]> {
|
|
356
442
|
const turnedOn = new Set(
|
|
@@ -361,11 +447,11 @@ export async function silentOptionalRules(
|
|
|
361
447
|
),
|
|
362
448
|
);
|
|
363
449
|
const reached = new Set<string>();
|
|
364
|
-
for (const f of
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
for (const id of turnedOn) if (isOn(cfg
|
|
450
|
+
for (const f of new Set(files.filter((p) => basename(p) === MAIN))) {
|
|
451
|
+
// Undefined for a file outside ESLint's cwd or scope: it reaches nothing.
|
|
452
|
+
const cfg = (await eslint.calculateConfigForFile(f)) as
|
|
453
|
+
{ rules?: Record<string, unknown> } | undefined;
|
|
454
|
+
for (const id of turnedOn) if (isOn(cfg?.rules?.[id])) reached.add(id);
|
|
369
455
|
}
|
|
370
456
|
return [...turnedOn].filter((id) => !reached.has(id));
|
|
371
457
|
}
|
|
@@ -387,8 +473,8 @@ export function paperRuleBlocks(
|
|
|
387
473
|
const out = { preset: [] as RuleBlock[], own: [] as RuleBlock[] };
|
|
388
474
|
for (const dir of papers) {
|
|
389
475
|
const p = paperPreset(dir, PRESET_DEPS);
|
|
390
|
-
if (p.kind === "settings-problem"
|
|
391
|
-
return { ok: false, error:
|
|
476
|
+
if (p.kind === "settings-problem")
|
|
477
|
+
return { ok: false, error: settingsProblemLine(dir, p.problem) };
|
|
392
478
|
const blocks = rulesOfPaper(dir, p);
|
|
393
479
|
if (!blocks.ok) return blocks;
|
|
394
480
|
out.preset.push(...blocks.value.preset);
|
|
@@ -404,9 +490,9 @@ export function paperRuleBlocks(
|
|
|
404
490
|
*/
|
|
405
491
|
function rulesOfPaper(
|
|
406
492
|
dir: string,
|
|
407
|
-
p: PaperPreset,
|
|
493
|
+
p: Exclude<PaperPreset, { kind: "settings-problem" }>,
|
|
408
494
|
): Parsed<{ preset: RuleBlock[]; own: RuleBlock[] }> {
|
|
409
|
-
const
|
|
495
|
+
const { settings } = p;
|
|
410
496
|
const fromPreset =
|
|
411
497
|
p.kind === "resolved"
|
|
412
498
|
? parseRuleEntries(
|
|
@@ -428,17 +514,17 @@ function rulesOfPaper(
|
|
|
428
514
|
)
|
|
429
515
|
: ownRules(dir, settings);
|
|
430
516
|
if (!own.ok) return own;
|
|
517
|
+
// No `files`: `narrowToOwners` scopes the block to what paperlint lints under `dir`, exceptions
|
|
518
|
+
// included. A copy of the globs here is how the siblings index got parsed as JavaScript (#101).
|
|
431
519
|
const scoped = (rules: Record<string, RuleEntry>): RuleBlock[] =>
|
|
432
|
-
Object.keys(rules).length
|
|
433
|
-
? [{ basePath: dir, files: PAPER_FILE_PATTERNS, rules }]
|
|
434
|
-
: [];
|
|
520
|
+
Object.keys(rules).length ? [{ basePath: dir, rules }] : [];
|
|
435
521
|
return {
|
|
436
522
|
ok: true,
|
|
437
523
|
value: { preset: scoped(fromPreset.value), own: [...own.value] },
|
|
438
524
|
};
|
|
439
525
|
}
|
|
440
526
|
|
|
441
|
-
/** A paper's `{ id: severity }` rules, as one block over the paper's files. */
|
|
527
|
+
/** A paper's `{ id: severity }` rules, as one block over the paper's files (narrowed later, #101). */
|
|
442
528
|
function ownRules(
|
|
443
529
|
dir: string,
|
|
444
530
|
settings: PaperSettings,
|
|
@@ -447,16 +533,13 @@ function ownRules(
|
|
|
447
533
|
if (!own.ok) return own;
|
|
448
534
|
return {
|
|
449
535
|
ok: true,
|
|
450
|
-
value: own.value
|
|
451
|
-
? [{ basePath: dir, files: PAPER_FILE_PATTERNS, rules: own.value }]
|
|
452
|
-
: [],
|
|
536
|
+
value: own.value ? [{ basePath: dir, rules: own.value }] : [],
|
|
453
537
|
};
|
|
454
538
|
}
|
|
455
539
|
|
|
456
540
|
/**
|
|
457
|
-
* The
|
|
458
|
-
*
|
|
459
|
-
* language — `paperlint.json` itself would be parsed as JavaScript.
|
|
541
|
+
* The globs paperlint lints, for the refusal message only. Which files a block reaches is decided by
|
|
542
|
+
* `narrowToOwners` from `ownedScopes` (src/paper-files.ts), where each glob keeps its exceptions.
|
|
460
543
|
*/
|
|
461
544
|
const PAPER_FILE_PATTERNS: string[] = ownedPatterns(
|
|
462
545
|
buildConfig({}, { sentinel: "tex language" }),
|
|
@@ -500,6 +583,9 @@ export function parseSettings(
|
|
|
500
583
|
const lintRoot = (home: string, paths: readonly string[]): string =>
|
|
501
584
|
commonDir([home, ...paths]);
|
|
502
585
|
|
|
586
|
+
/** A path as the reader typed it: relative to where the command runs, or whole when it IS there. */
|
|
587
|
+
const shown = (cwd: string, p: string): string => relative(cwd, p) || p;
|
|
588
|
+
|
|
503
589
|
/** The longest shared leading run of path segments. */
|
|
504
590
|
const commonDir = (paths: readonly string[]): string => {
|
|
505
591
|
const [first = [], ...rest] = paths.map((p) => p.split(sep));
|
|
@@ -525,29 +611,47 @@ export function parseArgs(argv: readonly string[]): Args {
|
|
|
525
611
|
noHooks: false,
|
|
526
612
|
paper: null,
|
|
527
613
|
format: null,
|
|
614
|
+
venue: null,
|
|
615
|
+
kind: null,
|
|
528
616
|
hooksMode: null,
|
|
529
617
|
// -1 = warnings NEVER fail the run. In this set most findings are advisory by design, and a
|
|
530
618
|
// gate that fails on advice gets muted entirely.
|
|
531
619
|
maxWarnings: -1,
|
|
532
620
|
};
|
|
621
|
+
// Consumed from the front: a value flag takes the next argument by shifting it off.
|
|
533
622
|
const rest = [...argv];
|
|
534
|
-
|
|
623
|
+
const first = rest[0];
|
|
624
|
+
if (first && !first.startsWith("-")) {
|
|
625
|
+
out.cmd = first;
|
|
626
|
+
rest.shift();
|
|
627
|
+
}
|
|
535
628
|
|
|
536
629
|
// 🔴 A FLAG WHOSE VALUE WAS TAKEN AWAY IS A REFUSAL, NOT A DEFAULT. The compiler found this
|
|
537
|
-
// during the move to TypeScript:
|
|
630
|
+
// during the move to TypeScript: the value past the last argument is `undefined`, and
|
|
538
631
|
// `paperlint lint --config` (the value forgotten, or eaten by a substitution in CI) silently turned
|
|
539
632
|
// into "no config given" — that is, it went to auto-discovery and linted against SOMEONE ELSE'S
|
|
540
633
|
// file, saying nothing. The failure is one-sided and toward silence, so it is cured by
|
|
541
634
|
// behaviour, not by a type cast.
|
|
542
|
-
const valueFor = (flag: string
|
|
543
|
-
const v = rest
|
|
635
|
+
const valueFor = (flag: string): string | undefined => {
|
|
636
|
+
const v = rest.shift();
|
|
544
637
|
if (v === undefined) out.missingValue = flag;
|
|
545
638
|
return v;
|
|
546
639
|
};
|
|
547
640
|
|
|
548
|
-
for (let
|
|
549
|
-
|
|
550
|
-
|
|
641
|
+
for (let arg = rest.shift(); arg !== undefined; arg = rest.shift()) {
|
|
642
|
+
// `--flag=value` is `--flag value` for every flag that takes a value: the CI action writes
|
|
643
|
+
// `--max-warnings="$N"`, and that form used to fall through to the list of paths.
|
|
644
|
+
const eq = arg.indexOf("=");
|
|
645
|
+
const inline =
|
|
646
|
+
arg.startsWith("--") && eq > 0 && VALUE_FLAGS.has(arg.slice(0, eq))
|
|
647
|
+
? arg.slice(eq + 1)
|
|
648
|
+
: undefined;
|
|
649
|
+
const a = inline === undefined ? arg : arg.slice(0, eq);
|
|
650
|
+
const take = (): string | undefined => {
|
|
651
|
+
if (inline === undefined) return valueFor(a);
|
|
652
|
+
if (inline === "") out.missingValue = a;
|
|
653
|
+
return inline === "" ? undefined : inline;
|
|
654
|
+
};
|
|
551
655
|
if (a === "--json") out.json = true;
|
|
552
656
|
else if (a === "--fix") out.fix = true;
|
|
553
657
|
else if (a === "--all") out.all = true;
|
|
@@ -557,32 +661,47 @@ export function parseArgs(argv: readonly string[]): Args {
|
|
|
557
661
|
else if (a === "--no-hooks") out.noHooks = true;
|
|
558
662
|
else if (a.startsWith("--hooks="))
|
|
559
663
|
out.hooksMode = a.slice("--hooks=".length);
|
|
560
|
-
else if (a === "--paper") out.paper =
|
|
561
|
-
else if (a === "--format") out.format =
|
|
664
|
+
else if (a === "--paper") out.paper = take() ?? null;
|
|
665
|
+
else if (a === "--format") out.format = take() ?? null;
|
|
666
|
+
else if (a === "--venue") out.venue = take() ?? null;
|
|
667
|
+
else if (a === "--kind") out.kind = take() ?? null;
|
|
562
668
|
// `--options` was the first spelling and is kept working. It named the wrong thing — every
|
|
563
669
|
// other tool in the stack calls this file its config — but a flag in someone's CI is not
|
|
564
670
|
// ours to break.
|
|
565
|
-
else if (a === "--config" || a === "--options")
|
|
566
|
-
out.config = valueFor(a, ++i) ?? null;
|
|
671
|
+
else if (a === "--config" || a === "--options") out.config = take() ?? null;
|
|
567
672
|
else if (a === "--max-warnings") {
|
|
568
|
-
const v =
|
|
673
|
+
const v = take();
|
|
569
674
|
if (v !== undefined) out.maxWarnings = Number(v);
|
|
570
675
|
} else if (a === "--help" || a === "-h") out.help = true;
|
|
676
|
+
// An unknown flag is refused by name. Read as a path, it silently lints something else.
|
|
677
|
+
else if (a.startsWith("-") && a !== "-") out.unknownFlag ??= a;
|
|
571
678
|
else out.paths.push(a);
|
|
572
679
|
}
|
|
573
680
|
return out;
|
|
574
681
|
}
|
|
575
682
|
|
|
683
|
+
/** The flags that take a value, in either spelling: `--flag value` or `--flag=value`. */
|
|
684
|
+
const VALUE_FLAGS: ReadonlySet<string> = new Set([
|
|
685
|
+
"--paper",
|
|
686
|
+
"--format",
|
|
687
|
+
"--venue",
|
|
688
|
+
"--kind",
|
|
689
|
+
"--config",
|
|
690
|
+
"--options",
|
|
691
|
+
"--max-warnings",
|
|
692
|
+
]);
|
|
693
|
+
|
|
576
694
|
/**
|
|
577
695
|
* This package's own version, from the `package.json` beside `src/` and `dist/` alike. `init` pins
|
|
578
696
|
* the CI action to its release tag; an unreadable manifest yields `undefined`, and init then keeps
|
|
579
697
|
* the placeholder instead of guessing.
|
|
580
698
|
*/
|
|
581
|
-
function ownVersion(
|
|
699
|
+
export function ownVersion(
|
|
700
|
+
readManifest: () => string = () =>
|
|
701
|
+
readFileSync(new URL("../package.json", import.meta.url), "utf8"),
|
|
702
|
+
): string | undefined {
|
|
582
703
|
try {
|
|
583
|
-
const v = JSON.parse(
|
|
584
|
-
readFileSync(new URL("../package.json", import.meta.url), "utf8"),
|
|
585
|
-
)?.version;
|
|
704
|
+
const v = JSON.parse(readManifest())?.version;
|
|
586
705
|
return typeof v === "string" ? v : undefined;
|
|
587
706
|
} catch {
|
|
588
707
|
return undefined;
|
|
@@ -636,12 +755,9 @@ export function readConfig(
|
|
|
636
755
|
//
|
|
637
756
|
// 🔴 IN `--json` MODE — TO stderr. Machine output must be ONE parsable document: a line before
|
|
638
757
|
// the array breaks any `| jq`, and it breaks it for the consumer, not for us.
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
relative(cwd, configPath) || CONFIG_FILE,
|
|
643
|
-
root,
|
|
644
|
-
);
|
|
758
|
+
const shownPath = shown(cwd, configPath);
|
|
759
|
+
(a.json ? err : log)(`config: ${shownPath}`);
|
|
760
|
+
const settings = parseSettings(parsed, shownPath, root);
|
|
645
761
|
if (!settings.ok) {
|
|
646
762
|
err(settings.error);
|
|
647
763
|
return { code: 2 };
|
|
@@ -769,16 +885,143 @@ export async function createPaperAt(
|
|
|
769
885
|
log,
|
|
770
886
|
err,
|
|
771
887
|
cwd,
|
|
772
|
-
|
|
888
|
+
venue = null,
|
|
889
|
+
}: {
|
|
890
|
+
log: typeof console.log;
|
|
891
|
+
err: typeof console.error;
|
|
892
|
+
cwd: string;
|
|
893
|
+
/** What `--venue` chose; null writes the template's `paperlint.json` as it is. */
|
|
894
|
+
venue?: VenueChoice | null;
|
|
895
|
+
},
|
|
773
896
|
): Promise<number> {
|
|
774
|
-
const result = newPaper(papersRoot, name, format);
|
|
775
|
-
const here = (p: string): string =>
|
|
897
|
+
const result = newPaper(papersRoot, name, format, { venue });
|
|
898
|
+
const here = (p: string): string => shown(cwd, p);
|
|
776
899
|
for (const line of reportNewPaper(result, here)) log(line);
|
|
777
900
|
if (!result.ok) return 2;
|
|
901
|
+
const config = here(join(result.dir, CONFIG_FILE));
|
|
902
|
+
if (venue === null)
|
|
903
|
+
log(
|
|
904
|
+
` venue: none yet — set "extends" in ${config}, or next time: paperlint new <name> --venue <preset> (${SHIPPED_VENUES().join(", ")})`,
|
|
905
|
+
);
|
|
906
|
+
else if (venue.kind === null && venue.kinds.length > 0)
|
|
907
|
+
log(
|
|
908
|
+
` kind: not set — \`${venue.label}\` sets a page limit per kind (${venue.kinds.join(", ")}); add "kind" to ${config}. Until then lint reports pdf/profile`,
|
|
909
|
+
);
|
|
778
910
|
log(``);
|
|
779
911
|
return run(["lint", result.dir], { log, err, cwd });
|
|
780
912
|
}
|
|
781
913
|
|
|
914
|
+
/** A venue `paperlint new` will write, with what the messages need to say about it. */
|
|
915
|
+
export interface VenueChoice extends VenueSetting {
|
|
916
|
+
/** The preset's word in messages (`agenticdev`, `my-workshop`). */
|
|
917
|
+
readonly label: string;
|
|
918
|
+
/** The preset's kinds; empty when it sets no page limit. */
|
|
919
|
+
readonly kinds: readonly string[];
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
/**
|
|
923
|
+
* `--venue` / `--kind` → the `extends` and `kind` to write into `<paperDir>/paperlint.json`, or why
|
|
924
|
+
* not. Parsed here, at the boundary, and resolved through the same `resolvePreset` lint uses — so a
|
|
925
|
+
* venue `new` accepts is a venue lint resolves. On a terminal with no `--venue` it asks, with
|
|
926
|
+
* "none" as the default; without a terminal it chooses nothing (null), as before.
|
|
927
|
+
*
|
|
928
|
+
* A path is relative to where the command runs, because that is where it was typed; it is written
|
|
929
|
+
* relative to the paper's `paperlint.json`, because that is what `extends` is relative to. Written
|
|
930
|
+
* as typed, `./venues/x.jsonc` would name `<paper>/venues/x.jsonc`.
|
|
931
|
+
*/
|
|
932
|
+
export async function chooseVenue(
|
|
933
|
+
flags: { readonly venue: string | null; readonly kind: string | null },
|
|
934
|
+
{
|
|
935
|
+
paperDir,
|
|
936
|
+
cwd,
|
|
937
|
+
interactive,
|
|
938
|
+
ask,
|
|
939
|
+
}: {
|
|
940
|
+
paperDir: string;
|
|
941
|
+
cwd: string;
|
|
942
|
+
interactive: boolean;
|
|
943
|
+
ask: (q: string) => Promise<string>;
|
|
944
|
+
},
|
|
945
|
+
): Promise<Parsed<VenueChoice | null>> {
|
|
946
|
+
const shipped = SHIPPED_VENUES();
|
|
947
|
+
if (flags.kind !== null && flags.venue === null)
|
|
948
|
+
return bad(
|
|
949
|
+
`--kind needs --venue: a kind is a page limit of one venue preset — \`paperlint new <name> --venue <preset> --kind ${flags.kind}\``,
|
|
950
|
+
);
|
|
951
|
+
const asked = async (q: string): Promise<string> =>
|
|
952
|
+
(await askOrDefault(ask, q))?.trim() ?? "";
|
|
953
|
+
const venue =
|
|
954
|
+
flags.venue ??
|
|
955
|
+
(interactive
|
|
956
|
+
? await asked(`venue: ${[...shipped, "none"].join(" / ")} [none] `)
|
|
957
|
+
: "");
|
|
958
|
+
if (venue === "" || (flags.venue === null && venue === "none"))
|
|
959
|
+
return { ok: true, value: null };
|
|
960
|
+
const spec = venueSpec(venue, { shipped, paperDir, cwd });
|
|
961
|
+
if (!spec.ok) return spec;
|
|
962
|
+
const r = resolvePreset(spec.value, join(paperDir, CONFIG_FILE), PRESET_DEPS);
|
|
963
|
+
if (!r.ok) return bad(`--venue ${venue}: ${presetProblemText(r.error)}`);
|
|
964
|
+
const kinds = [...r.value.format.kinds.keys()];
|
|
965
|
+
const label = r.value.label;
|
|
966
|
+
if (flags.kind !== null) {
|
|
967
|
+
if (kinds.length === 0)
|
|
968
|
+
return bad(
|
|
969
|
+
`--kind ${flags.kind}: \`${label}\` has no kinds — it sets no page limit, so there is no kind to choose`,
|
|
970
|
+
);
|
|
971
|
+
if (!kinds.includes(flags.kind))
|
|
972
|
+
return bad(
|
|
973
|
+
`--kind ${flags.kind}: \`${label}\` has no kind \`${flags.kind}\`; its kinds: ${kinds.join(", ")}`,
|
|
974
|
+
);
|
|
975
|
+
}
|
|
976
|
+
const kind =
|
|
977
|
+
flags.kind ??
|
|
978
|
+
(interactive && kinds.length > 0
|
|
979
|
+
? await asked(`kind: ${[...kinds, "later"].join(" / ")} [later] `)
|
|
980
|
+
: null);
|
|
981
|
+
return {
|
|
982
|
+
ok: true,
|
|
983
|
+
value: {
|
|
984
|
+
extends: spec.value,
|
|
985
|
+
kind: kind !== null && kinds.includes(kind) ? kind : null,
|
|
986
|
+
label,
|
|
987
|
+
kinds,
|
|
988
|
+
},
|
|
989
|
+
};
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
const bad = (error: string): { ok: false; error: string } => ({
|
|
993
|
+
ok: false,
|
|
994
|
+
error,
|
|
995
|
+
});
|
|
996
|
+
|
|
997
|
+
/** What `--venue` names → the `extends` value, relative to `<paperDir>/paperlint.json`. */
|
|
998
|
+
function venueSpec(
|
|
999
|
+
venue: string,
|
|
1000
|
+
{
|
|
1001
|
+
shipped,
|
|
1002
|
+
paperDir,
|
|
1003
|
+
cwd,
|
|
1004
|
+
}: { shipped: readonly string[]; paperDir: string; cwd: string },
|
|
1005
|
+
): Parsed<string> {
|
|
1006
|
+
if (venue.startsWith("./") || venue.startsWith("../")) {
|
|
1007
|
+
const file = resolve(cwd, venue);
|
|
1008
|
+
if (!existsSync(file))
|
|
1009
|
+
return bad(
|
|
1010
|
+
`--venue ${venue}: no such file (${file}) — a path is relative to where you run the command`,
|
|
1011
|
+
);
|
|
1012
|
+
const rel = relative(paperDir, file).split(sep).join("/");
|
|
1013
|
+
return { ok: true, value: rel.startsWith("../") ? rel : `./${rel}` };
|
|
1014
|
+
}
|
|
1015
|
+
const name = venue.startsWith(SHIPPED_PREFIX)
|
|
1016
|
+
? venue.slice(SHIPPED_PREFIX.length)
|
|
1017
|
+
: venue;
|
|
1018
|
+
if (!shipped.includes(name))
|
|
1019
|
+
return bad(
|
|
1020
|
+
`--venue ${venue}: no such venue preset. Shipped: ${shipped.join(", ")} — or a path to your own preset, starting with ./ or ../`,
|
|
1021
|
+
);
|
|
1022
|
+
return { ok: true, value: `${SHIPPED_PREFIX}${name}` };
|
|
1023
|
+
}
|
|
1024
|
+
|
|
782
1025
|
/**
|
|
783
1026
|
* `paperlint new <name>` — the papers directory comes from the same declaration every other command
|
|
784
1027
|
* reads; there is no second way to name it. See `new-paper.ts` for what it writes and why.
|
|
@@ -800,7 +1043,7 @@ async function runNew(
|
|
|
800
1043
|
const [name, ...extra] = a.paths;
|
|
801
1044
|
if (!name || extra.length > 0) {
|
|
802
1045
|
err(
|
|
803
|
-
`\`new\` takes exactly one paper name: \`paperlint new my-paper [--format tex|md]\``,
|
|
1046
|
+
`\`new\` takes exactly one paper name: \`paperlint new my-paper [--venue <preset>] [--kind <kind>] [--format tex|md]\``,
|
|
804
1047
|
);
|
|
805
1048
|
return 2;
|
|
806
1049
|
}
|
|
@@ -814,10 +1057,10 @@ async function runNew(
|
|
|
814
1057
|
}
|
|
815
1058
|
format = a.format;
|
|
816
1059
|
} else if (processInteractivity(a.yes).interactive) {
|
|
817
|
-
const f =
|
|
818
|
-
(
|
|
819
|
-
);
|
|
820
|
-
if (isFormat(f
|
|
1060
|
+
const f = (
|
|
1061
|
+
await askOrDefault(ask, `format: tex / md [${DEFAULT_FORMAT}] `)
|
|
1062
|
+
)?.trim();
|
|
1063
|
+
if (isFormat(f)) format = f;
|
|
821
1064
|
}
|
|
822
1065
|
const cfg = readConfig({ ...a, json: false }, { log: () => {}, err, cwd });
|
|
823
1066
|
if (cfg.code !== undefined) return cfg.code;
|
|
@@ -831,9 +1074,32 @@ async function runNew(
|
|
|
831
1074
|
}
|
|
832
1075
|
if (roots.length > 1)
|
|
833
1076
|
log(
|
|
834
|
-
`several papers directories are declared — using the first: ${
|
|
1077
|
+
`several papers directories are declared — using the first: ${shown(cwd, papersRoot)}`,
|
|
1078
|
+
);
|
|
1079
|
+
const paperDir = join(papersRoot, name);
|
|
1080
|
+
// An existing paperlint.json is never overwritten, so a venue for it is refused, not dropped.
|
|
1081
|
+
const hasConfig = existsSync(join(paperDir, CONFIG_FILE));
|
|
1082
|
+
if (hasConfig && (a.venue !== null || a.kind !== null)) {
|
|
1083
|
+
err(
|
|
1084
|
+
`${relative(cwd, join(paperDir, CONFIG_FILE))} already exists and is never overwritten — set "extends" and "kind" in it by hand`,
|
|
835
1085
|
);
|
|
836
|
-
|
|
1086
|
+
return 2;
|
|
1087
|
+
}
|
|
1088
|
+
const venue = hasConfig
|
|
1089
|
+
? { ok: true as const, value: null }
|
|
1090
|
+
: await chooseVenue(a, {
|
|
1091
|
+
paperDir,
|
|
1092
|
+
cwd,
|
|
1093
|
+
interactive: processInteractivity(a.yes).interactive,
|
|
1094
|
+
ask,
|
|
1095
|
+
});
|
|
1096
|
+
if (!venue.ok) return (err(venue.error), 2);
|
|
1097
|
+
return createPaperAt(papersRoot, name, format, {
|
|
1098
|
+
log,
|
|
1099
|
+
err,
|
|
1100
|
+
cwd,
|
|
1101
|
+
venue: venue.value,
|
|
1102
|
+
});
|
|
837
1103
|
}
|
|
838
1104
|
|
|
839
1105
|
/**
|
|
@@ -849,7 +1115,13 @@ async function runBuild(
|
|
|
849
1115
|
log,
|
|
850
1116
|
err,
|
|
851
1117
|
cwd,
|
|
852
|
-
|
|
1118
|
+
checkReferences,
|
|
1119
|
+
}: {
|
|
1120
|
+
log: typeof console.log;
|
|
1121
|
+
err: typeof console.error;
|
|
1122
|
+
cwd: string;
|
|
1123
|
+
checkReferences: CheckReferences;
|
|
1124
|
+
},
|
|
853
1125
|
): Promise<number> {
|
|
854
1126
|
const cfg = readConfig(a, { log, err, cwd });
|
|
855
1127
|
if (cfg.code !== undefined) return cfg.code;
|
|
@@ -881,7 +1153,7 @@ async function runBuild(
|
|
|
881
1153
|
cwd,
|
|
882
1154
|
dryRun: a.dryRun,
|
|
883
1155
|
log,
|
|
884
|
-
checkReferences
|
|
1156
|
+
checkReferences,
|
|
885
1157
|
engine: () => engineEnv(targets, a, { log, err }),
|
|
886
1158
|
});
|
|
887
1159
|
if (out.kind === "no-engine") return 1;
|
|
@@ -953,7 +1225,12 @@ const SIMPLE: Readonly<
|
|
|
953
1225
|
string,
|
|
954
1226
|
(
|
|
955
1227
|
a: Args,
|
|
956
|
-
io: {
|
|
1228
|
+
io: {
|
|
1229
|
+
log: typeof console.log;
|
|
1230
|
+
err: typeof console.error;
|
|
1231
|
+
cwd: string;
|
|
1232
|
+
checkReferences: CheckReferences;
|
|
1233
|
+
},
|
|
957
1234
|
) => number | Promise<number>
|
|
958
1235
|
>
|
|
959
1236
|
> = {
|
|
@@ -982,6 +1259,44 @@ function hostBanalInstaller(): ToolInstaller {
|
|
|
982
1259
|
return banalInstaller({ ...ports, download }, s);
|
|
983
1260
|
}
|
|
984
1261
|
|
|
1262
|
+
/**
|
|
1263
|
+
* init's TeX Live step: whether paperlint's own tree is installed, and how to install it —
|
|
1264
|
+
* `runToolchain` over what this project's papers need. `toolchain` is the seam a test replaces:
|
|
1265
|
+
* the real one downloads ~270 MB.
|
|
1266
|
+
*/
|
|
1267
|
+
export function initTexLive(
|
|
1268
|
+
a: Args,
|
|
1269
|
+
{
|
|
1270
|
+
log,
|
|
1271
|
+
err,
|
|
1272
|
+
cwd,
|
|
1273
|
+
}: { log: typeof console.log; err: typeof console.error; cwd: string },
|
|
1274
|
+
toolchain: typeof runToolchain = runToolchain,
|
|
1275
|
+
): { readonly installed: () => boolean; readonly install: () => number } {
|
|
1276
|
+
return {
|
|
1277
|
+
installed: () => cachedTree(cacheRoot(process.env)) !== null,
|
|
1278
|
+
install: () =>
|
|
1279
|
+
toolchain({
|
|
1280
|
+
check: false,
|
|
1281
|
+
log,
|
|
1282
|
+
err,
|
|
1283
|
+
banal: hostBanalInstaller(),
|
|
1284
|
+
tex: toolchainTex(resolve(cwd, a.paths[0] ?? ".")),
|
|
1285
|
+
}),
|
|
1286
|
+
};
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
/**
|
|
1290
|
+
* The first papers directory the CLI would lint from `cwd`, or null when the config does not read
|
|
1291
|
+
* or names none — what `doctor` (and `init`, through it) compares the hooks' directory against.
|
|
1292
|
+
*/
|
|
1293
|
+
function cliPapers(a: Args, cwd: string): string | null {
|
|
1294
|
+
const read = readConfig(a, { log: () => {}, err: () => {}, cwd });
|
|
1295
|
+
return read.code === undefined
|
|
1296
|
+
? (toPaths(papersDirOf(read.opts))[0] ?? null)
|
|
1297
|
+
: null;
|
|
1298
|
+
}
|
|
1299
|
+
|
|
985
1300
|
/** `paperlint init`: its flags checked here, the install itself in `init.ts`. */
|
|
986
1301
|
async function runInit(
|
|
987
1302
|
a: Args,
|
|
@@ -1014,26 +1329,9 @@ async function runInit(
|
|
|
1014
1329
|
format: isFormat(a.format) ? a.format : null,
|
|
1015
1330
|
createPaper: (papersRoot, name, format) =>
|
|
1016
1331
|
createPaperAt(papersRoot, name, format, { log, err, cwd }),
|
|
1017
|
-
tex: {
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
runToolchain({
|
|
1021
|
-
check: false,
|
|
1022
|
-
log,
|
|
1023
|
-
err,
|
|
1024
|
-
banal: hostBanalInstaller(),
|
|
1025
|
-
tex: toolchainTex(resolve(cwd, a.paths[0] ?? ".")),
|
|
1026
|
-
}),
|
|
1027
|
-
},
|
|
1028
|
-
resolveCliPapers: (root: string): string | null => {
|
|
1029
|
-
const read = readConfig(
|
|
1030
|
-
{ ...a, config: null },
|
|
1031
|
-
{ log: () => {}, err: () => {}, cwd: root },
|
|
1032
|
-
);
|
|
1033
|
-
return read.code === undefined
|
|
1034
|
-
? (toPaths(papersDirOf(read.opts))[0] ?? null)
|
|
1035
|
-
: null;
|
|
1036
|
-
},
|
|
1332
|
+
tex: initTexLive(a, { log, err, cwd }),
|
|
1333
|
+
resolveCliPapers: (root: string): string | null =>
|
|
1334
|
+
cliPapers({ ...a, config: null }, root),
|
|
1037
1335
|
});
|
|
1038
1336
|
}
|
|
1039
1337
|
|
|
@@ -1043,10 +1341,16 @@ export async function run(
|
|
|
1043
1341
|
log = console.log,
|
|
1044
1342
|
err = console.error,
|
|
1045
1343
|
cwd = process.cwd(),
|
|
1344
|
+
checkReferences = onlineReferences,
|
|
1046
1345
|
}: {
|
|
1047
1346
|
log?: typeof console.log;
|
|
1048
1347
|
err?: typeof console.error;
|
|
1049
1348
|
cwd?: string;
|
|
1349
|
+
/**
|
|
1350
|
+
* `build`'s online reference check — the real services by default. The e2e build passes a
|
|
1351
|
+
* fake: no test depends on Crossref, Semantic Scholar or DBLP answering.
|
|
1352
|
+
*/
|
|
1353
|
+
checkReferences?: CheckReferences;
|
|
1050
1354
|
} = {},
|
|
1051
1355
|
): Promise<number> {
|
|
1052
1356
|
const a = parseArgs(argv);
|
|
@@ -1061,6 +1365,12 @@ export async function run(
|
|
|
1061
1365
|
);
|
|
1062
1366
|
return 2;
|
|
1063
1367
|
}
|
|
1368
|
+
if (a.unknownFlag !== undefined) {
|
|
1369
|
+
err(
|
|
1370
|
+
`unknown flag \`${a.unknownFlag}\` — \`paperlint --help\` lists every flag`,
|
|
1371
|
+
);
|
|
1372
|
+
return 2;
|
|
1373
|
+
}
|
|
1064
1374
|
if (a.help || !a.cmd) {
|
|
1065
1375
|
log(USAGE);
|
|
1066
1376
|
return a.help ? 0 : 2;
|
|
@@ -1073,11 +1383,7 @@ export async function run(
|
|
|
1073
1383
|
// broken is precisely its job. So a failed read becomes "the CLI would lint nothing", which is
|
|
1074
1384
|
// what it prints, rather than an early exit that tells the reader nothing about the hooks.
|
|
1075
1385
|
if (a.cmd === "doctor") {
|
|
1076
|
-
const
|
|
1077
|
-
const papers =
|
|
1078
|
-
read.code === undefined
|
|
1079
|
-
? (toPaths(papersDirOf(read.opts))[0] ?? null)
|
|
1080
|
-
: null;
|
|
1386
|
+
const papers = cliPapers(a, cwd);
|
|
1081
1387
|
return doctor({
|
|
1082
1388
|
log,
|
|
1083
1389
|
cwd,
|
|
@@ -1086,7 +1392,7 @@ export async function run(
|
|
|
1086
1392
|
});
|
|
1087
1393
|
}
|
|
1088
1394
|
const simple = SIMPLE[a.cmd];
|
|
1089
|
-
if (simple) return await simple(a, { log, err, cwd });
|
|
1395
|
+
if (simple) return await simple(a, { log, err, cwd, checkReferences });
|
|
1090
1396
|
if (a.cmd === "check")
|
|
1091
1397
|
err(
|
|
1092
1398
|
`\`check\` is now \`lint\` — running it anyway. Update the call to \`paperlint lint\`.`,
|
|
@@ -1142,13 +1448,15 @@ export async function run(
|
|
|
1142
1448
|
rules: [...papers.value.preset, ...(opts.rules ?? []), ...papers.value.own],
|
|
1143
1449
|
};
|
|
1144
1450
|
|
|
1451
|
+
// Loaded lazily because only `lint` needs it. If the module cannot be loaded (a broken install:
|
|
1452
|
+
// the file missing from the package), lint still runs over the markdown papers rather than
|
|
1453
|
+
// failing outright — the LaTeX rules simply have no language to run in.
|
|
1145
1454
|
let texLanguage: unknown = null;
|
|
1146
1455
|
try {
|
|
1147
|
-
// @ts-expect-error — the module is .mjs and has no types
|
|
1148
|
-
// case here, it is caught by the catch below.
|
|
1456
|
+
// @ts-expect-error — the module is .mjs and has no types
|
|
1149
1457
|
({ texLanguage } = await import("../eslint-rules/latex-language.mjs"));
|
|
1150
1458
|
} catch {
|
|
1151
|
-
|
|
1459
|
+
texLanguage = null;
|
|
1152
1460
|
}
|
|
1153
1461
|
|
|
1154
1462
|
const eslint = new ESLint({
|
|
@@ -1161,7 +1469,7 @@ export async function run(
|
|
|
1161
1469
|
const unowned = await firstUnownedFile(eslint, paths);
|
|
1162
1470
|
if (unowned !== null) {
|
|
1163
1471
|
err(
|
|
1164
|
-
`${
|
|
1472
|
+
`${shown(cwd, unowned)} is not a file paperlint lints — it lints ${PAPER_FILE_PATTERNS.join(", ")}`,
|
|
1165
1473
|
);
|
|
1166
1474
|
return 2;
|
|
1167
1475
|
}
|
|
@@ -1188,7 +1496,7 @@ export async function run(
|
|
|
1188
1496
|
// is not invoked — and, not being invoked, it physically cannot report that.
|
|
1189
1497
|
if (results.length === 0) {
|
|
1190
1498
|
err(
|
|
1191
|
-
`nothing was linted under ${paths.map((x) =>
|
|
1499
|
+
`nothing was linted under ${paths.map((x) => shown(cwd, x)).join(", ")} — no PIPELINE-STATUS.md, paper.md/tex or reviews/ found there. A clean report over zero files is not a clean report.`,
|
|
1192
1500
|
);
|
|
1193
1501
|
return 1;
|
|
1194
1502
|
}
|
|
@@ -1199,6 +1507,11 @@ export async function run(
|
|
|
1199
1507
|
err,
|
|
1200
1508
|
where: relative(cwd, root) || ".",
|
|
1201
1509
|
opts: withPapers,
|
|
1510
|
+
// Every paper of the project, not only the ones on the command line (#103).
|
|
1511
|
+
projectPapers: papersRoots(cfg)
|
|
1512
|
+
.flatMap((r) => papersIn(r))
|
|
1513
|
+
.map((d) => join(d, MAIN))
|
|
1514
|
+
.filter((f) => existsSync(f)),
|
|
1202
1515
|
});
|
|
1203
1516
|
}
|
|
1204
1517
|
|
|
@@ -1232,7 +1545,7 @@ const isEmptySet = (e: unknown): boolean => {
|
|
|
1232
1545
|
return (
|
|
1233
1546
|
fail?.messageTemplate === "file-not-found" ||
|
|
1234
1547
|
fail?.messageTemplate === "all-matched-files-ignored" ||
|
|
1235
|
-
/No files matching/i.test(fail?.message
|
|
1548
|
+
/No files matching/i.test(String(fail?.message))
|
|
1236
1549
|
);
|
|
1237
1550
|
};
|
|
1238
1551
|
|
|
@@ -1251,22 +1564,24 @@ async function reportLint(
|
|
|
1251
1564
|
err,
|
|
1252
1565
|
where,
|
|
1253
1566
|
opts,
|
|
1567
|
+
projectPapers,
|
|
1254
1568
|
}: {
|
|
1255
1569
|
a: Args;
|
|
1256
1570
|
log: typeof console.log;
|
|
1257
1571
|
err: typeof console.error;
|
|
1258
1572
|
where: string;
|
|
1259
1573
|
opts: PaperlintConfig;
|
|
1574
|
+
projectPapers: readonly string[];
|
|
1260
1575
|
},
|
|
1261
1576
|
): Promise<number> {
|
|
1262
1577
|
const silent = await silentOptionalRules(
|
|
1263
1578
|
eslint,
|
|
1264
|
-
results.map((r) => r.filePath),
|
|
1579
|
+
[...results.map((r) => r.filePath), ...projectPapers],
|
|
1265
1580
|
opts,
|
|
1266
1581
|
);
|
|
1267
1582
|
if (silent.length > 0) {
|
|
1268
1583
|
err(
|
|
1269
|
-
`${silent.join(", ")} is turned on in "rules", but no
|
|
1584
|
+
`${silent.join(", ")} is turned on in "rules", but no paper.tex of the project gets it — check the block's ` +
|
|
1270
1585
|
`"files" (relative to ${where}). A rule that never runs reports exactly like a rule that passed.`,
|
|
1271
1586
|
);
|
|
1272
1587
|
return 1;
|