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/dist/cli.js
CHANGED
|
@@ -45,10 +45,10 @@ import { paperRules, stringFields, } from "./paper-settings.js";
|
|
|
45
45
|
import { referenceRules, REFERENCE_RULE_LEVELS } from "./reference-rules.js";
|
|
46
46
|
import { onlineReferences } from "./adapters/references/index.js";
|
|
47
47
|
import { narrowToOwners, ownedPatterns, ruleOwners, scopeToOwned, } from "./paper-files.js";
|
|
48
|
-
import { paperPreset,
|
|
48
|
+
import { paperPreset, settingsProblemLine, presetProblemText, resolvePreset, shippedPresets, SHIPPED_PREFIX, } from "./presets.js";
|
|
49
49
|
import { mergeRequirements, declaredUnion, requirementsFor, NO_REQUIREMENTS, } from "./tex-requirements.js";
|
|
50
50
|
import { doctor } from "./doctor.js";
|
|
51
|
-
import { init, processInteractivity,
|
|
51
|
+
import { askOnTerminal, askOrDefault, init, processInteractivity, } from "./init.js";
|
|
52
52
|
import { DEFAULT_FORMAT, FORMATS, isFormat, newPaper, reportNewPaper, } from "./new-paper.js";
|
|
53
53
|
// The one source for the consumer's config key lives in the .mjs half of the package (the ESLint
|
|
54
54
|
// rules and the skill scripts import it too); its types are in lib/paper-config.d.mts.
|
|
@@ -72,12 +72,14 @@ import reviewFrontmatter from "../eslint-rules/review-frontmatter.mjs";
|
|
|
72
72
|
import siblingFrontmatter from "../eslint-rules/sibling-frontmatter.mjs";
|
|
73
73
|
// @ts-expect-error — an ESLint rule in .mjs, it has no types
|
|
74
74
|
import pdfRules from "../eslint-rules/pdf-last-page-balance.mjs";
|
|
75
|
+
/** The shipped venue presets, read from the package's venues directory — the one list. */
|
|
76
|
+
const SHIPPED_VENUES = () => shippedPresets(packageVenuesDir());
|
|
75
77
|
const USAGE = `paperlint — machine-checkable gates for a paper kept in git
|
|
76
78
|
|
|
77
79
|
npx paperlint init [dir] set the project up: detect the papers directory, declare it
|
|
78
80
|
in package.json, link the skills, wire the hooks into
|
|
79
81
|
.claude/settings.json, offer the CI step, report what is missing
|
|
80
|
-
npx paperlint new <name> [--format tex|md]
|
|
82
|
+
npx paperlint new <name> [--venue <preset>] [--kind <kind>] [--format tex|md]
|
|
81
83
|
create <papers>/<name>/ from the template; never overwrites,
|
|
82
84
|
on an existing folder adds only the missing files, then lints it
|
|
83
85
|
npx paperlint lint [paths…] run every rule over your papers
|
|
@@ -105,6 +107,15 @@ init:
|
|
|
105
107
|
--paper <name> create this paper too (without a human, the only way init creates one)
|
|
106
108
|
--format tex|md the new paper's source format; default tex
|
|
107
109
|
|
|
110
|
+
new:
|
|
111
|
+
--venue <preset> the venue preset, written as "extends" into the paper's paperlint.json:
|
|
112
|
+
a shipped one (${SHIPPED_VENUES().join(", ")}), or a path to your
|
|
113
|
+
own preset starting with ./ or ../, relative to where you run the command.
|
|
114
|
+
On a terminal without --venue, new asks; "none" leaves it unset
|
|
115
|
+
--kind <kind> the paper's kind at that venue (its page limit), e.g. short — one of the
|
|
116
|
+
preset's kinds; needs --venue
|
|
117
|
+
--format tex|md the paper's source format; default tex
|
|
118
|
+
|
|
108
119
|
lint:
|
|
109
120
|
npx paperlint lint [paths…] [--fix] [--config <file.json>] [--json]
|
|
110
121
|
|
|
@@ -137,7 +148,7 @@ settings — paperlint.json, at two levels, one schema. Both are optional.
|
|
|
137
148
|
|
|
138
149
|
{ "extends": "paperlint:aisec", "kind": "research", "rules": { "pdf/last-page-balance": "error" } }
|
|
139
150
|
|
|
140
|
-
"extends" names a venue preset: paperlint:<name> (shipped:
|
|
151
|
+
"extends" names a venue preset: paperlint:<name> (shipped: ${SHIPPED_VENUES().join(", ")})
|
|
141
152
|
or ./path.jsonc, relative to the paperlint.json. npm presets are not supported yet.
|
|
142
153
|
|
|
143
154
|
"rules" is { "<rule>": "<severity>" } for every paper file in scope, or ESLint flat-config
|
|
@@ -260,6 +271,54 @@ export function buildConfig(opts = {}, texLanguage) {
|
|
|
260
271
|
* config rather than listed again. `@eslint/markdown` is a dependency's plugin, not paperlint's.
|
|
261
272
|
*/
|
|
262
273
|
export const SHIPPED_RULES = shippedRuleIds(buildConfig({}, { sentinel: "tex language" }), [markdown]);
|
|
274
|
+
/**
|
|
275
|
+
* Every plugin paperlint's config registers, ONE object per name, read off that config. In it one
|
|
276
|
+
* name is bound to different objects in different blocks (`paper` beside `PIPELINE-STATUS.md` is
|
|
277
|
+
* not `paper` beside `paper.tex`); here their rules are merged, so a config that lints any paper
|
|
278
|
+
* file knows every rule id. `@eslint/markdown` is a dependency's plugin and is left out. The LaTeX
|
|
279
|
+
* language rides on `tex` when `texLanguage` is given, and that is how a consumer registers it: a
|
|
280
|
+
* second `tex` plugin of its own is refused by ESLint ("Cannot redefine plugin "tex"", measured
|
|
281
|
+
* with and without the language here). Without it, `tex` carries rules only — for a config that
|
|
282
|
+
* lints markdown papers alone.
|
|
283
|
+
*/
|
|
284
|
+
export function rulePlugins(texLanguage) {
|
|
285
|
+
const out = {};
|
|
286
|
+
for (const block of buildConfig({}, texLanguage ?? { sentinel: "tex" }))
|
|
287
|
+
for (const [name, plugin] of Object.entries(block.plugins ?? {}))
|
|
288
|
+
if (plugin !== markdown)
|
|
289
|
+
out[name] = { rules: { ...out[name]?.rules, ...plugin.rules } };
|
|
290
|
+
if (out["tex"] && texLanguage !== undefined)
|
|
291
|
+
return {
|
|
292
|
+
...out,
|
|
293
|
+
tex: { ...out["tex"], languages: { latex: texLanguage } },
|
|
294
|
+
};
|
|
295
|
+
return out;
|
|
296
|
+
}
|
|
297
|
+
/**
|
|
298
|
+
* A flat-config fragment for a consumer's OWN ESLint run over paper files (#104): every rule
|
|
299
|
+
* paperlint ships, registered and turned off. Without it a `% eslint-disable-next-line
|
|
300
|
+
* paper/leading-zero -- why` — the documented escape hatch — fails that run with "Definition for
|
|
301
|
+
* rule 'paper/leading-zero' was not found". Off, because `paperlint lint` is where they run.
|
|
302
|
+
*
|
|
303
|
+
* The second block is not optional, and it is measured: a directive naming a rule that is off
|
|
304
|
+
* suppresses nothing, so ESLint reports it as an unused directive (a warning by default, an error
|
|
305
|
+
* under `reportUnusedDisableDirectives: "error"`). On the files paperlint lints that report is
|
|
306
|
+
* switched off in THIS run — `paperlint lint` still reports a directive that silences nothing.
|
|
307
|
+
*/
|
|
308
|
+
export function rulesOff(texLanguage) {
|
|
309
|
+
return [
|
|
310
|
+
{
|
|
311
|
+
name: "paperlint/rules-off",
|
|
312
|
+
plugins: rulePlugins(texLanguage),
|
|
313
|
+
rules: Object.fromEntries([...SHIPPED_RULES].map((id) => [id, "off"])),
|
|
314
|
+
},
|
|
315
|
+
{
|
|
316
|
+
name: "paperlint/rules-off/directives",
|
|
317
|
+
files: PAPER_FILE_PATTERNS,
|
|
318
|
+
linterOptions: { reportUnusedDisableDirectives: "off" },
|
|
319
|
+
},
|
|
320
|
+
];
|
|
321
|
+
}
|
|
263
322
|
/** Whether a rule entry (`"error"`, `2`, `["warn", {…}]`) turns the rule on. */
|
|
264
323
|
const isOn = (entry) => {
|
|
265
324
|
const sev = Array.isArray(entry) ? entry[0] : entry;
|
|
@@ -274,18 +333,24 @@ export const OPTIONAL_RULES = new Set([...SHIPPED_RULES].filter((id) => !buildCo
|
|
|
274
333
|
* 🔴 AN OPTIONAL RULE THAT IS ON AND REACHES NO PAPER IS A GREEN ZERO. A `files` glob that matches
|
|
275
334
|
* nothing — a typo, a path relative to the wrong directory — leaves the rule never invoked, and a
|
|
276
335
|
* rule that never runs reports exactly like a rule that passed. So for every optional rule the
|
|
277
|
-
* consumer turned on, some
|
|
336
|
+
* consumer turned on, some `paper.tex` must actually have it enabled; the ones none has are
|
|
278
337
|
* returned, for the caller to refuse.
|
|
338
|
+
*
|
|
339
|
+
* `files` is every `paper.tex` the guard may count: the ones this run linted AND every paper of the
|
|
340
|
+
* project. Judged against the run alone, `paperlint lint papers/c` failed on a block written for
|
|
341
|
+
* papers a and b (#103) — a block that reaches a paper outside this run is not dead. A glob that
|
|
342
|
+
* reaches no paper of the project at all still fails, from a subset as from the whole.
|
|
279
343
|
*/
|
|
280
|
-
export async function silentOptionalRules(eslint,
|
|
344
|
+
export async function silentOptionalRules(eslint, files, opts) {
|
|
281
345
|
const turnedOn = new Set((opts.rules ?? []).flatMap((b) => Object.entries(b.rules)
|
|
282
346
|
.filter(([id, e]) => OPTIONAL_RULES.has(id) && isOn(e))
|
|
283
347
|
.map(([id]) => id)));
|
|
284
348
|
const reached = new Set();
|
|
285
|
-
for (const f of
|
|
349
|
+
for (const f of new Set(files.filter((p) => basename(p) === MAIN))) {
|
|
350
|
+
// Undefined for a file outside ESLint's cwd or scope: it reaches nothing.
|
|
286
351
|
const cfg = (await eslint.calculateConfigForFile(f));
|
|
287
352
|
for (const id of turnedOn)
|
|
288
|
-
if (isOn(cfg
|
|
353
|
+
if (isOn(cfg?.rules?.[id]))
|
|
289
354
|
reached.add(id);
|
|
290
355
|
}
|
|
291
356
|
return [...turnedOn].filter((id) => !reached.has(id));
|
|
@@ -303,8 +368,8 @@ export function paperRuleBlocks(paths) {
|
|
|
303
368
|
const out = { preset: [], own: [] };
|
|
304
369
|
for (const dir of papers) {
|
|
305
370
|
const p = paperPreset(dir, PRESET_DEPS);
|
|
306
|
-
if (p.kind === "settings-problem"
|
|
307
|
-
return { ok: false, error:
|
|
371
|
+
if (p.kind === "settings-problem")
|
|
372
|
+
return { ok: false, error: settingsProblemLine(dir, p.problem) };
|
|
308
373
|
const blocks = rulesOfPaper(dir, p);
|
|
309
374
|
if (!blocks.ok)
|
|
310
375
|
return blocks;
|
|
@@ -319,7 +384,7 @@ export function paperRuleBlocks(paths) {
|
|
|
319
384
|
* contributes nothing here; `pdf/profile` reports it on the paper.
|
|
320
385
|
*/
|
|
321
386
|
function rulesOfPaper(dir, p) {
|
|
322
|
-
const
|
|
387
|
+
const { settings } = p;
|
|
323
388
|
const fromPreset = p.kind === "resolved"
|
|
324
389
|
? parseRuleEntries(p.preset.rules, `the venue preset ${p.preset.chain.join(" → ")} → "rules"`, SHIPPED_RULES)
|
|
325
390
|
: { ok: true, value: {} };
|
|
@@ -332,30 +397,27 @@ function rulesOfPaper(dir, p) {
|
|
|
332
397
|
: ownRules(dir, settings);
|
|
333
398
|
if (!own.ok)
|
|
334
399
|
return own;
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
400
|
+
// No `files`: `narrowToOwners` scopes the block to what paperlint lints under `dir`, exceptions
|
|
401
|
+
// included. A copy of the globs here is how the siblings index got parsed as JavaScript (#101).
|
|
402
|
+
const scoped = (rules) => Object.keys(rules).length ? [{ basePath: dir, rules }] : [];
|
|
338
403
|
return {
|
|
339
404
|
ok: true,
|
|
340
405
|
value: { preset: scoped(fromPreset.value), own: [...own.value] },
|
|
341
406
|
};
|
|
342
407
|
}
|
|
343
|
-
/** A paper's `{ id: severity }` rules, as one block over the paper's files. */
|
|
408
|
+
/** A paper's `{ id: severity }` rules, as one block over the paper's files (narrowed later, #101). */
|
|
344
409
|
function ownRules(dir, settings) {
|
|
345
410
|
const own = paperRules(dir, settings, SHIPPED_RULES);
|
|
346
411
|
if (!own.ok)
|
|
347
412
|
return own;
|
|
348
413
|
return {
|
|
349
414
|
ok: true,
|
|
350
|
-
value: own.value
|
|
351
|
-
? [{ basePath: dir, files: PAPER_FILE_PATTERNS, rules: own.value }]
|
|
352
|
-
: [],
|
|
415
|
+
value: own.value ? [{ basePath: dir, rules: own.value }] : [],
|
|
353
416
|
};
|
|
354
417
|
}
|
|
355
418
|
/**
|
|
356
|
-
* The
|
|
357
|
-
*
|
|
358
|
-
* language — `paperlint.json` itself would be parsed as JavaScript.
|
|
419
|
+
* The globs paperlint lints, for the refusal message only. Which files a block reaches is decided by
|
|
420
|
+
* `narrowToOwners` from `ownedScopes` (src/paper-files.ts), where each glob keeps its exceptions.
|
|
359
421
|
*/
|
|
360
422
|
const PAPER_FILE_PATTERNS = ownedPatterns(buildConfig({}, { sentinel: "tex language" }));
|
|
361
423
|
/**
|
|
@@ -390,6 +452,8 @@ export function parseSettings(json, where, baseDir) {
|
|
|
390
452
|
* papers inside the config's directory, that is the config's directory itself.
|
|
391
453
|
*/
|
|
392
454
|
const lintRoot = (home, paths) => commonDir([home, ...paths]);
|
|
455
|
+
/** A path as the reader typed it: relative to where the command runs, or whole when it IS there. */
|
|
456
|
+
const shown = (cwd, p) => relative(cwd, p) || p;
|
|
393
457
|
/** The longest shared leading run of path segments. */
|
|
394
458
|
const commonDir = (paths) => {
|
|
395
459
|
const [first = [], ...rest] = paths.map((p) => p.split(sep));
|
|
@@ -412,30 +476,47 @@ export function parseArgs(argv) {
|
|
|
412
476
|
noHooks: false,
|
|
413
477
|
paper: null,
|
|
414
478
|
format: null,
|
|
479
|
+
venue: null,
|
|
480
|
+
kind: null,
|
|
415
481
|
hooksMode: null,
|
|
416
482
|
// -1 = warnings NEVER fail the run. In this set most findings are advisory by design, and a
|
|
417
483
|
// gate that fails on advice gets muted entirely.
|
|
418
484
|
maxWarnings: -1,
|
|
419
485
|
};
|
|
486
|
+
// Consumed from the front: a value flag takes the next argument by shifting it off.
|
|
420
487
|
const rest = [...argv];
|
|
421
|
-
|
|
422
|
-
|
|
488
|
+
const first = rest[0];
|
|
489
|
+
if (first && !first.startsWith("-")) {
|
|
490
|
+
out.cmd = first;
|
|
491
|
+
rest.shift();
|
|
492
|
+
}
|
|
423
493
|
// 🔴 A FLAG WHOSE VALUE WAS TAKEN AWAY IS A REFUSAL, NOT A DEFAULT. The compiler found this
|
|
424
|
-
// during the move to TypeScript:
|
|
494
|
+
// during the move to TypeScript: the value past the last argument is `undefined`, and
|
|
425
495
|
// `paperlint lint --config` (the value forgotten, or eaten by a substitution in CI) silently turned
|
|
426
496
|
// into "no config given" — that is, it went to auto-discovery and linted against SOMEONE ELSE'S
|
|
427
497
|
// file, saying nothing. The failure is one-sided and toward silence, so it is cured by
|
|
428
498
|
// behaviour, not by a type cast.
|
|
429
|
-
const valueFor = (flag
|
|
430
|
-
const v = rest
|
|
499
|
+
const valueFor = (flag) => {
|
|
500
|
+
const v = rest.shift();
|
|
431
501
|
if (v === undefined)
|
|
432
502
|
out.missingValue = flag;
|
|
433
503
|
return v;
|
|
434
504
|
};
|
|
435
|
-
for (let
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
505
|
+
for (let arg = rest.shift(); arg !== undefined; arg = rest.shift()) {
|
|
506
|
+
// `--flag=value` is `--flag value` for every flag that takes a value: the CI action writes
|
|
507
|
+
// `--max-warnings="$N"`, and that form used to fall through to the list of paths.
|
|
508
|
+
const eq = arg.indexOf("=");
|
|
509
|
+
const inline = arg.startsWith("--") && eq > 0 && VALUE_FLAGS.has(arg.slice(0, eq))
|
|
510
|
+
? arg.slice(eq + 1)
|
|
511
|
+
: undefined;
|
|
512
|
+
const a = inline === undefined ? arg : arg.slice(0, eq);
|
|
513
|
+
const take = () => {
|
|
514
|
+
if (inline === undefined)
|
|
515
|
+
return valueFor(a);
|
|
516
|
+
if (inline === "")
|
|
517
|
+
out.missingValue = a;
|
|
518
|
+
return inline === "" ? undefined : inline;
|
|
519
|
+
};
|
|
439
520
|
if (a === "--json")
|
|
440
521
|
out.json = true;
|
|
441
522
|
else if (a === "--fix")
|
|
@@ -453,34 +534,51 @@ export function parseArgs(argv) {
|
|
|
453
534
|
else if (a.startsWith("--hooks="))
|
|
454
535
|
out.hooksMode = a.slice("--hooks=".length);
|
|
455
536
|
else if (a === "--paper")
|
|
456
|
-
out.paper =
|
|
537
|
+
out.paper = take() ?? null;
|
|
457
538
|
else if (a === "--format")
|
|
458
|
-
out.format =
|
|
539
|
+
out.format = take() ?? null;
|
|
540
|
+
else if (a === "--venue")
|
|
541
|
+
out.venue = take() ?? null;
|
|
542
|
+
else if (a === "--kind")
|
|
543
|
+
out.kind = take() ?? null;
|
|
459
544
|
// `--options` was the first spelling and is kept working. It named the wrong thing — every
|
|
460
545
|
// other tool in the stack calls this file its config — but a flag in someone's CI is not
|
|
461
546
|
// ours to break.
|
|
462
547
|
else if (a === "--config" || a === "--options")
|
|
463
|
-
out.config =
|
|
548
|
+
out.config = take() ?? null;
|
|
464
549
|
else if (a === "--max-warnings") {
|
|
465
|
-
const v =
|
|
550
|
+
const v = take();
|
|
466
551
|
if (v !== undefined)
|
|
467
552
|
out.maxWarnings = Number(v);
|
|
468
553
|
}
|
|
469
554
|
else if (a === "--help" || a === "-h")
|
|
470
555
|
out.help = true;
|
|
556
|
+
// An unknown flag is refused by name. Read as a path, it silently lints something else.
|
|
557
|
+
else if (a.startsWith("-") && a !== "-")
|
|
558
|
+
out.unknownFlag ??= a;
|
|
471
559
|
else
|
|
472
560
|
out.paths.push(a);
|
|
473
561
|
}
|
|
474
562
|
return out;
|
|
475
563
|
}
|
|
564
|
+
/** The flags that take a value, in either spelling: `--flag value` or `--flag=value`. */
|
|
565
|
+
const VALUE_FLAGS = new Set([
|
|
566
|
+
"--paper",
|
|
567
|
+
"--format",
|
|
568
|
+
"--venue",
|
|
569
|
+
"--kind",
|
|
570
|
+
"--config",
|
|
571
|
+
"--options",
|
|
572
|
+
"--max-warnings",
|
|
573
|
+
]);
|
|
476
574
|
/**
|
|
477
575
|
* This package's own version, from the `package.json` beside `src/` and `dist/` alike. `init` pins
|
|
478
576
|
* the CI action to its release tag; an unreadable manifest yields `undefined`, and init then keeps
|
|
479
577
|
* the placeholder instead of guessing.
|
|
480
578
|
*/
|
|
481
|
-
function ownVersion() {
|
|
579
|
+
export function ownVersion(readManifest = () => readFileSync(new URL("../package.json", import.meta.url), "utf8")) {
|
|
482
580
|
try {
|
|
483
|
-
const v = JSON.parse(
|
|
581
|
+
const v = JSON.parse(readManifest())?.version;
|
|
484
582
|
return typeof v === "string" ? v : undefined;
|
|
485
583
|
}
|
|
486
584
|
catch {
|
|
@@ -524,8 +622,9 @@ export function readConfig(a, { log = console.log, err = console.error, cwd = pr
|
|
|
524
622
|
//
|
|
525
623
|
// 🔴 IN `--json` MODE — TO stderr. Machine output must be ONE parsable document: a line before
|
|
526
624
|
// the array breaks any `| jq`, and it breaks it for the consumer, not for us.
|
|
527
|
-
|
|
528
|
-
|
|
625
|
+
const shownPath = shown(cwd, configPath);
|
|
626
|
+
(a.json ? err : log)(`config: ${shownPath}`);
|
|
627
|
+
const settings = parseSettings(parsed, shownPath, root);
|
|
529
628
|
if (!settings.ok) {
|
|
530
629
|
err(settings.error);
|
|
531
630
|
return { code: 2 };
|
|
@@ -617,16 +716,90 @@ resolve = (spec) => createRequire(import.meta.url).resolve(spec), } = {}) {
|
|
|
617
716
|
* become two implementations. The lint runs on THAT folder, so the first thing printed after the
|
|
618
717
|
* file list is its verdict rather than the old "missing PIPELINE-STATUS.md".
|
|
619
718
|
*/
|
|
620
|
-
export async function createPaperAt(papersRoot, name, format, { log, err, cwd, }) {
|
|
621
|
-
const result = newPaper(papersRoot, name, format);
|
|
622
|
-
const here = (p) =>
|
|
719
|
+
export async function createPaperAt(papersRoot, name, format, { log, err, cwd, venue = null, }) {
|
|
720
|
+
const result = newPaper(papersRoot, name, format, { venue });
|
|
721
|
+
const here = (p) => shown(cwd, p);
|
|
623
722
|
for (const line of reportNewPaper(result, here))
|
|
624
723
|
log(line);
|
|
625
724
|
if (!result.ok)
|
|
626
725
|
return 2;
|
|
726
|
+
const config = here(join(result.dir, CONFIG_FILE));
|
|
727
|
+
if (venue === null)
|
|
728
|
+
log(` venue: none yet — set "extends" in ${config}, or next time: paperlint new <name> --venue <preset> (${SHIPPED_VENUES().join(", ")})`);
|
|
729
|
+
else if (venue.kind === null && venue.kinds.length > 0)
|
|
730
|
+
log(` kind: not set — \`${venue.label}\` sets a page limit per kind (${venue.kinds.join(", ")}); add "kind" to ${config}. Until then lint reports pdf/profile`);
|
|
627
731
|
log(``);
|
|
628
732
|
return run(["lint", result.dir], { log, err, cwd });
|
|
629
733
|
}
|
|
734
|
+
/**
|
|
735
|
+
* `--venue` / `--kind` → the `extends` and `kind` to write into `<paperDir>/paperlint.json`, or why
|
|
736
|
+
* not. Parsed here, at the boundary, and resolved through the same `resolvePreset` lint uses — so a
|
|
737
|
+
* venue `new` accepts is a venue lint resolves. On a terminal with no `--venue` it asks, with
|
|
738
|
+
* "none" as the default; without a terminal it chooses nothing (null), as before.
|
|
739
|
+
*
|
|
740
|
+
* A path is relative to where the command runs, because that is where it was typed; it is written
|
|
741
|
+
* relative to the paper's `paperlint.json`, because that is what `extends` is relative to. Written
|
|
742
|
+
* as typed, `./venues/x.jsonc` would name `<paper>/venues/x.jsonc`.
|
|
743
|
+
*/
|
|
744
|
+
export async function chooseVenue(flags, { paperDir, cwd, interactive, ask, }) {
|
|
745
|
+
const shipped = SHIPPED_VENUES();
|
|
746
|
+
if (flags.kind !== null && flags.venue === null)
|
|
747
|
+
return bad(`--kind needs --venue: a kind is a page limit of one venue preset — \`paperlint new <name> --venue <preset> --kind ${flags.kind}\``);
|
|
748
|
+
const asked = async (q) => (await askOrDefault(ask, q))?.trim() ?? "";
|
|
749
|
+
const venue = flags.venue ??
|
|
750
|
+
(interactive
|
|
751
|
+
? await asked(`venue: ${[...shipped, "none"].join(" / ")} [none] `)
|
|
752
|
+
: "");
|
|
753
|
+
if (venue === "" || (flags.venue === null && venue === "none"))
|
|
754
|
+
return { ok: true, value: null };
|
|
755
|
+
const spec = venueSpec(venue, { shipped, paperDir, cwd });
|
|
756
|
+
if (!spec.ok)
|
|
757
|
+
return spec;
|
|
758
|
+
const r = resolvePreset(spec.value, join(paperDir, CONFIG_FILE), PRESET_DEPS);
|
|
759
|
+
if (!r.ok)
|
|
760
|
+
return bad(`--venue ${venue}: ${presetProblemText(r.error)}`);
|
|
761
|
+
const kinds = [...r.value.format.kinds.keys()];
|
|
762
|
+
const label = r.value.label;
|
|
763
|
+
if (flags.kind !== null) {
|
|
764
|
+
if (kinds.length === 0)
|
|
765
|
+
return bad(`--kind ${flags.kind}: \`${label}\` has no kinds — it sets no page limit, so there is no kind to choose`);
|
|
766
|
+
if (!kinds.includes(flags.kind))
|
|
767
|
+
return bad(`--kind ${flags.kind}: \`${label}\` has no kind \`${flags.kind}\`; its kinds: ${kinds.join(", ")}`);
|
|
768
|
+
}
|
|
769
|
+
const kind = flags.kind ??
|
|
770
|
+
(interactive && kinds.length > 0
|
|
771
|
+
? await asked(`kind: ${[...kinds, "later"].join(" / ")} [later] `)
|
|
772
|
+
: null);
|
|
773
|
+
return {
|
|
774
|
+
ok: true,
|
|
775
|
+
value: {
|
|
776
|
+
extends: spec.value,
|
|
777
|
+
kind: kind !== null && kinds.includes(kind) ? kind : null,
|
|
778
|
+
label,
|
|
779
|
+
kinds,
|
|
780
|
+
},
|
|
781
|
+
};
|
|
782
|
+
}
|
|
783
|
+
const bad = (error) => ({
|
|
784
|
+
ok: false,
|
|
785
|
+
error,
|
|
786
|
+
});
|
|
787
|
+
/** What `--venue` names → the `extends` value, relative to `<paperDir>/paperlint.json`. */
|
|
788
|
+
function venueSpec(venue, { shipped, paperDir, cwd, }) {
|
|
789
|
+
if (venue.startsWith("./") || venue.startsWith("../")) {
|
|
790
|
+
const file = resolve(cwd, venue);
|
|
791
|
+
if (!existsSync(file))
|
|
792
|
+
return bad(`--venue ${venue}: no such file (${file}) — a path is relative to where you run the command`);
|
|
793
|
+
const rel = relative(paperDir, file).split(sep).join("/");
|
|
794
|
+
return { ok: true, value: rel.startsWith("../") ? rel : `./${rel}` };
|
|
795
|
+
}
|
|
796
|
+
const name = venue.startsWith(SHIPPED_PREFIX)
|
|
797
|
+
? venue.slice(SHIPPED_PREFIX.length)
|
|
798
|
+
: venue;
|
|
799
|
+
if (!shipped.includes(name))
|
|
800
|
+
return bad(`--venue ${venue}: no such venue preset. Shipped: ${shipped.join(", ")} — or a path to your own preset, starting with ./ or ../`);
|
|
801
|
+
return { ok: true, value: `${SHIPPED_PREFIX}${name}` };
|
|
802
|
+
}
|
|
630
803
|
/**
|
|
631
804
|
* `paperlint new <name>` — the papers directory comes from the same declaration every other command
|
|
632
805
|
* reads; there is no second way to name it. See `new-paper.ts` for what it writes and why.
|
|
@@ -634,7 +807,7 @@ export async function createPaperAt(papersRoot, name, format, { log, err, cwd, }
|
|
|
634
807
|
async function runNew(a, { log, err, cwd, ask = askOnTerminal, }) {
|
|
635
808
|
const [name, ...extra] = a.paths;
|
|
636
809
|
if (!name || extra.length > 0) {
|
|
637
|
-
err(`\`new\` takes exactly one paper name: \`paperlint new my-paper [--format tex|md]\``);
|
|
810
|
+
err(`\`new\` takes exactly one paper name: \`paperlint new my-paper [--venue <preset>] [--kind <kind>] [--format tex|md]\``);
|
|
638
811
|
return 2;
|
|
639
812
|
}
|
|
640
813
|
let format = DEFAULT_FORMAT;
|
|
@@ -646,9 +819,9 @@ async function runNew(a, { log, err, cwd, ask = askOnTerminal, }) {
|
|
|
646
819
|
format = a.format;
|
|
647
820
|
}
|
|
648
821
|
else if (processInteractivity(a.yes).interactive) {
|
|
649
|
-
const f = await ask
|
|
650
|
-
if (isFormat(f
|
|
651
|
-
format = f
|
|
822
|
+
const f = (await askOrDefault(ask, `format: tex / md [${DEFAULT_FORMAT}] `))?.trim();
|
|
823
|
+
if (isFormat(f))
|
|
824
|
+
format = f;
|
|
652
825
|
}
|
|
653
826
|
const cfg = readConfig({ ...a, json: false }, { log: () => { }, err, cwd });
|
|
654
827
|
if (cfg.code !== undefined)
|
|
@@ -660,8 +833,30 @@ async function runNew(a, { log, err, cwd, ask = askOnTerminal, }) {
|
|
|
660
833
|
return 2;
|
|
661
834
|
}
|
|
662
835
|
if (roots.length > 1)
|
|
663
|
-
log(`several papers directories are declared — using the first: ${
|
|
664
|
-
|
|
836
|
+
log(`several papers directories are declared — using the first: ${shown(cwd, papersRoot)}`);
|
|
837
|
+
const paperDir = join(papersRoot, name);
|
|
838
|
+
// An existing paperlint.json is never overwritten, so a venue for it is refused, not dropped.
|
|
839
|
+
const hasConfig = existsSync(join(paperDir, CONFIG_FILE));
|
|
840
|
+
if (hasConfig && (a.venue !== null || a.kind !== null)) {
|
|
841
|
+
err(`${relative(cwd, join(paperDir, CONFIG_FILE))} already exists and is never overwritten — set "extends" and "kind" in it by hand`);
|
|
842
|
+
return 2;
|
|
843
|
+
}
|
|
844
|
+
const venue = hasConfig
|
|
845
|
+
? { ok: true, value: null }
|
|
846
|
+
: await chooseVenue(a, {
|
|
847
|
+
paperDir,
|
|
848
|
+
cwd,
|
|
849
|
+
interactive: processInteractivity(a.yes).interactive,
|
|
850
|
+
ask,
|
|
851
|
+
});
|
|
852
|
+
if (!venue.ok)
|
|
853
|
+
return (err(venue.error), 2);
|
|
854
|
+
return createPaperAt(papersRoot, name, format, {
|
|
855
|
+
log,
|
|
856
|
+
err,
|
|
857
|
+
cwd,
|
|
858
|
+
venue: venue.value,
|
|
859
|
+
});
|
|
665
860
|
}
|
|
666
861
|
/**
|
|
667
862
|
* 🔴 THE TARGET IS NAMED, "EVERYTHING" IS AN OPTION. That is how it is for everyone whose build is
|
|
@@ -670,7 +865,7 @@ async function runNew(a, { log, err, cwd, ask = askOnTerminal, }) {
|
|
|
670
865
|
* everything" on a corpus of five papers is twenty pdflatex runs instead of one, and almost never
|
|
671
866
|
* what was wanted.
|
|
672
867
|
*/
|
|
673
|
-
async function runBuild(a, { log, err, cwd, }) {
|
|
868
|
+
async function runBuild(a, { log, err, cwd, checkReferences, }) {
|
|
674
869
|
const cfg = readConfig(a, { log, err, cwd });
|
|
675
870
|
if (cfg.code !== undefined)
|
|
676
871
|
return cfg.code;
|
|
@@ -698,7 +893,7 @@ async function runBuild(a, { log, err, cwd, }) {
|
|
|
698
893
|
cwd,
|
|
699
894
|
dryRun: a.dryRun,
|
|
700
895
|
log,
|
|
701
|
-
checkReferences
|
|
896
|
+
checkReferences,
|
|
702
897
|
engine: () => engineEnv(targets, a, { log, err }),
|
|
703
898
|
});
|
|
704
899
|
if (out.kind === "no-engine")
|
|
@@ -782,6 +977,33 @@ function hostBanalInstaller() {
|
|
|
782
977
|
});
|
|
783
978
|
return banalInstaller({ ...ports, download }, s);
|
|
784
979
|
}
|
|
980
|
+
/**
|
|
981
|
+
* init's TeX Live step: whether paperlint's own tree is installed, and how to install it —
|
|
982
|
+
* `runToolchain` over what this project's papers need. `toolchain` is the seam a test replaces:
|
|
983
|
+
* the real one downloads ~270 MB.
|
|
984
|
+
*/
|
|
985
|
+
export function initTexLive(a, { log, err, cwd, }, toolchain = runToolchain) {
|
|
986
|
+
return {
|
|
987
|
+
installed: () => cachedTree(cacheRoot(process.env)) !== null,
|
|
988
|
+
install: () => toolchain({
|
|
989
|
+
check: false,
|
|
990
|
+
log,
|
|
991
|
+
err,
|
|
992
|
+
banal: hostBanalInstaller(),
|
|
993
|
+
tex: toolchainTex(resolve(cwd, a.paths[0] ?? ".")),
|
|
994
|
+
}),
|
|
995
|
+
};
|
|
996
|
+
}
|
|
997
|
+
/**
|
|
998
|
+
* The first papers directory the CLI would lint from `cwd`, or null when the config does not read
|
|
999
|
+
* or names none — what `doctor` (and `init`, through it) compares the hooks' directory against.
|
|
1000
|
+
*/
|
|
1001
|
+
function cliPapers(a, cwd) {
|
|
1002
|
+
const read = readConfig(a, { log: () => { }, err: () => { }, cwd });
|
|
1003
|
+
return read.code === undefined
|
|
1004
|
+
? (toPaths(papersDirOf(read.opts))[0] ?? null)
|
|
1005
|
+
: null;
|
|
1006
|
+
}
|
|
785
1007
|
/** `paperlint init`: its flags checked here, the install itself in `init.ts`. */
|
|
786
1008
|
async function runInit(a, { log, err, cwd, }) {
|
|
787
1009
|
// Deferred, not implemented: named and refused, rather than read as the directory argument.
|
|
@@ -804,25 +1026,11 @@ async function runInit(a, { log, err, cwd, }) {
|
|
|
804
1026
|
paper: a.paper,
|
|
805
1027
|
format: isFormat(a.format) ? a.format : null,
|
|
806
1028
|
createPaper: (papersRoot, name, format) => createPaperAt(papersRoot, name, format, { log, err, cwd }),
|
|
807
|
-
tex: {
|
|
808
|
-
|
|
809
|
-
install: () => runToolchain({
|
|
810
|
-
check: false,
|
|
811
|
-
log,
|
|
812
|
-
err,
|
|
813
|
-
banal: hostBanalInstaller(),
|
|
814
|
-
tex: toolchainTex(resolve(cwd, a.paths[0] ?? ".")),
|
|
815
|
-
}),
|
|
816
|
-
},
|
|
817
|
-
resolveCliPapers: (root) => {
|
|
818
|
-
const read = readConfig({ ...a, config: null }, { log: () => { }, err: () => { }, cwd: root });
|
|
819
|
-
return read.code === undefined
|
|
820
|
-
? (toPaths(papersDirOf(read.opts))[0] ?? null)
|
|
821
|
-
: null;
|
|
822
|
-
},
|
|
1029
|
+
tex: initTexLive(a, { log, err, cwd }),
|
|
1030
|
+
resolveCliPapers: (root) => cliPapers({ ...a, config: null }, root),
|
|
823
1031
|
});
|
|
824
1032
|
}
|
|
825
|
-
export async function run(argv, { log = console.log, err = console.error, cwd = process.cwd(), } = {}) {
|
|
1033
|
+
export async function run(argv, { log = console.log, err = console.error, cwd = process.cwd(), checkReferences = onlineReferences, } = {}) {
|
|
826
1034
|
const a = parseArgs(argv);
|
|
827
1035
|
// The refusal must come FIRST: behind a flag without a value there is usually a typo, or a
|
|
828
1036
|
// substitution in CI that collapsed into nothing, and any continuation works on something other
|
|
@@ -833,6 +1041,10 @@ export async function run(argv, { log = console.log, err = console.error, cwd =
|
|
|
833
1041
|
`not what the command line said.`);
|
|
834
1042
|
return 2;
|
|
835
1043
|
}
|
|
1044
|
+
if (a.unknownFlag !== undefined) {
|
|
1045
|
+
err(`unknown flag \`${a.unknownFlag}\` — \`paperlint --help\` lists every flag`);
|
|
1046
|
+
return 2;
|
|
1047
|
+
}
|
|
836
1048
|
if (a.help || !a.cmd) {
|
|
837
1049
|
log(USAGE);
|
|
838
1050
|
return a.help ? 0 : 2;
|
|
@@ -846,10 +1058,7 @@ export async function run(argv, { log = console.log, err = console.error, cwd =
|
|
|
846
1058
|
// broken is precisely its job. So a failed read becomes "the CLI would lint nothing", which is
|
|
847
1059
|
// what it prints, rather than an early exit that tells the reader nothing about the hooks.
|
|
848
1060
|
if (a.cmd === "doctor") {
|
|
849
|
-
const
|
|
850
|
-
const papers = read.code === undefined
|
|
851
|
-
? (toPaths(papersDirOf(read.opts))[0] ?? null)
|
|
852
|
-
: null;
|
|
1061
|
+
const papers = cliPapers(a, cwd);
|
|
853
1062
|
return doctor({
|
|
854
1063
|
log,
|
|
855
1064
|
cwd,
|
|
@@ -859,7 +1068,7 @@ export async function run(argv, { log = console.log, err = console.error, cwd =
|
|
|
859
1068
|
}
|
|
860
1069
|
const simple = SIMPLE[a.cmd];
|
|
861
1070
|
if (simple)
|
|
862
|
-
return await simple(a, { log, err, cwd });
|
|
1071
|
+
return await simple(a, { log, err, cwd, checkReferences });
|
|
863
1072
|
if (a.cmd === "check")
|
|
864
1073
|
err(`\`check\` is now \`lint\` — running it anyway. Update the call to \`paperlint lint\`.`);
|
|
865
1074
|
if (a.cmd !== "lint" && a.cmd !== "check") {
|
|
@@ -908,14 +1117,16 @@ export async function run(argv, { log = console.log, err = console.error, cwd =
|
|
|
908
1117
|
...opts,
|
|
909
1118
|
rules: [...papers.value.preset, ...(opts.rules ?? []), ...papers.value.own],
|
|
910
1119
|
};
|
|
1120
|
+
// Loaded lazily because only `lint` needs it. If the module cannot be loaded (a broken install:
|
|
1121
|
+
// the file missing from the package), lint still runs over the markdown papers rather than
|
|
1122
|
+
// failing outright — the LaTeX rules simply have no language to run in.
|
|
911
1123
|
let texLanguage = null;
|
|
912
1124
|
try {
|
|
913
|
-
// @ts-expect-error — the module is .mjs and has no types
|
|
914
|
-
// case here, it is caught by the catch below.
|
|
1125
|
+
// @ts-expect-error — the module is .mjs and has no types
|
|
915
1126
|
({ texLanguage } = await import("../eslint-rules/latex-language.mjs"));
|
|
916
1127
|
}
|
|
917
1128
|
catch {
|
|
918
|
-
|
|
1129
|
+
texLanguage = null;
|
|
919
1130
|
}
|
|
920
1131
|
const eslint = new ESLint({
|
|
921
1132
|
cwd: lintRoot(root, paths),
|
|
@@ -925,7 +1136,7 @@ export async function run(argv, { log = console.log, err = console.error, cwd =
|
|
|
925
1136
|
});
|
|
926
1137
|
const unowned = await firstUnownedFile(eslint, paths);
|
|
927
1138
|
if (unowned !== null) {
|
|
928
|
-
err(`${
|
|
1139
|
+
err(`${shown(cwd, unowned)} is not a file paperlint lints — it lints ${PAPER_FILE_PATTERNS.join(", ")}`);
|
|
929
1140
|
return 2;
|
|
930
1141
|
}
|
|
931
1142
|
// 🔴 ESLint THROWS on an empty set (`NoFilesFoundError`) — the guard below simply never got
|
|
@@ -951,7 +1162,7 @@ export async function run(argv, { log = console.log, err = console.error, cwd =
|
|
|
951
1162
|
// indistinguishable from "not a single rule got a single file". A rule whose glob did not match
|
|
952
1163
|
// is not invoked — and, not being invoked, it physically cannot report that.
|
|
953
1164
|
if (results.length === 0) {
|
|
954
|
-
err(`nothing was linted under ${paths.map((x) =>
|
|
1165
|
+
err(`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.`);
|
|
955
1166
|
return 1;
|
|
956
1167
|
}
|
|
957
1168
|
return await reportLint(eslint, results, structure, {
|
|
@@ -960,6 +1171,11 @@ export async function run(argv, { log = console.log, err = console.error, cwd =
|
|
|
960
1171
|
err,
|
|
961
1172
|
where: relative(cwd, root) || ".",
|
|
962
1173
|
opts: withPapers,
|
|
1174
|
+
// Every paper of the project, not only the ones on the command line (#103).
|
|
1175
|
+
projectPapers: papersRoots(cfg)
|
|
1176
|
+
.flatMap((r) => papersIn(r))
|
|
1177
|
+
.map((d) => join(d, MAIN))
|
|
1178
|
+
.filter((f) => existsSync(f)),
|
|
963
1179
|
});
|
|
964
1180
|
}
|
|
965
1181
|
/**
|
|
@@ -985,7 +1201,7 @@ const isEmptySet = (e) => {
|
|
|
985
1201
|
const fail = e;
|
|
986
1202
|
return (fail?.messageTemplate === "file-not-found" ||
|
|
987
1203
|
fail?.messageTemplate === "all-matched-files-ignored" ||
|
|
988
|
-
/No files matching/i.test(fail?.message
|
|
1204
|
+
/No files matching/i.test(String(fail?.message)));
|
|
989
1205
|
};
|
|
990
1206
|
/**
|
|
991
1207
|
* The end of `paperlint lint`: refuse an optional rule that reached no paper, print the findings, and
|
|
@@ -993,10 +1209,10 @@ const isEmptySet = (e) => {
|
|
|
993
1209
|
*/
|
|
994
1210
|
async function reportLint(eslint,
|
|
995
1211
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- #49: replace with a real type
|
|
996
|
-
results, structure, { a, log, err, where, opts, }) {
|
|
997
|
-
const silent = await silentOptionalRules(eslint, results.map((r) => r.filePath), opts);
|
|
1212
|
+
results, structure, { a, log, err, where, opts, projectPapers, }) {
|
|
1213
|
+
const silent = await silentOptionalRules(eslint, [...results.map((r) => r.filePath), ...projectPapers], opts);
|
|
998
1214
|
if (silent.length > 0) {
|
|
999
|
-
err(`${silent.join(", ")} is turned on in "rules", but no
|
|
1215
|
+
err(`${silent.join(", ")} is turned on in "rules", but no paper.tex of the project gets it — check the block's ` +
|
|
1000
1216
|
`"files" (relative to ${where}). A rule that never runs reports exactly like a rule that passed.`);
|
|
1001
1217
|
return 1;
|
|
1002
1218
|
}
|