paperlint 2.1.0 → 3.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +6 -1
- package/README.md +243 -235
- package/action.yml +4 -6
- package/dist/adapters/references/index.d.ts +13 -0
- package/dist/adapters/references/index.d.ts.map +1 -0
- package/dist/adapters/references/index.js +55 -0
- package/dist/adapters/references/index.js.map +1 -0
- package/dist/adapters/references/reach.io.d.ts +3 -0
- package/dist/adapters/references/reach.io.d.ts.map +1 -0
- package/dist/adapters/references/reach.io.js +14 -0
- package/dist/adapters/references/reach.io.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 +15 -0
- package/dist/build.d.ts.map +1 -1
- package/dist/build.js +60 -7
- package/dist/build.js.map +1 -1
- package/dist/cli.d.ts +94 -30
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +500 -288
- package/dist/cli.js.map +1 -1
- package/dist/doctor.d.ts.map +1 -1
- package/dist/doctor.js +40 -72
- package/dist/doctor.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/facts-file.d.ts +2 -2
- package/dist/facts-file.js +2 -2
- package/dist/hooks-settings.d.ts +0 -21
- package/dist/hooks-settings.d.ts.map +1 -1
- package/dist/hooks-settings.js +17 -135
- package/dist/hooks-settings.js.map +1 -1
- package/dist/init.d.ts +39 -29
- package/dist/init.d.ts.map +1 -1
- package/dist/init.js +144 -173
- package/dist/init.js.map +1 -1
- package/dist/link-skills.d.ts +1 -1
- package/dist/link-skills.d.ts.map +1 -1
- package/dist/link-skills.js +11 -29
- 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 +49 -25
- package/dist/new-paper.js.map +1 -1
- package/dist/paper-files.d.ts +85 -0
- package/dist/paper-files.d.ts.map +1 -0
- package/dist/paper-files.js +96 -0
- package/dist/paper-files.js.map +1 -0
- package/dist/paper-settings.d.ts +13 -41
- package/dist/paper-settings.d.ts.map +1 -1
- package/dist/paper-settings.js +71 -71
- package/dist/paper-settings.js.map +1 -1
- package/dist/ports/check-references.d.ts +31 -0
- package/dist/ports/check-references.d.ts.map +1 -0
- package/dist/ports/check-references.js +7 -0
- package/dist/ports/check-references.js.map +1 -0
- package/dist/presets.d.ts +1 -1
- package/dist/presets.d.ts.map +1 -1
- package/dist/presets.js +4 -6
- package/dist/presets.js.map +1 -1
- package/dist/reference-rules.d.ts +47 -0
- package/dist/reference-rules.d.ts.map +1 -0
- package/dist/reference-rules.js +156 -0
- package/dist/reference-rules.js.map +1 -0
- package/dist/references.d.ts +36 -0
- package/dist/references.d.ts.map +1 -0
- package/dist/references.js +91 -0
- package/dist/references.js.map +1 -0
- package/dist/rules-config.d.ts +5 -3
- package/dist/rules-config.d.ts.map +1 -1
- package/dist/rules-config.js +15 -6
- package/dist/rules-config.js.map +1 -1
- package/dist/structure.js +3 -3
- package/dist/structure.js.map +1 -1
- package/dist/tex-requirements.js +2 -2
- package/dist/tex-requirements.js.map +1 -1
- package/dist/toolchain.d.ts +5 -1
- package/dist/toolchain.d.ts.map +1 -1
- package/dist/toolchain.js +22 -5
- package/dist/toolchain.js.map +1 -1
- package/dist/types.d.ts +26 -18
- 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 +163 -104
- package/docs/e2e.md +1 -1
- package/docs/install.md +25 -40
- package/docs/optional-rules.md +5 -4
- package/docs/rules.md +65 -24
- package/docs/skills.md +80 -0
- package/eslint-rules/bib-reachable-entry.mjs +75 -0
- package/eslint-rules/latex-language.mjs +29 -1
- package/eslint-rules/paper-stages.harness.mjs +1 -117
- package/eslint-rules/paper-stages.mjs +0 -120
- package/eslint-rules/paper-stages.mutations.mjs +0 -76
- package/eslint-rules/paper-typography.mjs +306 -191
- package/eslint-rules/paper-typography.test.mjs +337 -0
- package/eslint-rules/papers.harness.mjs +8 -12
- package/eslint-rules/papers.mjs +17 -31
- package/eslint-rules/pdf-last-page-balance.mjs +4 -7
- package/eslint-rules/review-frontmatter.mjs +118 -0
- package/eslint-rules/review-frontmatter.schema.json +36 -0
- package/eslint-rules/review-frontmatter.test.mjs +118 -0
- package/eslint-rules/sibling-frontmatter.mjs +19 -0
- package/eslint-rules/sibling-frontmatter.schema.json +14 -0
- package/eslint.config.mjs +0 -15
- package/fixtures/real-markdown-paper/baseline.json +6 -7
- 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 +39 -98
- package/hooks/hooks.mutations.mjs +6 -6
- package/hooks/paper-edit-guard.hook.d.mts +2 -3
- package/hooks/paper-edit-guard.hook.mjs +31 -42
- package/hooks/paper-skills-nudge.hook.mjs +21 -19
- package/hooks/paper-status-gates.hook.mjs +21 -19
- package/hooks/paper-status-gates.sh +8 -13
- package/lib/paper-config.d.mts +15 -25
- package/lib/paper-config.harness.mjs +18 -35
- package/lib/paper-config.mjs +83 -105
- package/lib/paper-config.mutations.mjs +2 -2
- package/lib/skill-checks.mjs +1 -1
- package/lib/skill-trigger-cases.harness.mjs +9 -8
- package/lib/skill-trigger-cases.mjs +7 -15
- package/package.json +1 -1
- package/scripts/eslint-report-guard.mjs +1 -1
- package/scripts/harness-api.frozen.json +0 -3
- package/scripts/mutation-batteries.frozen.json +5 -14
- package/skills/analyze-sibling-paper/SKILL.md +16 -12
- package/skills/analyze-sibling-paper/SKILL.md.spec.ts +15 -11
- package/skills/cold-read-diff/SKILL.md +20 -1
- package/skills/cold-read-diff/SKILL.md.spec.ts +19 -0
- package/skills/grade-paper-writing/SKILL.md +20 -1
- package/skills/grade-paper-writing/SKILL.md.spec.ts +19 -0
- package/skills/paper-pipeline/scripts/consumer.d.mts +0 -1
- package/skills/paper-pipeline/scripts/consumer.harness.mjs +8 -12
- package/skills/paper-pipeline/scripts/consumer.mjs +19 -33
- package/skills/paper-pipeline/scripts/extract-ref-facts.harness.mjs +6 -4
- package/skills/paper-pipeline/scripts/run-mechanical.mjs +2 -11
- package/skills/plan-paper-timeline/SKILL.md +2 -2
- package/skills/plan-paper-timeline/SKILL.md.spec.ts +1 -1
- package/skills/plan-paper-timeline/plan-paper-timeline.effects.harness.mjs +1 -1
- package/skills/plan-paper-timeline/plan-paper-timeline.effects.mutations.mjs +2 -2
- package/skills/render-paper/check-render.sh +2 -3
- package/skills/render-paper/render-paper.harness.mjs +2 -2
- package/skills/verify-citations/SKILL.md +10 -1
- package/skills/verify-citations/SKILL.md.spec.ts +9 -0
- package/skills/verify-citations/scripts/bib-authors.mjs +48 -26
- package/src/adapters/references/index.ts +102 -0
- package/src/adapters/references/reach.io.ts +12 -0
- package/src/build-engine.ts +6 -1
- package/src/build.harness.mjs +54 -6
- package/src/build.ts +89 -11
- package/src/cli.harness.mjs +149 -294
- package/src/cli.mutations.mjs +35 -44
- package/src/cli.ts +637 -328
- package/src/doctor.harness.mjs +41 -84
- package/src/doctor.mutations.mjs +9 -9
- package/src/doctor.ts +44 -89
- package/src/engine.harness.mjs +50 -0
- package/src/engine.ts +35 -0
- package/src/facts-file.test.ts +0 -7
- package/src/facts-file.ts +2 -2
- package/src/hooks-settings.harness.mjs +7 -92
- package/src/hooks-settings.mutations.mjs +2 -17
- package/src/hooks-settings.ts +18 -162
- package/src/init.test.ts +206 -0
- package/src/init.ts +187 -231
- package/src/link-skills.harness.mjs +1 -38
- package/src/link-skills.mutations.mjs +9 -5
- package/src/link-skills.ts +10 -34
- package/src/lint-fix.test.ts +120 -0
- package/src/linted-set.test.ts +118 -0
- package/src/new-paper.harness.mjs +2 -0
- package/src/new-paper.mutations.mjs +0 -14
- package/src/new-paper.test.ts +173 -14
- package/src/new-paper.ts +82 -30
- package/src/paper-files.test.ts +395 -0
- package/src/paper-files.ts +200 -0
- package/src/paper-settings-commands.test.ts +121 -122
- package/src/paper-settings.test.ts +93 -64
- package/src/paper-settings.ts +99 -97
- package/src/parse-args.test.ts +48 -0
- package/src/ports/check-references.ts +28 -0
- package/src/presets.ts +5 -15
- package/src/reference-rules.ts +229 -0
- package/src/references.test.ts +209 -0
- package/src/references.ts +138 -0
- package/src/rules-config.harness.mjs +24 -8
- package/src/rules-config.ts +19 -7
- package/src/rules-off.test.ts +111 -0
- package/src/structure.harness.mjs +4 -2
- package/src/structure.ts +3 -3
- package/src/tex-requirements.ts +2 -2
- package/src/toolchain.harness.mjs +41 -6
- package/src/toolchain.ts +29 -5
- package/src/types.ts +36 -18
- package/src/venue-rules.test.ts +23 -11
- package/src/venue-rules.ts +12 -14
- package/templates/paper/paperlint.json +1 -1
- package/eslint-rules/doc-fields.harness.mjs +0 -336
- package/eslint-rules/doc-fields.mjs +0 -186
- package/eslint-rules/doc-fields.mutations.mjs +0 -96
- package/eslint-rules/paper-typography.harness.mjs +0 -291
- package/eslint-rules/paper-typography.mutations.mjs +0 -131
- package/eslint-rules/review-findings-cause.harness.mjs +0 -228
- package/eslint-rules/review-findings-cause.mjs +0 -135
- package/eslint-rules/review-findings-cause.mutations.mjs +0 -72
- package/fixtures/paper-stages/authors-ran/PIPELINE-STATUS.md +0 -16
- package/fixtures/paper-stages/marker-in-prose/PIPELINE-STATUS.md +0 -17
- package/fixtures/review-findings-cause/clean.md +0 -17
- package/fixtures/review-findings-cause/defect.md +0 -14
- package/fixtures/review-findings-cause/old-debt.md +0 -14
- package/fixtures/review-findings-cause/quiet-in-fence.md +0 -16
package/src/cli.ts
CHANGED
|
@@ -13,10 +13,8 @@
|
|
|
13
13
|
* uses: zernie/paperlint@<sha> ← action.yml
|
|
14
14
|
*
|
|
15
15
|
* ⚠️ THE BOUNDARY THIS UTILITY HAS NO RIGHT TO ERASE: the consumer's data stays with the consumer.
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* `.claude/skills/verify-citations/...` into a rule's message. So they live in the consumer's
|
|
19
|
-
* `package.json`, under the `paperlint` key.
|
|
16
|
+
* Where the papers are and the project's own rule blocks — that is about ONE corpus, so it lives in
|
|
17
|
+
* the consumer's own `paperlint.json`, at the project root and in each paper.
|
|
20
18
|
*
|
|
21
19
|
* 🔴 WHY THE COMMAND IS CALLED `lint` AND NOT `check`. It does exactly what everyone else calls by
|
|
22
20
|
* that word: reads files, changes nothing, prints findings, exits non-zero. `check` is taken in the
|
|
@@ -26,7 +24,7 @@
|
|
|
26
24
|
* replaced it: silently breaking someone else's workflow is worse than asking them to fix a line.
|
|
27
25
|
*/
|
|
28
26
|
import { ESLint, type Linter } from "eslint";
|
|
29
|
-
import { readFileSync, existsSync } from "node:fs";
|
|
27
|
+
import { readFileSync, existsSync, statSync } from "node:fs";
|
|
30
28
|
import { createRequire } from "node:module";
|
|
31
29
|
import { spawnSync } from "node:child_process";
|
|
32
30
|
import { fileURLToPath } from "node:url";
|
|
@@ -46,15 +44,31 @@ import {
|
|
|
46
44
|
} from "./structure.ts";
|
|
47
45
|
import { buildPapers, papersIn, anyFailed, remedyFor, MAIN } from "./build.ts";
|
|
48
46
|
import { prepareEngine } from "./build-engine.ts";
|
|
49
|
-
import { runToolchain } from "./toolchain.ts";
|
|
47
|
+
import { cacheRoot, cachedTree, runToolchain } from "./toolchain.ts";
|
|
50
48
|
import { banalInstaller, parseBanalSettings } from "./adapters/banal/index.ts";
|
|
51
49
|
import { curlDownload } from "./adapters/curl/index.ts";
|
|
52
50
|
import { hostDirs, nodeAdapters, nodeFiles } from "./adapters/node/index.ts";
|
|
53
51
|
import { VENUE_RULE_LEVELS, venueRules } from "./venue-rules.ts";
|
|
54
|
-
import {
|
|
52
|
+
import {
|
|
53
|
+
paperRules,
|
|
54
|
+
stringFields,
|
|
55
|
+
type PaperSettings,
|
|
56
|
+
} from "./paper-settings.ts";
|
|
57
|
+
import { referenceRules, REFERENCE_RULE_LEVELS } from "./reference-rules.ts";
|
|
58
|
+
import { onlineReferences } from "./adapters/references/index.ts";
|
|
59
|
+
import {
|
|
60
|
+
narrowToOwners,
|
|
61
|
+
ownedPatterns,
|
|
62
|
+
ruleOwners,
|
|
63
|
+
scopeToOwned,
|
|
64
|
+
} from "./paper-files.ts";
|
|
55
65
|
import {
|
|
56
66
|
paperPreset,
|
|
57
67
|
paperPresetProblem,
|
|
68
|
+
presetProblemText,
|
|
69
|
+
resolvePreset,
|
|
70
|
+
shippedPresets,
|
|
71
|
+
SHIPPED_PREFIX,
|
|
58
72
|
type PaperPreset,
|
|
59
73
|
} from "./presets.ts";
|
|
60
74
|
import type { ToolInstaller } from "./ports/tool-installer.ts";
|
|
@@ -74,17 +88,16 @@ import {
|
|
|
74
88
|
newPaper,
|
|
75
89
|
reportNewPaper,
|
|
76
90
|
type PaperFormat,
|
|
91
|
+
type VenueSetting,
|
|
77
92
|
} from "./new-paper.ts";
|
|
78
93
|
// The one source for the consumer's config key lives in the .mjs half of the package (the ESLint
|
|
79
94
|
// rules and the skill scripts import it too); its types are in lib/paper-config.d.mts.
|
|
80
95
|
import {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
LEGACY_KEY_MESSAGE,
|
|
96
|
+
CONFIG_FILE,
|
|
97
|
+
DEFAULT_PAPERS_ROOT,
|
|
84
98
|
PAPERS_DIR_FIELD,
|
|
85
99
|
SETTINGS_KEYS,
|
|
86
|
-
|
|
87
|
-
renamedFieldMessage,
|
|
100
|
+
findProjectRoot,
|
|
88
101
|
} from "../lib/paper-config.mjs";
|
|
89
102
|
import {
|
|
90
103
|
parseRuleBlocks,
|
|
@@ -93,6 +106,7 @@ import {
|
|
|
93
106
|
unknownKeys,
|
|
94
107
|
type Parsed,
|
|
95
108
|
type RuleBlock,
|
|
109
|
+
type RuleEntry,
|
|
96
110
|
} from "./rules-config.ts";
|
|
97
111
|
export { init };
|
|
98
112
|
export { nextSteps } from "./init.ts";
|
|
@@ -106,18 +120,23 @@ import typography from "../eslint-rules/paper-typography.mjs";
|
|
|
106
120
|
// @ts-expect-error — an ESLint rule in .mjs, it has no types
|
|
107
121
|
import texBuild from "../eslint-rules/tex-build.mjs";
|
|
108
122
|
// @ts-expect-error — an ESLint rule in .mjs, it has no types
|
|
109
|
-
import
|
|
123
|
+
import bibReachable from "../eslint-rules/bib-reachable-entry.mjs";
|
|
110
124
|
// @ts-expect-error — an ESLint rule in .mjs, it has no types
|
|
111
|
-
import
|
|
125
|
+
import reviewFrontmatter from "../eslint-rules/review-frontmatter.mjs";
|
|
126
|
+
// @ts-expect-error — an ESLint rule in .mjs, it has no types
|
|
127
|
+
import siblingFrontmatter from "../eslint-rules/sibling-frontmatter.mjs";
|
|
112
128
|
// @ts-expect-error — an ESLint rule in .mjs, it has no types
|
|
113
129
|
import pdfRules from "../eslint-rules/pdf-last-page-balance.mjs";
|
|
114
130
|
|
|
131
|
+
/** The shipped venue presets, read from the package's venues directory — the one list. */
|
|
132
|
+
const SHIPPED_VENUES = (): string[] => shippedPresets(packageVenuesDir());
|
|
133
|
+
|
|
115
134
|
const USAGE = `paperlint — machine-checkable gates for a paper kept in git
|
|
116
135
|
|
|
117
136
|
npx paperlint init [dir] set the project up: detect the papers directory, declare it
|
|
118
137
|
in package.json, link the skills, wire the hooks into
|
|
119
138
|
.claude/settings.json, offer the CI step, report what is missing
|
|
120
|
-
npx paperlint new <name> [--format tex|md]
|
|
139
|
+
npx paperlint new <name> [--venue <preset>] [--kind <kind>] [--format tex|md]
|
|
121
140
|
create <papers>/<name>/ from the template; never overwrites,
|
|
122
141
|
on an existing folder adds only the missing files, then lints it
|
|
123
142
|
npx paperlint lint [paths…] run every rule over your papers
|
|
@@ -127,7 +146,10 @@ const USAGE = `paperlint — machine-checkable gates for a paper kept in git
|
|
|
127
146
|
paper directory is ignored (--dry-run: print the plan only).
|
|
128
147
|
Compiles with paperlint's TeX Live, else one on PATH that has every
|
|
129
148
|
package the venue declares; on a terminal it offers to install
|
|
130
|
-
one, without a terminal it stops and names \`npx paperlint toolchain
|
|
149
|
+
one, without a terminal it stops and names \`npx paperlint toolchain\`.
|
|
150
|
+
Then checks the references online (the cited works exist, titles
|
|
151
|
+
and authors match) into _build/references.json — never failing
|
|
152
|
+
the build: without network it records "not checked"
|
|
131
153
|
npx paperlint toolchain [--check] install TeX Live with every package the venue presets declare
|
|
132
154
|
into ~/.cache/paperlint/texlive (PAPERLINT_TEXLIVE_DIR overrides); a second
|
|
133
155
|
run does nothing. --check: report what is missing, change nothing
|
|
@@ -142,49 +164,56 @@ init:
|
|
|
142
164
|
--paper <name> create this paper too (without a human, the only way init creates one)
|
|
143
165
|
--format tex|md the new paper's source format; default tex
|
|
144
166
|
|
|
167
|
+
new:
|
|
168
|
+
--venue <preset> the venue preset, written as "extends" into the paper's paperlint.json:
|
|
169
|
+
a shipped one (${SHIPPED_VENUES().join(", ")}), or a path to your
|
|
170
|
+
own preset starting with ./ or ../, relative to where you run the command.
|
|
171
|
+
On a terminal without --venue, new asks; "none" leaves it unset
|
|
172
|
+
--kind <kind> the paper's kind at that venue (its page limit), e.g. short — one of the
|
|
173
|
+
preset's kinds; needs --venue
|
|
174
|
+
--format tex|md the paper's source format; default tex
|
|
175
|
+
|
|
145
176
|
lint:
|
|
146
|
-
npx paperlint lint [paths…] [--config <file.json>] [--json]
|
|
177
|
+
npx paperlint lint [paths…] [--fix] [--config <file.json>] [--json]
|
|
147
178
|
|
|
148
179
|
<paths…> where your papers live, e.g. papers. Optional ONLY because the declaration
|
|
149
180
|
names it — one of the two must name the scope. There is no default
|
|
150
181
|
of ".": linting whatever happens to be in the checkout is how a green
|
|
151
182
|
report over a scope nobody chose gets produced.
|
|
183
|
+
--fix write every fix the rules offer (section signs, leading zeros, figure
|
|
184
|
+
references), then report what is left
|
|
152
185
|
--config <file> read the settings from this file instead of the discovered one
|
|
153
186
|
--json machine-readable findings on stdout, nothing else on it
|
|
154
187
|
--max-warnings <n> fail when warnings exceed n. Default -1: warnings never fail, because
|
|
155
188
|
most findings here are advisory and a gate that fails on advice gets muted
|
|
156
189
|
|
|
157
|
-
settings —
|
|
158
|
-
current directory, the way every other tool in the stack finds its config. \`--config\` names
|
|
159
|
-
another file of the same shape. \`papersDir\` is required; the rest is optional:
|
|
160
|
-
|
|
161
|
-
"paperlint": {
|
|
162
|
-
"papersDir": "papers",
|
|
163
|
-
"authorListCommand": "node scripts/bib-authors.mjs",
|
|
164
|
-
"typographyDebt": { "papers/my-paper": { "sectionSign": 12 } },
|
|
165
|
-
"docFields": { "read": { "values": ["full", "abstract", "none"] } },
|
|
166
|
-
"reviewSince": "2026-08-23",
|
|
167
|
-
"minFindings": 3,
|
|
168
|
-
"causeMarker": "Cause:",
|
|
169
|
-
"rules": [ { "files": ["papers/my-paper/**"],
|
|
170
|
-
"rules": { "pdf/last-page-balance": "error" } } ]
|
|
171
|
-
}
|
|
190
|
+
settings — paperlint.json, at two levels, one schema. Both are optional.
|
|
172
191
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
pdf/last-page-balance. The venue rules (pdf/fresh, pdf/profile, pdf/fonts, pdf/geometry,
|
|
176
|
-
pdf/limits, pdf/body-size, pdf/measured) are on for every paper whose paperlint.json names a venue;
|
|
177
|
-
set one to "off" there to skip it.
|
|
192
|
+
paperlint.json (the project root, beside package.json) — found by walking up from the
|
|
193
|
+
current directory. \`--config\` names another file of the same shape.
|
|
178
194
|
|
|
179
|
-
|
|
195
|
+
{
|
|
196
|
+
"papersDir": "papers",
|
|
197
|
+
"rules": [ { "files": ["papers/my-paper/**"],
|
|
198
|
+
"rules": { "pdf/last-page-balance": "error" } } ]
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
"papersDir" defaults to "papers". "extends", "kind" and "pdf" here are defaults for every
|
|
202
|
+
paper. "papersDir", "structure" and the skills' keys are allowed only here.
|
|
203
|
+
|
|
204
|
+
<papersDir>/<paper>/paperlint.json — one paper, merged over the root file:
|
|
180
205
|
|
|
181
|
-
|
|
206
|
+
{ "extends": "paperlint:aisec", "kind": "research", "rules": { "pdf/last-page-balance": "error" } }
|
|
182
207
|
|
|
183
|
-
"extends" names a venue preset: paperlint:<name> (shipped:
|
|
208
|
+
"extends" names a venue preset: paperlint:<name> (shipped: ${SHIPPED_VENUES().join(", ")})
|
|
184
209
|
or ./path.jsonc, relative to the paperlint.json. npm presets are not supported yet.
|
|
185
210
|
|
|
186
|
-
"rules"
|
|
187
|
-
|
|
211
|
+
"rules" is { "<rule>": "<severity>" } for every paper file in scope, or ESLint flat-config
|
|
212
|
+
blocks (files, ignores, rules) with globs relative to that file. Order, later wins: paperlint's
|
|
213
|
+
own, the venue preset's, the root file's, the paper's. Optional rules (off unless turned on):
|
|
214
|
+
pdf/last-page-balance. The venue rules (pdf/fresh, pdf/profile, pdf/fonts, pdf/geometry,
|
|
215
|
+
pdf/limits, pdf/body-size, pdf/measured) are on for every paper with a venue preset; set one to
|
|
216
|
+
"off" to skip it. An unknown key, in either file, is an error.
|
|
188
217
|
`;
|
|
189
218
|
|
|
190
219
|
/** The config the user would otherwise write by hand. The data comes from `opts`, the mechanism is here. */
|
|
@@ -193,7 +222,17 @@ export function buildConfig(
|
|
|
193
222
|
texLanguage: unknown,
|
|
194
223
|
): unknown[] {
|
|
195
224
|
const paperRules = { ...researchQuestion.rules, ...typography.rules };
|
|
196
|
-
|
|
225
|
+
// The reference rules judge `_build/references.json`, and only on `paper.tex`.
|
|
226
|
+
const texPaperRules = {
|
|
227
|
+
...paperRules,
|
|
228
|
+
...referenceRules({ files: nodeFiles }),
|
|
229
|
+
};
|
|
230
|
+
// Each typography rule reports every occurrence where it is, and fixes it (`--fix`).
|
|
231
|
+
const prose = {
|
|
232
|
+
"paper/research-question": "warn",
|
|
233
|
+
"paper/section-word": "warn",
|
|
234
|
+
"paper/leading-zero": "warn",
|
|
235
|
+
};
|
|
197
236
|
const md = {
|
|
198
237
|
language: "markdown/gfm",
|
|
199
238
|
languageOptions: { frontmatter: "yaml" },
|
|
@@ -228,51 +267,30 @@ export function buildConfig(
|
|
|
228
267
|
rules: {
|
|
229
268
|
"paper/stages": "error",
|
|
230
269
|
"paper/source": "error",
|
|
231
|
-
"paper/author-list": [
|
|
232
|
-
"warn",
|
|
233
|
-
opts.authorListCommand ? { command: opts.authorListCommand } : {},
|
|
234
|
-
],
|
|
235
270
|
},
|
|
236
271
|
},
|
|
237
272
|
{
|
|
238
273
|
files: ["**/paper.md", "**/draft.md"],
|
|
239
274
|
plugins: { markdown, paper: { rules: paperRules } },
|
|
240
275
|
...md,
|
|
241
|
-
rules:
|
|
242
|
-
"paper/research-question": "warn",
|
|
243
|
-
"paper/typography": typographyOpt,
|
|
244
|
-
},
|
|
276
|
+
rules: prose,
|
|
245
277
|
},
|
|
246
278
|
{
|
|
247
279
|
files: ["**/reviews/*.md"],
|
|
248
|
-
plugins: {
|
|
249
|
-
markdown,
|
|
250
|
-
review: { rules: { ...findingsCause.rules } },
|
|
251
|
-
doc: docFields,
|
|
252
|
-
},
|
|
280
|
+
plugins: { markdown, review: reviewFrontmatter },
|
|
253
281
|
...md,
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
{
|
|
267
|
-
minFindings: opts.minFindings ?? 3,
|
|
268
|
-
...(opts.causeMarker ? { causeMarker: opts.causeMarker } : {}),
|
|
269
|
-
...(opts.reviewSince ? { sinceCreated: opts.reviewSince } : {}),
|
|
270
|
-
},
|
|
271
|
-
],
|
|
272
|
-
...(opts.docFields
|
|
273
|
-
? { "doc/fields": ["warn", { fields: opts.docFields }] }
|
|
274
|
-
: {}),
|
|
275
|
-
},
|
|
282
|
+
// A review's frontmatter is a record, validated by paperlint's JSON Schema
|
|
283
|
+
// (eslint-rules/review-frontmatter.mjs).
|
|
284
|
+
rules: { "review/frontmatter": "error" },
|
|
285
|
+
},
|
|
286
|
+
{
|
|
287
|
+
// A sibling card says how much of the competing paper was read (`read:`). The cards are
|
|
288
|
+
// written by this package's analyze-sibling-paper skill; its README is an index, not a card.
|
|
289
|
+
files: ["**/siblings/*.md"],
|
|
290
|
+
ignores: ["**/siblings/README.md"],
|
|
291
|
+
plugins: { markdown, sibling: siblingFrontmatter },
|
|
292
|
+
...md,
|
|
293
|
+
rules: { "sibling/frontmatter": "warn" },
|
|
276
294
|
},
|
|
277
295
|
];
|
|
278
296
|
|
|
@@ -283,21 +301,32 @@ export function buildConfig(
|
|
|
283
301
|
files: ["**/paper.tex"],
|
|
284
302
|
plugins: {
|
|
285
303
|
tex: { languages: { latex: texLanguage }, rules: texBuild },
|
|
286
|
-
paper: { rules:
|
|
304
|
+
paper: { rules: texPaperRules },
|
|
305
|
+
bib: bibReachable,
|
|
287
306
|
},
|
|
288
307
|
language: "tex/latex",
|
|
289
308
|
rules: {
|
|
290
|
-
|
|
291
|
-
|
|
309
|
+
...prose,
|
|
310
|
+
...REFERENCE_RULE_LEVELS,
|
|
311
|
+
"paper/figure-ref-style": "warn",
|
|
312
|
+
"bib/reachable-entry": "warn",
|
|
292
313
|
"tex/future-promise": "warn",
|
|
293
314
|
"tex/acm-frontmatter-override": "error",
|
|
294
315
|
// Silent for a paper whose paperlint.json names no venue (src/venue-rules.ts).
|
|
295
316
|
...VENUE_RULE_LEVELS,
|
|
296
317
|
},
|
|
297
318
|
});
|
|
319
|
+
// 🔴 ONLY THE FILES THESE BLOCKS CLAIM ARE LINTED (src/paper-files.ts). Without this block ESLint's
|
|
320
|
+
// built-in defaults lint every .js/.mjs/.cjs under the papers directory — a paper's vendored
|
|
321
|
+
// `repro/` code failed the run with 74 parse errors and not one finding on a paper file. It goes
|
|
322
|
+
// FIRST: the `.template/` ignore below must come after its directory un-ignore to win.
|
|
323
|
+
const owners = ruleOwners(cfg);
|
|
324
|
+
cfg.unshift(scopeToOwned(ownedPatterns(cfg)));
|
|
298
325
|
// The consumer's own blocks, LAST, so a later block wins — ESLint's rule. Parsed by
|
|
299
|
-
// `readConfig`; each carries the settings file's directory as its `basePath`.
|
|
300
|
-
|
|
326
|
+
// `readConfig`; each carries the settings file's directory as its `basePath`. Each is split so a
|
|
327
|
+
// rule reaches only the files its plugin is registered for: `paper` is a different plugin beside
|
|
328
|
+
// PIPELINE-STATUS.md than beside paper.tex, and ESLint throws on a rule its plugin lacks.
|
|
329
|
+
cfg.push(...(opts.rules ?? []).flatMap((b) => narrowToOwners(b, owners)));
|
|
301
330
|
return cfg;
|
|
302
331
|
}
|
|
303
332
|
|
|
@@ -310,6 +339,64 @@ export const SHIPPED_RULES: ReadonlySet<string> = shippedRuleIds(
|
|
|
310
339
|
[markdown],
|
|
311
340
|
);
|
|
312
341
|
|
|
342
|
+
/** A plugin as `rulePlugins` gives it: its rules, and for `tex` the LaTeX language when given. */
|
|
343
|
+
export interface RulePlugin {
|
|
344
|
+
readonly rules?: Readonly<Record<string, unknown>>;
|
|
345
|
+
readonly languages?: Readonly<Record<string, unknown>>;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Every plugin paperlint's config registers, ONE object per name, read off that config. In it one
|
|
350
|
+
* name is bound to different objects in different blocks (`paper` beside `PIPELINE-STATUS.md` is
|
|
351
|
+
* not `paper` beside `paper.tex`); here their rules are merged, so a config that lints any paper
|
|
352
|
+
* file knows every rule id. `@eslint/markdown` is a dependency's plugin and is left out. The LaTeX
|
|
353
|
+
* language rides on `tex` when `texLanguage` is given, and that is how a consumer registers it: a
|
|
354
|
+
* second `tex` plugin of its own is refused by ESLint ("Cannot redefine plugin "tex"", measured
|
|
355
|
+
* with and without the language here). Without it, `tex` carries rules only — for a config that
|
|
356
|
+
* lints markdown papers alone.
|
|
357
|
+
*/
|
|
358
|
+
export function rulePlugins(texLanguage?: unknown): Record<string, RulePlugin> {
|
|
359
|
+
const out: Record<string, { rules: Record<string, unknown> }> = {};
|
|
360
|
+
for (const block of buildConfig({}, texLanguage ?? { sentinel: "tex" }))
|
|
361
|
+
for (const [name, plugin] of Object.entries(
|
|
362
|
+
(block as { plugins?: Record<string, RulePlugin> }).plugins ?? {},
|
|
363
|
+
))
|
|
364
|
+
if (plugin !== markdown)
|
|
365
|
+
out[name] = { rules: { ...out[name]?.rules, ...plugin.rules } };
|
|
366
|
+
if (out["tex"] && texLanguage !== undefined)
|
|
367
|
+
return {
|
|
368
|
+
...out,
|
|
369
|
+
tex: { ...out["tex"], languages: { latex: texLanguage } },
|
|
370
|
+
};
|
|
371
|
+
return out;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* A flat-config fragment for a consumer's OWN ESLint run over paper files (#104): every rule
|
|
376
|
+
* paperlint ships, registered and turned off. Without it a `% eslint-disable-next-line
|
|
377
|
+
* paper/leading-zero -- why` — the documented escape hatch — fails that run with "Definition for
|
|
378
|
+
* rule 'paper/leading-zero' was not found". Off, because `paperlint lint` is where they run.
|
|
379
|
+
*
|
|
380
|
+
* The second block is not optional, and it is measured: a directive naming a rule that is off
|
|
381
|
+
* suppresses nothing, so ESLint reports it as an unused directive (a warning by default, an error
|
|
382
|
+
* under `reportUnusedDisableDirectives: "error"`). On the files paperlint lints that report is
|
|
383
|
+
* switched off in THIS run — `paperlint lint` still reports a directive that silences nothing.
|
|
384
|
+
*/
|
|
385
|
+
export function rulesOff(texLanguage?: unknown): Linter.Config[] {
|
|
386
|
+
return [
|
|
387
|
+
{
|
|
388
|
+
name: "paperlint/rules-off",
|
|
389
|
+
plugins: rulePlugins(texLanguage) as Linter.Config["plugins"],
|
|
390
|
+
rules: Object.fromEntries([...SHIPPED_RULES].map((id) => [id, "off"])),
|
|
391
|
+
},
|
|
392
|
+
{
|
|
393
|
+
name: "paperlint/rules-off/directives",
|
|
394
|
+
files: PAPER_FILE_PATTERNS,
|
|
395
|
+
linterOptions: { reportUnusedDisableDirectives: "off" },
|
|
396
|
+
},
|
|
397
|
+
];
|
|
398
|
+
}
|
|
399
|
+
|
|
313
400
|
/** Whether a rule entry (`"error"`, `2`, `["warn", {…}]`) turns the rule on. */
|
|
314
401
|
const isOn = (entry: unknown): boolean => {
|
|
315
402
|
const sev = Array.isArray(entry) ? entry[0] : entry;
|
|
@@ -333,12 +420,17 @@ export const OPTIONAL_RULES: ReadonlySet<string> = new Set(
|
|
|
333
420
|
* 🔴 AN OPTIONAL RULE THAT IS ON AND REACHES NO PAPER IS A GREEN ZERO. A `files` glob that matches
|
|
334
421
|
* nothing — a typo, a path relative to the wrong directory — leaves the rule never invoked, and a
|
|
335
422
|
* rule that never runs reports exactly like a rule that passed. So for every optional rule the
|
|
336
|
-
* consumer turned on, some
|
|
423
|
+
* consumer turned on, some `paper.tex` must actually have it enabled; the ones none has are
|
|
337
424
|
* returned, for the caller to refuse.
|
|
425
|
+
*
|
|
426
|
+
* `files` is every `paper.tex` the guard may count: the ones this run linted AND every paper of the
|
|
427
|
+
* project. Judged against the run alone, `paperlint lint papers/c` failed on a block written for
|
|
428
|
+
* papers a and b (#103) — a block that reaches a paper outside this run is not dead. A glob that
|
|
429
|
+
* reaches no paper of the project at all still fails, from a subset as from the whole.
|
|
338
430
|
*/
|
|
339
431
|
export async function silentOptionalRules(
|
|
340
432
|
eslint: ESLint,
|
|
341
|
-
|
|
433
|
+
files: readonly string[],
|
|
342
434
|
opts: PaperlintConfig,
|
|
343
435
|
): Promise<string[]> {
|
|
344
436
|
const turnedOn = new Set(
|
|
@@ -349,44 +441,52 @@ export async function silentOptionalRules(
|
|
|
349
441
|
),
|
|
350
442
|
);
|
|
351
443
|
const reached = new Set<string>();
|
|
352
|
-
for (const f of
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
for (const id of turnedOn) if (isOn(cfg
|
|
444
|
+
for (const f of new Set(files.filter((p) => basename(p) === MAIN))) {
|
|
445
|
+
// Undefined for a file outside ESLint's cwd or scope: it reaches nothing.
|
|
446
|
+
const cfg = (await eslint.calculateConfigForFile(f)) as
|
|
447
|
+
{ rules?: Record<string, unknown> } | undefined;
|
|
448
|
+
for (const id of turnedOn) if (isOn(cfg?.rules?.[id])) reached.add(id);
|
|
357
449
|
}
|
|
358
450
|
return [...turnedOn].filter((id) => !reached.has(id));
|
|
359
451
|
}
|
|
360
452
|
|
|
361
453
|
/**
|
|
362
|
-
* Every linted paper's
|
|
363
|
-
*
|
|
364
|
-
*
|
|
365
|
-
*
|
|
454
|
+
* Every linted paper's rules, as ESLint blocks scoped to that paper, in two groups: what its venue
|
|
455
|
+
* preset turns on, and what its own `paperlint.json` says. The caller puts the root's blocks between
|
|
456
|
+
* them. A file that does not parse, or names a rule paperlint does not ship, stops the run with one
|
|
457
|
+
* line naming the file — the same strictness as the root's own `rules`.
|
|
366
458
|
*/
|
|
367
|
-
export function paperRuleBlocks(
|
|
368
|
-
|
|
369
|
-
|
|
459
|
+
export function paperRuleBlocks(
|
|
460
|
+
paths: readonly string[],
|
|
461
|
+
): Parsed<{ preset: RuleBlock[]; own: RuleBlock[] }> {
|
|
462
|
+
// A FILE named on the command line belongs to the paper it sits in: that paper's settings apply.
|
|
463
|
+
const dirs = paths.map((p) =>
|
|
464
|
+
existsSync(p) && statSync(p).isFile() ? dirname(p) : p,
|
|
465
|
+
);
|
|
466
|
+
const papers = [...new Set(dirs.flatMap((p) => [p, ...papersIn(p)]))];
|
|
467
|
+
const out = { preset: [] as RuleBlock[], own: [] as RuleBlock[] };
|
|
370
468
|
for (const dir of papers) {
|
|
371
469
|
const p = paperPreset(dir, PRESET_DEPS);
|
|
372
470
|
if (p.kind === "settings-problem" && p.problem.kind === "broken")
|
|
373
471
|
return { ok: false, error: paperPresetProblem(dir, p) ?? dir };
|
|
374
|
-
const
|
|
375
|
-
if (!
|
|
376
|
-
|
|
472
|
+
const blocks = rulesOfPaper(dir, p);
|
|
473
|
+
if (!blocks.ok) return blocks;
|
|
474
|
+
out.preset.push(...blocks.value.preset);
|
|
475
|
+
out.own.push(...blocks.value.own);
|
|
377
476
|
}
|
|
378
477
|
return { ok: true, value: out };
|
|
379
478
|
}
|
|
380
479
|
|
|
381
480
|
/**
|
|
382
|
-
* One paper's
|
|
383
|
-
*
|
|
481
|
+
* One paper's blocks: its preset chain's `rules`, and its own — either `{ id: severity }` over the
|
|
482
|
+
* paper's files, or ESLint blocks with globs relative to the paper. A preset that does not resolve
|
|
483
|
+
* contributes nothing here; `pdf/profile` reports it on the paper.
|
|
384
484
|
*/
|
|
385
|
-
function rulesOfPaper(
|
|
485
|
+
function rulesOfPaper(
|
|
486
|
+
dir: string,
|
|
487
|
+
p: PaperPreset,
|
|
488
|
+
): Parsed<{ preset: RuleBlock[]; own: RuleBlock[] }> {
|
|
386
489
|
const settings = "settings" in p ? p.settings : null;
|
|
387
|
-
if (settings === null) return { ok: true, value: null };
|
|
388
|
-
const own = paperRules(dir, settings, SHIPPED_RULES);
|
|
389
|
-
if (!own.ok) return own;
|
|
390
490
|
const fromPreset =
|
|
391
491
|
p.kind === "resolved"
|
|
392
492
|
? parseRuleEntries(
|
|
@@ -396,38 +496,63 @@ function rulesOfPaper(dir: string, p: PaperPreset): Parsed<RuleBlock | null> {
|
|
|
396
496
|
)
|
|
397
497
|
: { ok: true as const, value: {} };
|
|
398
498
|
if (!fromPreset.ok) return fromPreset;
|
|
399
|
-
const
|
|
499
|
+
const own =
|
|
500
|
+
settings === null
|
|
501
|
+
? { ok: true as const, value: [] }
|
|
502
|
+
: Array.isArray(settings.rules)
|
|
503
|
+
? parseRuleBlocks(
|
|
504
|
+
settings.rules,
|
|
505
|
+
join(dir, CONFIG_FILE),
|
|
506
|
+
SHIPPED_RULES,
|
|
507
|
+
dir,
|
|
508
|
+
)
|
|
509
|
+
: ownRules(dir, settings);
|
|
510
|
+
if (!own.ok) return own;
|
|
511
|
+
// No `files`: `narrowToOwners` scopes the block to what paperlint lints under `dir`, exceptions
|
|
512
|
+
// included. A copy of the globs here is how the siblings index got parsed as JavaScript (#101).
|
|
513
|
+
const scoped = (rules: Record<string, RuleEntry>): RuleBlock[] =>
|
|
514
|
+
Object.keys(rules).length ? [{ basePath: dir, rules }] : [];
|
|
515
|
+
return {
|
|
516
|
+
ok: true,
|
|
517
|
+
value: { preset: scoped(fromPreset.value), own: [...own.value] },
|
|
518
|
+
};
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
/** A paper's `{ id: severity }` rules, as one block over the paper's files (narrowed later, #101). */
|
|
522
|
+
function ownRules(
|
|
523
|
+
dir: string,
|
|
524
|
+
settings: PaperSettings,
|
|
525
|
+
): Parsed<readonly RuleBlock[]> {
|
|
526
|
+
const own = paperRules(dir, settings, SHIPPED_RULES);
|
|
527
|
+
if (!own.ok) return own;
|
|
400
528
|
return {
|
|
401
529
|
ok: true,
|
|
402
|
-
value:
|
|
403
|
-
? { basePath: dir, files: PAPER_FILE_PATTERNS, rules }
|
|
404
|
-
: null,
|
|
530
|
+
value: own.value ? [{ basePath: dir, rules: own.value }] : [],
|
|
405
531
|
};
|
|
406
532
|
}
|
|
407
533
|
|
|
408
534
|
/**
|
|
409
|
-
* The
|
|
410
|
-
*
|
|
411
|
-
* language — `paperlint.json` itself would be parsed as JavaScript.
|
|
535
|
+
* The globs paperlint lints, for the refusal message only. Which files a block reaches is decided by
|
|
536
|
+
* `narrowToOwners` from `ownedScopes` (src/paper-files.ts), where each glob keeps its exceptions.
|
|
412
537
|
*/
|
|
413
|
-
const PAPER_FILE_PATTERNS: string[] =
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
(b) => (b as { files?: string[] }).files ?? [],
|
|
417
|
-
),
|
|
418
|
-
),
|
|
419
|
-
];
|
|
538
|
+
const PAPER_FILE_PATTERNS: string[] = ownedPatterns(
|
|
539
|
+
buildConfig({}, { sentinel: "tex language" }),
|
|
540
|
+
);
|
|
420
541
|
|
|
421
542
|
/**
|
|
422
|
-
* The
|
|
423
|
-
*
|
|
543
|
+
* The root `paperlint.json` after the boundary: an unknown key is refused by name, the paper
|
|
544
|
+
* defaults (`extends`, `kind`, `pdf`) are checked the way a paper's own are, and `rules` becomes
|
|
545
|
+
* parsed config blocks. Nothing after this sees the raw object.
|
|
424
546
|
*/
|
|
425
547
|
export function parseSettings(
|
|
426
|
-
|
|
548
|
+
json: unknown,
|
|
427
549
|
where: string,
|
|
428
550
|
baseDir: string,
|
|
429
551
|
): Parsed<PaperlintConfig> {
|
|
430
|
-
|
|
552
|
+
if (typeof json !== "object" || json === null || Array.isArray(json))
|
|
553
|
+
return { ok: false, error: `${where}: must be a JSON object` };
|
|
554
|
+
const raw = json as Record<string, unknown>;
|
|
555
|
+
const opts = raw as PaperlintConfig;
|
|
431
556
|
const unknown = unknownKeys(raw);
|
|
432
557
|
if (unknown.length > 0)
|
|
433
558
|
return {
|
|
@@ -436,15 +561,17 @@ export function parseSettings(
|
|
|
436
561
|
`${where}: unknown key${unknown.length > 1 ? "s" : ""} ${unknown.map((k) => `"${k}"`).join(", ")} — ` +
|
|
437
562
|
`a typo would otherwise read as "not set". Known keys: ${Object.keys(SETTINGS_KEYS).join(", ")}`,
|
|
438
563
|
};
|
|
564
|
+
const defaults = stringFields(raw);
|
|
565
|
+
if (!defaults.ok) return { ok: false, error: `${where}: ${defaults.error}` };
|
|
439
566
|
const rules = parseRuleBlocks(raw["rules"], where, SHIPPED_RULES, baseDir);
|
|
440
567
|
if (!rules.ok) return rules;
|
|
441
568
|
return { ok: true, value: { ...opts, rules: rules.value } };
|
|
442
569
|
}
|
|
443
570
|
|
|
444
571
|
/**
|
|
445
|
-
* The directory ESLint runs from. ESLint ignores every file outside it (#48), and
|
|
446
|
-
* `
|
|
447
|
-
*
|
|
572
|
+
* The directory ESLint runs from. ESLint ignores every file outside it (#48), and the consumer's
|
|
573
|
+
* `rules` globs are written relative to the config's directory. So: the deepest directory holding
|
|
574
|
+
* the config's directory and every path. With the
|
|
448
575
|
* papers inside the config's directory, that is the config's directory itself.
|
|
449
576
|
*/
|
|
450
577
|
const lintRoot = (home: string, paths: readonly string[]): string =>
|
|
@@ -467,6 +594,7 @@ export function parseArgs(argv: readonly string[]): Args {
|
|
|
467
594
|
paths: [],
|
|
468
595
|
config: null,
|
|
469
596
|
json: false,
|
|
597
|
+
fix: false,
|
|
470
598
|
all: false,
|
|
471
599
|
dryRun: false,
|
|
472
600
|
check: false,
|
|
@@ -474,6 +602,8 @@ export function parseArgs(argv: readonly string[]): Args {
|
|
|
474
602
|
noHooks: false,
|
|
475
603
|
paper: null,
|
|
476
604
|
format: null,
|
|
605
|
+
venue: null,
|
|
606
|
+
kind: null,
|
|
477
607
|
hooksMode: null,
|
|
478
608
|
// -1 = warnings NEVER fail the run. In this set most findings are advisory by design, and a
|
|
479
609
|
// gate that fails on advice gets muted entirely.
|
|
@@ -495,9 +625,23 @@ export function parseArgs(argv: readonly string[]): Args {
|
|
|
495
625
|
};
|
|
496
626
|
|
|
497
627
|
for (let i = 0; i < rest.length; i++) {
|
|
498
|
-
const
|
|
499
|
-
if (
|
|
628
|
+
const arg = rest[i];
|
|
629
|
+
if (arg === undefined) continue;
|
|
630
|
+
// `--flag=value` is `--flag value` for every flag that takes a value: the CI action writes
|
|
631
|
+
// `--max-warnings="$N"`, and that form used to fall through to the list of paths.
|
|
632
|
+
const eq = arg.indexOf("=");
|
|
633
|
+
const inline =
|
|
634
|
+
arg.startsWith("--") && eq > 0 && VALUE_FLAGS.has(arg.slice(0, eq))
|
|
635
|
+
? arg.slice(eq + 1)
|
|
636
|
+
: undefined;
|
|
637
|
+
const a = inline === undefined ? arg : arg.slice(0, eq);
|
|
638
|
+
const take = (): string | undefined => {
|
|
639
|
+
if (inline === undefined) return valueFor(a, ++i);
|
|
640
|
+
if (inline === "") out.missingValue = a;
|
|
641
|
+
return inline === "" ? undefined : inline;
|
|
642
|
+
};
|
|
500
643
|
if (a === "--json") out.json = true;
|
|
644
|
+
else if (a === "--fix") out.fix = true;
|
|
501
645
|
else if (a === "--all") out.all = true;
|
|
502
646
|
else if (a === "--dry-run") out.dryRun = true;
|
|
503
647
|
else if (a === "--check") out.check = true;
|
|
@@ -505,23 +649,35 @@ export function parseArgs(argv: readonly string[]): Args {
|
|
|
505
649
|
else if (a === "--no-hooks") out.noHooks = true;
|
|
506
650
|
else if (a.startsWith("--hooks="))
|
|
507
651
|
out.hooksMode = a.slice("--hooks=".length);
|
|
508
|
-
else if (a === "--paper") out.paper =
|
|
509
|
-
else if (a === "--format") out.format =
|
|
652
|
+
else if (a === "--paper") out.paper = take() ?? null;
|
|
653
|
+
else if (a === "--format") out.format = take() ?? null;
|
|
654
|
+
else if (a === "--venue") out.venue = take() ?? null;
|
|
655
|
+
else if (a === "--kind") out.kind = take() ?? null;
|
|
510
656
|
// `--options` was the first spelling and is kept working. It named the wrong thing — every
|
|
511
657
|
// other tool in the stack calls this file its config — but a flag in someone's CI is not
|
|
512
658
|
// ours to break.
|
|
513
|
-
else if (a === "--config" || a === "--options")
|
|
514
|
-
out.config = valueFor(a, ++i) ?? null;
|
|
659
|
+
else if (a === "--config" || a === "--options") out.config = take() ?? null;
|
|
515
660
|
else if (a === "--max-warnings") {
|
|
516
|
-
const v =
|
|
661
|
+
const v = take();
|
|
517
662
|
if (v !== undefined) out.maxWarnings = Number(v);
|
|
518
663
|
} else if (a === "--help" || a === "-h") out.help = true;
|
|
664
|
+
// An unknown flag is refused by name. Read as a path, it silently lints something else.
|
|
665
|
+
else if (a.startsWith("-") && a !== "-") out.unknownFlag ??= a;
|
|
519
666
|
else out.paths.push(a);
|
|
520
667
|
}
|
|
521
668
|
return out;
|
|
522
669
|
}
|
|
523
670
|
|
|
524
|
-
|
|
671
|
+
/** The flags that take a value, in either spelling: `--flag value` or `--flag=value`. */
|
|
672
|
+
const VALUE_FLAGS: ReadonlySet<string> = new Set([
|
|
673
|
+
"--paper",
|
|
674
|
+
"--format",
|
|
675
|
+
"--venue",
|
|
676
|
+
"--kind",
|
|
677
|
+
"--config",
|
|
678
|
+
"--options",
|
|
679
|
+
"--max-warnings",
|
|
680
|
+
]);
|
|
525
681
|
|
|
526
682
|
/**
|
|
527
683
|
* This package's own version, from the `package.json` beside `src/` and `dist/` alike. `init` pins
|
|
@@ -539,46 +695,17 @@ function ownVersion(): string | undefined {
|
|
|
539
695
|
}
|
|
540
696
|
}
|
|
541
697
|
|
|
542
|
-
/**
|
|
543
|
-
* 🔴 THE CLI READS `package.json`, THE ONE DECLARATION. `paperlint init` writes the settings under
|
|
544
|
-
* the `package.json` key that the three hooks and `eslint-rules` already read; the CLI reads the
|
|
545
|
-
* same file, so the install and the check cannot look at different files (issue #33,
|
|
546
|
-
* `docs/install.md`).
|
|
547
|
-
*
|
|
548
|
-
* The walk goes up to the filesystem root, the way eslint, prettier and tsc find theirs, so a run
|
|
549
|
-
* from inside one paper sees the same settings as a run from the repository root.
|
|
550
|
-
*/
|
|
551
|
-
export function findConfig(startDir: string): string | null {
|
|
552
|
-
let dir = resolve(startDir);
|
|
553
|
-
for (;;) {
|
|
554
|
-
const pkg = join(dir, PKG_NAME);
|
|
555
|
-
if (existsSync(pkg) && declaresSettings(pkg)) return pkg;
|
|
556
|
-
const up = dirname(dir);
|
|
557
|
-
if (up === dir) return null;
|
|
558
|
-
dir = up;
|
|
559
|
-
}
|
|
560
|
-
}
|
|
561
|
-
|
|
562
|
-
/**
|
|
563
|
-
* A `package.json` WITHOUT the key is not a declaration and must not stop the walk — every
|
|
564
|
-
* project on the way up has one, so stopping there would make the search find nothing, always.
|
|
565
|
-
* An unparsable one is treated the same way here; `paperlint doctor` is the command that reports it.
|
|
566
|
-
*/
|
|
567
|
-
const declaresSettings = (pkgPath: string): boolean => {
|
|
568
|
-
try {
|
|
569
|
-
const d = declaredSettings(JSON.parse(readFileSync(pkgPath, "utf8")));
|
|
570
|
-
return d.settings !== undefined || d.conflict !== null;
|
|
571
|
-
} catch {
|
|
572
|
-
return false;
|
|
573
|
-
}
|
|
574
|
-
};
|
|
575
|
-
|
|
576
698
|
/**
|
|
577
699
|
* Reading the config, ONE reader for all commands. Pulled out of `run()` the moment a second
|
|
578
700
|
* command needed the same config (`build`): two copies of this block would have drifted apart on
|
|
579
701
|
* the very first edit — exactly the class that already cost us the empty-set guard in two places.
|
|
580
702
|
*
|
|
581
|
-
*
|
|
703
|
+
* 🔴 THE CONFIG FINDS ITSELF: the project root is the nearest directory up from `cwd` with a root
|
|
704
|
+
* `paperlint.json`, else with a `package.json`, else `cwd` (`findProjectRoot`). The file is
|
|
705
|
+
* optional — without one every setting has its default, and papers are in `papers/`. An explicit
|
|
706
|
+
* `--config` beats the discovered one: it was named out loud, and a substitution is never silent.
|
|
707
|
+
*
|
|
708
|
+
* @returns `{ opts, configPath, root }` on success, or `{ code }` — and then the caller exits with it.
|
|
582
709
|
*/
|
|
583
710
|
export function readConfig(
|
|
584
711
|
a: Args,
|
|
@@ -592,85 +719,60 @@ export function readConfig(
|
|
|
592
719
|
cwd?: string;
|
|
593
720
|
} = {},
|
|
594
721
|
): ConfigRead {
|
|
595
|
-
|
|
596
|
-
// loud, and a substitution is never silent. Either way the file has the shape of a
|
|
597
|
-
// `package.json`: the settings sit under the key.
|
|
598
|
-
const configPath = a.config ?? findConfig(cwd);
|
|
599
|
-
if (a.config && !existsSync(a.config)) {
|
|
722
|
+
if (a.config && !existsSync(resolve(cwd, a.config))) {
|
|
600
723
|
err(`config file not found: ${a.config}`);
|
|
601
724
|
return { code: 2 };
|
|
602
725
|
}
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
}
|
|
615
|
-
const d = declaredSettings(parsed);
|
|
616
|
-
if (d.conflict !== null) {
|
|
617
|
-
err(d.conflict);
|
|
618
|
-
return { code: 2 };
|
|
619
|
-
}
|
|
620
|
-
opts = (d.settings ?? {}) as PaperlintConfig;
|
|
621
|
-
legacyKey = d.legacy;
|
|
622
|
-
// The discovered config is NAMED out loud. Otherwise a run from someone else's directory picks
|
|
623
|
-
// up someone else's file and does not say so — and a typography-debt mismatch looks like a finding.
|
|
624
|
-
//
|
|
625
|
-
// 🔴 IN `--json` MODE — TO stderr. Machine output must be ONE parsable document: a line before
|
|
626
|
-
// the array breaks any `| jq`, and it breaks it for the consumer, not for us. Caught not by a
|
|
627
|
-
// test but by an attempt to wire our own action to this output; in the harness I first WORKED
|
|
628
|
-
// AROUND this line (stripped the first line before JSON.parse) — that is, the workaround hid
|
|
629
|
-
// the defect exactly where it should have been shouting.
|
|
630
|
-
(a.json ? err : log)(`config: ${relative(cwd, configPath) || PKG_NAME}`);
|
|
631
|
-
if (legacyKey) (a.json ? err : log)(` ⚠ ${LEGACY_KEY_MESSAGE}`);
|
|
632
|
-
}
|
|
633
|
-
|
|
634
|
-
// The old field name is refused before anything else is read from the settings: falling back
|
|
635
|
-
// to it would keep it working forever, and this package has no released users to migrate.
|
|
636
|
-
const where = `${configPath ? basename(configPath) : PKG_NAME} → "${legacyKey ? LEGACY_CONFIG_KEY : CONFIG_KEY}"`;
|
|
637
|
-
const renamed = configPath ? renamedFieldMessage(opts, where) : null;
|
|
638
|
-
if (renamed) {
|
|
639
|
-
err(renamed);
|
|
726
|
+
const root = a.config
|
|
727
|
+
? dirname(resolve(cwd, a.config))
|
|
728
|
+
: findProjectRoot(cwd);
|
|
729
|
+
const found = a.config ? resolve(cwd, a.config) : join(root, CONFIG_FILE);
|
|
730
|
+
const configPath = existsSync(found) ? found : null;
|
|
731
|
+
if (configPath === null) return { opts: {}, configPath, root };
|
|
732
|
+
|
|
733
|
+
let parsed: unknown;
|
|
734
|
+
try {
|
|
735
|
+
parsed = JSON.parse(readFileSync(configPath, "utf8"));
|
|
736
|
+
} catch (e) {
|
|
737
|
+
err(`${configPath} is not valid JSON: ${(e as Error).message}`);
|
|
640
738
|
return { code: 2 };
|
|
641
739
|
}
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
// 🔴 THE PAPERS DIRECTORY IS A REQUIRED FIELD. The papers directory is the one thing without which the tool
|
|
656
|
-
// does not know what it works on, and the one thing that cannot be guessed: a default of "." runs
|
|
657
|
-
// the rules over the whole checkout and exits green over a scope nobody chose.
|
|
658
|
-
if (configPath && !hasPapers(opts)) {
|
|
659
|
-
err(
|
|
660
|
-
`${configPath} must declare \`${PAPERS_DIR_FIELD}\` — the directory your papers live in, e.g.\n` +
|
|
661
|
-
` { "${CONFIG_KEY}": { "${PAPERS_DIR_FIELD}": "papers" } }\n` +
|
|
662
|
-
`It is the one thing this tool cannot guess. \`npx paperlint init\` writes it for you.`,
|
|
663
|
-
);
|
|
740
|
+
// The discovered config is NAMED out loud. Otherwise a run from someone else's directory picks
|
|
741
|
+
// up someone else's file and does not say so — and its settings then look like findings.
|
|
742
|
+
//
|
|
743
|
+
// 🔴 IN `--json` MODE — TO stderr. Machine output must be ONE parsable document: a line before
|
|
744
|
+
// the array breaks any `| jq`, and it breaks it for the consumer, not for us.
|
|
745
|
+
(a.json ? err : log)(`config: ${relative(cwd, configPath) || CONFIG_FILE}`);
|
|
746
|
+
const settings = parseSettings(
|
|
747
|
+
parsed,
|
|
748
|
+
relative(cwd, configPath) || CONFIG_FILE,
|
|
749
|
+
root,
|
|
750
|
+
);
|
|
751
|
+
if (!settings.ok) {
|
|
752
|
+
err(settings.error);
|
|
664
753
|
return { code: 2 };
|
|
665
754
|
}
|
|
666
|
-
return { opts, configPath };
|
|
755
|
+
return { opts: settings.value, configPath, root };
|
|
667
756
|
}
|
|
668
757
|
|
|
669
|
-
/** The papers directory field of the settings, read by its one declared name. */
|
|
758
|
+
/** The papers directory field of the settings, read by its one declared name; the default when absent. */
|
|
670
759
|
export function papersDirOf(opts: PaperlintConfig): unknown {
|
|
671
|
-
|
|
760
|
+
const declared = (opts as Record<string, unknown>)[PAPERS_DIR_FIELD];
|
|
761
|
+
return declared === undefined ? DEFAULT_PAPERS_ROOT : declared;
|
|
672
762
|
}
|
|
673
763
|
|
|
764
|
+
/** The error for a papers directory that is missing or holds no paper. */
|
|
765
|
+
export const noPapersMessage = (dir: string): string =>
|
|
766
|
+
`no papers in ${dir}/ — create one with \`npx paperlint new <name>\`, or set "${PAPERS_DIR_FIELD}" ` +
|
|
767
|
+
`in ${CONFIG_FILE} if your papers live elsewhere`;
|
|
768
|
+
|
|
769
|
+
/** The papers directories of a read config, absolute: each relative to the project root. */
|
|
770
|
+
export const papersRoots = (cfg: {
|
|
771
|
+
opts: PaperlintConfig;
|
|
772
|
+
root: string;
|
|
773
|
+
}): string[] =>
|
|
774
|
+
toPaths(papersDirOf(cfg.opts)).map((rel) => resolve(cfg.root, rel));
|
|
775
|
+
|
|
674
776
|
/** The papers directory may be one directory or several; both spellings normalise to a list. */
|
|
675
777
|
export function toPaths(papers: unknown): string[] {
|
|
676
778
|
if (typeof papers === "string") return papers.trim() ? [papers.trim()] : [];
|
|
@@ -679,9 +781,6 @@ export function toPaths(papers: unknown): string[] {
|
|
|
679
781
|
return [];
|
|
680
782
|
}
|
|
681
783
|
|
|
682
|
-
const hasPapers = (opts: PaperlintConfig): boolean =>
|
|
683
|
-
toPaths(papersDirOf(opts)).length > 0;
|
|
684
|
-
|
|
685
784
|
/**
|
|
686
785
|
* `paperlint hook <name>` — run an editor hook. It exists for ONE thing: so that the wiring does not
|
|
687
786
|
* address the runtime from the project root.
|
|
@@ -776,16 +875,143 @@ export async function createPaperAt(
|
|
|
776
875
|
log,
|
|
777
876
|
err,
|
|
778
877
|
cwd,
|
|
779
|
-
|
|
878
|
+
venue = null,
|
|
879
|
+
}: {
|
|
880
|
+
log: typeof console.log;
|
|
881
|
+
err: typeof console.error;
|
|
882
|
+
cwd: string;
|
|
883
|
+
/** What `--venue` chose; null writes the template's `paperlint.json` as it is. */
|
|
884
|
+
venue?: VenueChoice | null;
|
|
885
|
+
},
|
|
780
886
|
): Promise<number> {
|
|
781
|
-
const result = newPaper(papersRoot, name, format);
|
|
887
|
+
const result = newPaper(papersRoot, name, format, { venue });
|
|
782
888
|
const here = (p: string): string => relative(cwd, p) || p;
|
|
783
889
|
for (const line of reportNewPaper(result, here)) log(line);
|
|
784
890
|
if (!result.ok) return 2;
|
|
891
|
+
const config = here(join(result.dir, CONFIG_FILE));
|
|
892
|
+
if (venue === null)
|
|
893
|
+
log(
|
|
894
|
+
` venue: none yet — set "extends" in ${config}, or next time: paperlint new <name> --venue <preset> (${SHIPPED_VENUES().join(", ")})`,
|
|
895
|
+
);
|
|
896
|
+
else if (venue.kind === null && venue.kinds.length > 0)
|
|
897
|
+
log(
|
|
898
|
+
` kind: not set — \`${venue.label}\` sets a page limit per kind (${venue.kinds.join(", ")}); add "kind" to ${config}. Until then lint reports pdf/profile`,
|
|
899
|
+
);
|
|
785
900
|
log(``);
|
|
786
901
|
return run(["lint", result.dir], { log, err, cwd });
|
|
787
902
|
}
|
|
788
903
|
|
|
904
|
+
/** A venue `paperlint new` will write, with what the messages need to say about it. */
|
|
905
|
+
export interface VenueChoice extends VenueSetting {
|
|
906
|
+
/** The preset's word in messages (`agenticdev`, `my-workshop`). */
|
|
907
|
+
readonly label: string;
|
|
908
|
+
/** The preset's kinds; empty when it sets no page limit. */
|
|
909
|
+
readonly kinds: readonly string[];
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
/**
|
|
913
|
+
* `--venue` / `--kind` → the `extends` and `kind` to write into `<paperDir>/paperlint.json`, or why
|
|
914
|
+
* not. Parsed here, at the boundary, and resolved through the same `resolvePreset` lint uses — so a
|
|
915
|
+
* venue `new` accepts is a venue lint resolves. On a terminal with no `--venue` it asks, with
|
|
916
|
+
* "none" as the default; without a terminal it chooses nothing (null), as before.
|
|
917
|
+
*
|
|
918
|
+
* A path is relative to where the command runs, because that is where it was typed; it is written
|
|
919
|
+
* relative to the paper's `paperlint.json`, because that is what `extends` is relative to. Written
|
|
920
|
+
* as typed, `./venues/x.jsonc` would name `<paper>/venues/x.jsonc`.
|
|
921
|
+
*/
|
|
922
|
+
export async function chooseVenue(
|
|
923
|
+
flags: { readonly venue: string | null; readonly kind: string | null },
|
|
924
|
+
{
|
|
925
|
+
paperDir,
|
|
926
|
+
cwd,
|
|
927
|
+
interactive,
|
|
928
|
+
ask,
|
|
929
|
+
}: {
|
|
930
|
+
paperDir: string;
|
|
931
|
+
cwd: string;
|
|
932
|
+
interactive: boolean;
|
|
933
|
+
ask: (q: string) => Promise<string>;
|
|
934
|
+
},
|
|
935
|
+
): Promise<Parsed<VenueChoice | null>> {
|
|
936
|
+
const shipped = SHIPPED_VENUES();
|
|
937
|
+
if (flags.kind !== null && flags.venue === null)
|
|
938
|
+
return bad(
|
|
939
|
+
`--kind needs --venue: a kind is a page limit of one venue preset — \`paperlint new <name> --venue <preset> --kind ${flags.kind}\``,
|
|
940
|
+
);
|
|
941
|
+
const asked = async (q: string): Promise<string> =>
|
|
942
|
+
(await ask(q).catch(() => "")).trim();
|
|
943
|
+
const venue =
|
|
944
|
+
flags.venue ??
|
|
945
|
+
(interactive
|
|
946
|
+
? await asked(`venue: ${[...shipped, "none"].join(" / ")} [none] `)
|
|
947
|
+
: "");
|
|
948
|
+
if (venue === "" || (flags.venue === null && venue === "none"))
|
|
949
|
+
return { ok: true, value: null };
|
|
950
|
+
const spec = venueSpec(venue, { shipped, paperDir, cwd });
|
|
951
|
+
if (!spec.ok) return spec;
|
|
952
|
+
const r = resolvePreset(spec.value, join(paperDir, CONFIG_FILE), PRESET_DEPS);
|
|
953
|
+
if (!r.ok) return bad(`--venue ${venue}: ${presetProblemText(r.error)}`);
|
|
954
|
+
const kinds = [...r.value.format.kinds.keys()];
|
|
955
|
+
const label = r.value.label;
|
|
956
|
+
if (flags.kind !== null) {
|
|
957
|
+
if (kinds.length === 0)
|
|
958
|
+
return bad(
|
|
959
|
+
`--kind ${flags.kind}: \`${label}\` has no kinds — it sets no page limit, so there is no kind to choose`,
|
|
960
|
+
);
|
|
961
|
+
if (!kinds.includes(flags.kind))
|
|
962
|
+
return bad(
|
|
963
|
+
`--kind ${flags.kind}: \`${label}\` has no kind \`${flags.kind}\`; its kinds: ${kinds.join(", ")}`,
|
|
964
|
+
);
|
|
965
|
+
}
|
|
966
|
+
const kind =
|
|
967
|
+
flags.kind ??
|
|
968
|
+
(interactive && kinds.length > 0
|
|
969
|
+
? await asked(`kind: ${[...kinds, "later"].join(" / ")} [later] `)
|
|
970
|
+
: null);
|
|
971
|
+
return {
|
|
972
|
+
ok: true,
|
|
973
|
+
value: {
|
|
974
|
+
extends: spec.value,
|
|
975
|
+
kind: kind !== null && kinds.includes(kind) ? kind : null,
|
|
976
|
+
label,
|
|
977
|
+
kinds,
|
|
978
|
+
},
|
|
979
|
+
};
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
const bad = (error: string): { ok: false; error: string } => ({
|
|
983
|
+
ok: false,
|
|
984
|
+
error,
|
|
985
|
+
});
|
|
986
|
+
|
|
987
|
+
/** What `--venue` names → the `extends` value, relative to `<paperDir>/paperlint.json`. */
|
|
988
|
+
function venueSpec(
|
|
989
|
+
venue: string,
|
|
990
|
+
{
|
|
991
|
+
shipped,
|
|
992
|
+
paperDir,
|
|
993
|
+
cwd,
|
|
994
|
+
}: { shipped: readonly string[]; paperDir: string; cwd: string },
|
|
995
|
+
): Parsed<string> {
|
|
996
|
+
if (venue.startsWith("./") || venue.startsWith("../")) {
|
|
997
|
+
const file = resolve(cwd, venue);
|
|
998
|
+
if (!existsSync(file))
|
|
999
|
+
return bad(
|
|
1000
|
+
`--venue ${venue}: no such file (${file}) — a path is relative to where you run the command`,
|
|
1001
|
+
);
|
|
1002
|
+
const rel = relative(paperDir, file).split(sep).join("/");
|
|
1003
|
+
return { ok: true, value: rel.startsWith("../") ? rel : `./${rel}` };
|
|
1004
|
+
}
|
|
1005
|
+
const name = venue.startsWith(SHIPPED_PREFIX)
|
|
1006
|
+
? venue.slice(SHIPPED_PREFIX.length)
|
|
1007
|
+
: venue;
|
|
1008
|
+
if (!shipped.includes(name))
|
|
1009
|
+
return bad(
|
|
1010
|
+
`--venue ${venue}: no such venue preset. Shipped: ${shipped.join(", ")} — or a path to your own preset, starting with ./ or ../`,
|
|
1011
|
+
);
|
|
1012
|
+
return { ok: true, value: `${SHIPPED_PREFIX}${name}` };
|
|
1013
|
+
}
|
|
1014
|
+
|
|
789
1015
|
/**
|
|
790
1016
|
* `paperlint new <name>` — the papers directory comes from the same declaration every other command
|
|
791
1017
|
* reads; there is no second way to name it. See `new-paper.ts` for what it writes and why.
|
|
@@ -807,7 +1033,7 @@ async function runNew(
|
|
|
807
1033
|
const [name, ...extra] = a.paths;
|
|
808
1034
|
if (!name || extra.length > 0) {
|
|
809
1035
|
err(
|
|
810
|
-
`\`new\` takes exactly one paper name: \`paperlint new my-paper [--format tex|md]\``,
|
|
1036
|
+
`\`new\` takes exactly one paper name: \`paperlint new my-paper [--venue <preset>] [--kind <kind>] [--format tex|md]\``,
|
|
811
1037
|
);
|
|
812
1038
|
return 2;
|
|
813
1039
|
}
|
|
@@ -828,14 +1054,11 @@ async function runNew(
|
|
|
828
1054
|
}
|
|
829
1055
|
const cfg = readConfig({ ...a, json: false }, { log: () => {}, err, cwd });
|
|
830
1056
|
if (cfg.code !== undefined) return cfg.code;
|
|
831
|
-
const roots =
|
|
832
|
-
resolve(dirname(cfg.configPath ?? cwd), rel),
|
|
833
|
-
);
|
|
1057
|
+
const roots = papersRoots(cfg);
|
|
834
1058
|
const papersRoot = roots[0];
|
|
835
|
-
if (
|
|
1059
|
+
if (papersRoot === undefined) {
|
|
836
1060
|
err(
|
|
837
|
-
`no
|
|
838
|
-
`Run \`npx paperlint init\` first — it declares the directory in package.json.`,
|
|
1061
|
+
`"${PAPERS_DIR_FIELD}" names no directory, so there is nowhere to put \`${name}\`.`,
|
|
839
1062
|
);
|
|
840
1063
|
return 2;
|
|
841
1064
|
}
|
|
@@ -843,7 +1066,30 @@ async function runNew(
|
|
|
843
1066
|
log(
|
|
844
1067
|
`several papers directories are declared — using the first: ${relative(cwd, papersRoot) || papersRoot}`,
|
|
845
1068
|
);
|
|
846
|
-
|
|
1069
|
+
const paperDir = join(papersRoot, name);
|
|
1070
|
+
// An existing paperlint.json is never overwritten, so a venue for it is refused, not dropped.
|
|
1071
|
+
const hasConfig = existsSync(join(paperDir, CONFIG_FILE));
|
|
1072
|
+
if (hasConfig && (a.venue !== null || a.kind !== null)) {
|
|
1073
|
+
err(
|
|
1074
|
+
`${relative(cwd, join(paperDir, CONFIG_FILE))} already exists and is never overwritten — set "extends" and "kind" in it by hand`,
|
|
1075
|
+
);
|
|
1076
|
+
return 2;
|
|
1077
|
+
}
|
|
1078
|
+
const venue = hasConfig
|
|
1079
|
+
? { ok: true as const, value: null }
|
|
1080
|
+
: await chooseVenue(a, {
|
|
1081
|
+
paperDir,
|
|
1082
|
+
cwd,
|
|
1083
|
+
interactive: processInteractivity(a.yes).interactive,
|
|
1084
|
+
ask,
|
|
1085
|
+
});
|
|
1086
|
+
if (!venue.ok) return (err(venue.error), 2);
|
|
1087
|
+
return createPaperAt(papersRoot, name, format, {
|
|
1088
|
+
log,
|
|
1089
|
+
err,
|
|
1090
|
+
cwd,
|
|
1091
|
+
venue: venue.value,
|
|
1092
|
+
});
|
|
847
1093
|
}
|
|
848
1094
|
|
|
849
1095
|
/**
|
|
@@ -863,16 +1109,7 @@ async function runBuild(
|
|
|
863
1109
|
): Promise<number> {
|
|
864
1110
|
const cfg = readConfig(a, { log, err, cwd });
|
|
865
1111
|
if (cfg.code !== undefined) return cfg.code;
|
|
866
|
-
const
|
|
867
|
-
// The key that used to name the scripts to run. It is read by nothing now; saying so beats a
|
|
868
|
-
// setting that silently stopped doing anything.
|
|
869
|
-
if (opts.buildScripts !== undefined)
|
|
870
|
-
log(
|
|
871
|
-
`note: "buildScripts" in ${relative(cwd, configPath ?? "") || "the settings"} is ignored — paperlint builds the paper itself`,
|
|
872
|
-
);
|
|
873
|
-
const roots = toPaths(papersDirOf(opts)).map((rel) =>
|
|
874
|
-
resolve(configPath ? dirname(configPath) : cwd, rel),
|
|
875
|
-
);
|
|
1112
|
+
const roots = papersRoots(cfg);
|
|
876
1113
|
|
|
877
1114
|
let targets;
|
|
878
1115
|
if (a.all) {
|
|
@@ -900,6 +1137,7 @@ async function runBuild(
|
|
|
900
1137
|
cwd,
|
|
901
1138
|
dryRun: a.dryRun,
|
|
902
1139
|
log,
|
|
1140
|
+
checkReferences: onlineReferences,
|
|
903
1141
|
engine: () => engineEnv(targets, a, { log, err }),
|
|
904
1142
|
});
|
|
905
1143
|
if (out.kind === "no-engine") return 1;
|
|
@@ -931,12 +1169,7 @@ export function toolchainTex(cwd: string): TexRequirements {
|
|
|
931
1169
|
err: () => {},
|
|
932
1170
|
cwd,
|
|
933
1171
|
});
|
|
934
|
-
const roots =
|
|
935
|
-
cfg.code === undefined
|
|
936
|
-
? toPaths(papersDirOf(cfg.opts)).map((rel) =>
|
|
937
|
-
resolve(dirname(cfg.configPath ?? cwd), rel),
|
|
938
|
-
)
|
|
939
|
-
: [];
|
|
1172
|
+
const roots = cfg.code === undefined ? papersRoots(cfg) : [];
|
|
940
1173
|
const chains = roots
|
|
941
1174
|
.flatMap((r) => papersIn(r))
|
|
942
1175
|
.map((dir) => paperPreset(dir, PRESET_DEPS))
|
|
@@ -1005,6 +1238,61 @@ function hostBanalInstaller(): ToolInstaller {
|
|
|
1005
1238
|
return banalInstaller({ ...ports, download }, s);
|
|
1006
1239
|
}
|
|
1007
1240
|
|
|
1241
|
+
/** `paperlint init`: its flags checked here, the install itself in `init.ts`. */
|
|
1242
|
+
async function runInit(
|
|
1243
|
+
a: Args,
|
|
1244
|
+
{
|
|
1245
|
+
log,
|
|
1246
|
+
err,
|
|
1247
|
+
cwd,
|
|
1248
|
+
}: { log: typeof console.log; err: typeof console.error; cwd: string },
|
|
1249
|
+
): Promise<number> {
|
|
1250
|
+
// Deferred, not implemented: named and refused, rather than read as the directory argument.
|
|
1251
|
+
if (a.hooksMode !== null) {
|
|
1252
|
+
err(
|
|
1253
|
+
`--hooks=${a.hooksMode} is not implemented. init writes the hooks into .claude/settings.json ` +
|
|
1254
|
+
`(shared, committed) or, with --no-hooks, nowhere.`,
|
|
1255
|
+
);
|
|
1256
|
+
return 2;
|
|
1257
|
+
}
|
|
1258
|
+
if (a.format !== null && !isFormat(a.format)) {
|
|
1259
|
+
err(`--format must be one of ${FORMATS.join(", ")} — got \`${a.format}\``);
|
|
1260
|
+
return 2;
|
|
1261
|
+
}
|
|
1262
|
+
return await init(a.paths[0] ?? ".", {
|
|
1263
|
+
log,
|
|
1264
|
+
err,
|
|
1265
|
+
cwd,
|
|
1266
|
+
version: ownVersion(),
|
|
1267
|
+
yes: a.yes,
|
|
1268
|
+
hooks: !a.noHooks,
|
|
1269
|
+
paper: a.paper,
|
|
1270
|
+
format: isFormat(a.format) ? a.format : null,
|
|
1271
|
+
createPaper: (papersRoot, name, format) =>
|
|
1272
|
+
createPaperAt(papersRoot, name, format, { log, err, cwd }),
|
|
1273
|
+
tex: {
|
|
1274
|
+
installed: () => cachedTree(cacheRoot(process.env)) !== null,
|
|
1275
|
+
install: () =>
|
|
1276
|
+
runToolchain({
|
|
1277
|
+
check: false,
|
|
1278
|
+
log,
|
|
1279
|
+
err,
|
|
1280
|
+
banal: hostBanalInstaller(),
|
|
1281
|
+
tex: toolchainTex(resolve(cwd, a.paths[0] ?? ".")),
|
|
1282
|
+
}),
|
|
1283
|
+
},
|
|
1284
|
+
resolveCliPapers: (root: string): string | null => {
|
|
1285
|
+
const read = readConfig(
|
|
1286
|
+
{ ...a, config: null },
|
|
1287
|
+
{ log: () => {}, err: () => {}, cwd: root },
|
|
1288
|
+
);
|
|
1289
|
+
return read.code === undefined
|
|
1290
|
+
? (toPaths(papersDirOf(read.opts))[0] ?? null)
|
|
1291
|
+
: null;
|
|
1292
|
+
},
|
|
1293
|
+
});
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1008
1296
|
export async function run(
|
|
1009
1297
|
argv: readonly string[],
|
|
1010
1298
|
{
|
|
@@ -1029,6 +1317,12 @@ export async function run(
|
|
|
1029
1317
|
);
|
|
1030
1318
|
return 2;
|
|
1031
1319
|
}
|
|
1320
|
+
if (a.unknownFlag !== undefined) {
|
|
1321
|
+
err(
|
|
1322
|
+
`unknown flag \`${a.unknownFlag}\` — \`paperlint --help\` lists every flag`,
|
|
1323
|
+
);
|
|
1324
|
+
return 2;
|
|
1325
|
+
}
|
|
1032
1326
|
if (a.help || !a.cmd) {
|
|
1033
1327
|
log(USAGE);
|
|
1034
1328
|
return a.help ? 0 : 2;
|
|
@@ -1036,43 +1330,7 @@ export async function run(
|
|
|
1036
1330
|
// `init` asks the CLI's OWN reader what it would lint, so the two sides `doctor` compares are
|
|
1037
1331
|
// not two implementations of the same question. A second resolver here is the defect the
|
|
1038
1332
|
// comparison exists to catch.
|
|
1039
|
-
if (a.cmd === "init") {
|
|
1040
|
-
// Deferred, not implemented: named and refused, rather than read as the directory argument.
|
|
1041
|
-
if (a.hooksMode !== null) {
|
|
1042
|
-
err(
|
|
1043
|
-
`--hooks=${a.hooksMode} is not implemented. init writes the hooks into .claude/settings.json ` +
|
|
1044
|
-
`(shared, committed) or, with --no-hooks, nowhere.`,
|
|
1045
|
-
);
|
|
1046
|
-
return 2;
|
|
1047
|
-
}
|
|
1048
|
-
if (a.format !== null && !isFormat(a.format)) {
|
|
1049
|
-
err(
|
|
1050
|
-
`--format must be one of ${FORMATS.join(", ")} — got \`${a.format}\``,
|
|
1051
|
-
);
|
|
1052
|
-
return 2;
|
|
1053
|
-
}
|
|
1054
|
-
return await init(a.paths[0] ?? ".", {
|
|
1055
|
-
log,
|
|
1056
|
-
err,
|
|
1057
|
-
cwd,
|
|
1058
|
-
version: ownVersion(),
|
|
1059
|
-
yes: a.yes,
|
|
1060
|
-
hooks: !a.noHooks,
|
|
1061
|
-
paper: a.paper,
|
|
1062
|
-
format: isFormat(a.format) ? a.format : null,
|
|
1063
|
-
createPaper: (papersRoot, name, format) =>
|
|
1064
|
-
createPaperAt(papersRoot, name, format, { log, err, cwd }),
|
|
1065
|
-
resolveCliPapers: (root: string): string | null => {
|
|
1066
|
-
const read = readConfig(
|
|
1067
|
-
{ ...a, config: null },
|
|
1068
|
-
{ log: () => {}, err: () => {}, cwd: root },
|
|
1069
|
-
);
|
|
1070
|
-
return read.code === undefined
|
|
1071
|
-
? (toPaths(papersDirOf(read.opts))[0] ?? null)
|
|
1072
|
-
: null;
|
|
1073
|
-
},
|
|
1074
|
-
});
|
|
1075
|
-
}
|
|
1333
|
+
if (a.cmd === "init") return await runInit(a, { log, err, cwd });
|
|
1076
1334
|
// `doctor` reads the config but must NOT die on a broken one — reporting that the config is
|
|
1077
1335
|
// broken is precisely its job. So a failed read becomes "the CLI would lint nothing", which is
|
|
1078
1336
|
// what it prints, rather than an early exit that tells the reader nothing about the hooks.
|
|
@@ -1102,45 +1360,48 @@ export async function run(
|
|
|
1102
1360
|
|
|
1103
1361
|
const cfg = readConfig(a, { log, err, cwd });
|
|
1104
1362
|
if (cfg.code !== undefined) return cfg.code;
|
|
1105
|
-
const { opts,
|
|
1363
|
+
const { opts, root } = cfg;
|
|
1106
1364
|
|
|
1107
1365
|
// A command-line argument OVERRIDES the config: one paper out of the corpus gets linted without
|
|
1108
1366
|
// editing a file.
|
|
1109
1367
|
//
|
|
1110
|
-
// 🔴 A path FROM THE CONFIG is resolved relative to the
|
|
1111
|
-
// Otherwise walking up is pointless: from `papers/aisec-2026` the
|
|
1112
|
-
// `"papersDir": "papers"` would point at `papers/aisec-2026/papers
|
|
1113
|
-
// run would fail with "nothing found" where everything is in place. A command-line argument stays
|
|
1368
|
+
// 🔴 A path FROM THE CONFIG is resolved relative to the PROJECT ROOT, not the current directory.
|
|
1369
|
+
// Otherwise walking up is pointless: from `papers/aisec-2026` the root would be found, but
|
|
1370
|
+
// `"papersDir": "papers"` would point at `papers/aisec-2026/papers`. A command-line argument stays
|
|
1114
1371
|
// relative to the current directory: it was typed here and now.
|
|
1115
1372
|
//
|
|
1116
1373
|
// Both kinds end up ABSOLUTE: ESLint below runs from `lintRoot`, not from here, and would resolve a
|
|
1117
1374
|
// relative argument against the wrong directory.
|
|
1118
1375
|
const paths =
|
|
1119
|
-
a.paths.length > 0
|
|
1120
|
-
? a.paths.map((p) => resolve(cwd, p))
|
|
1121
|
-
: toPaths(papersDirOf(opts)).map((rel) =>
|
|
1122
|
-
resolve(dirname(configPath ?? cwd), rel),
|
|
1123
|
-
);
|
|
1376
|
+
a.paths.length > 0 ? a.paths.map((p) => resolve(cwd, p)) : papersRoots(cfg);
|
|
1124
1377
|
if (paths.length === 0) {
|
|
1125
1378
|
err(
|
|
1126
|
-
`nothing to lint:
|
|
1127
|
-
`Run \`npx paperlint init\` here, or pass the directory: \`paperlint lint papers\`.`,
|
|
1379
|
+
`nothing to lint: "${PAPERS_DIR_FIELD}" names no directory. Pass one: \`paperlint lint papers\`.`,
|
|
1128
1380
|
);
|
|
1129
1381
|
return 2;
|
|
1130
1382
|
}
|
|
1383
|
+
// 🔴 NO PAPERS WHERE THE CONFIG POINTS IS AN ERROR, NOT A CLEAN RUN. With the default in play the
|
|
1384
|
+
// directory may simply not exist yet; a run over nothing would be green and say nothing.
|
|
1385
|
+
if (a.paths.length === 0) {
|
|
1386
|
+
const empty = paths.find((p) => papersIn(p).length === 0);
|
|
1387
|
+
if (empty !== undefined) {
|
|
1388
|
+
err(noPapersMessage(relative(cwd, empty) || "."));
|
|
1389
|
+
return 2;
|
|
1390
|
+
}
|
|
1391
|
+
}
|
|
1131
1392
|
|
|
1132
1393
|
// 🔴 STRUCTURE IS CHECKED BEFORE ESLint AND SEPARATELY FROM IT. A rule is invoked for the file
|
|
1133
1394
|
// handed to it; a missing file is never handed over, so no rule at all can report the absence —
|
|
1134
1395
|
// a directory without `PIPELINE-STATUS.md` simply gets not a single rule and reports clean. The
|
|
1135
1396
|
// analysis of why a structure plugin for ESLint does not cure this is in `structure.mjs`.
|
|
1136
1397
|
const structure = checkStructure(paths, opts.structure, { cwd });
|
|
1137
|
-
//
|
|
1138
|
-
//
|
|
1398
|
+
// The order, most general first so the most specific wins (ESLint: a later block wins): each
|
|
1399
|
+
// paper's venue preset → the root paperlint.json → the paper's own paperlint.json.
|
|
1139
1400
|
const papers = paperRuleBlocks(paths);
|
|
1140
1401
|
if (!papers.ok) return (err(papers.error), 2);
|
|
1141
1402
|
const withPapers = {
|
|
1142
1403
|
...opts,
|
|
1143
|
-
rules: [...papers.value, ...(opts.rules ?? [])],
|
|
1404
|
+
rules: [...papers.value.preset, ...(opts.rules ?? []), ...papers.value.own],
|
|
1144
1405
|
};
|
|
1145
1406
|
|
|
1146
1407
|
let texLanguage: unknown = null;
|
|
@@ -1153,11 +1414,20 @@ export async function run(
|
|
|
1153
1414
|
}
|
|
1154
1415
|
|
|
1155
1416
|
const eslint = new ESLint({
|
|
1156
|
-
cwd: lintRoot(
|
|
1417
|
+
cwd: lintRoot(root, paths),
|
|
1157
1418
|
overrideConfigFile: true,
|
|
1158
1419
|
overrideConfig: buildConfig(withPapers, texLanguage) as Linter.Config[],
|
|
1420
|
+
fix: a.fix,
|
|
1159
1421
|
});
|
|
1160
1422
|
|
|
1423
|
+
const unowned = await firstUnownedFile(eslint, paths);
|
|
1424
|
+
if (unowned !== null) {
|
|
1425
|
+
err(
|
|
1426
|
+
`${relative(cwd, unowned) || unowned} is not a file paperlint lints — it lints ${PAPER_FILE_PATTERNS.join(", ")}`,
|
|
1427
|
+
);
|
|
1428
|
+
return 2;
|
|
1429
|
+
}
|
|
1430
|
+
|
|
1161
1431
|
// 🔴 ESLint THROWS on an empty set (`NoFilesFoundError`) — the guard below simply never got
|
|
1162
1432
|
// reached, which is what the very first run over an empty directory showed: instead of a clear
|
|
1163
1433
|
// message a stack from the depths of eslint-helpers.js flew out. A failure stays a failure, but
|
|
@@ -1167,15 +1437,13 @@ export async function run(
|
|
|
1167
1437
|
try {
|
|
1168
1438
|
results = await eslint.lintFiles(paths);
|
|
1169
1439
|
} catch (e) {
|
|
1170
|
-
|
|
1171
|
-
if (
|
|
1172
|
-
fail?.messageTemplate === "file-not-found" ||
|
|
1173
|
-
/No files matching/i.test(fail?.message ?? "")
|
|
1174
|
-
)
|
|
1175
|
-
results = [];
|
|
1440
|
+
if (isEmptySet(e)) results = [];
|
|
1176
1441
|
else throw e;
|
|
1177
1442
|
}
|
|
1178
1443
|
|
|
1444
|
+
// `--fix` writes what the rules fixed; the report below is what is LEFT.
|
|
1445
|
+
if (a.fix) await ESLint.outputFixes(results);
|
|
1446
|
+
|
|
1179
1447
|
// 🔴 THE GUARD AGAINST A GREEN ZERO, the same one as in action.yml and for the same reason:
|
|
1180
1448
|
// ESLint exits zero when there are no findings, and "no findings" is byte-for-byte
|
|
1181
1449
|
// indistinguishable from "not a single rule got a single file". A rule whose glob did not match
|
|
@@ -1191,11 +1459,50 @@ export async function run(
|
|
|
1191
1459
|
a,
|
|
1192
1460
|
log,
|
|
1193
1461
|
err,
|
|
1194
|
-
where: relative(cwd,
|
|
1462
|
+
where: relative(cwd, root) || ".",
|
|
1195
1463
|
opts: withPapers,
|
|
1464
|
+
// Every paper of the project, not only the ones on the command line (#103).
|
|
1465
|
+
projectPapers: papersRoots(cfg)
|
|
1466
|
+
.flatMap((r) => papersIn(r))
|
|
1467
|
+
.map((d) => join(d, MAIN))
|
|
1468
|
+
.filter((f) => existsSync(f)),
|
|
1196
1469
|
});
|
|
1197
1470
|
}
|
|
1198
1471
|
|
|
1472
|
+
/**
|
|
1473
|
+
* The first path that is a FILE paperlint does not lint, or null. ESLint would answer such a file
|
|
1474
|
+
* with a warning result ("File ignored because of a matching ignore pattern"), which counts as a
|
|
1475
|
+
* linted file and turns a run over nothing into a green one. The question is asked of ESLint's
|
|
1476
|
+
* own matcher, against the same scope that decides what a directory yields.
|
|
1477
|
+
*/
|
|
1478
|
+
async function firstUnownedFile(
|
|
1479
|
+
eslint: ESLint,
|
|
1480
|
+
paths: readonly string[],
|
|
1481
|
+
): Promise<string | null> {
|
|
1482
|
+
for (const p of paths)
|
|
1483
|
+
if (
|
|
1484
|
+
existsSync(p) &&
|
|
1485
|
+
statSync(p).isFile() &&
|
|
1486
|
+
(await eslint.isPathIgnored(p))
|
|
1487
|
+
)
|
|
1488
|
+
return p;
|
|
1489
|
+
return null;
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1492
|
+
/**
|
|
1493
|
+
* ESLint's refusals of an empty set: nothing matched (`file-not-found`), or everything that matched
|
|
1494
|
+
* is outside paperlint's scope (`all-matched-files-ignored` — a papers directory holding only
|
|
1495
|
+
* vendored scripts). Both mean "nothing was linted", which the caller reports itself.
|
|
1496
|
+
*/
|
|
1497
|
+
const isEmptySet = (e: unknown): boolean => {
|
|
1498
|
+
const fail = e as { messageTemplate?: string; message?: string } | null;
|
|
1499
|
+
return (
|
|
1500
|
+
fail?.messageTemplate === "file-not-found" ||
|
|
1501
|
+
fail?.messageTemplate === "all-matched-files-ignored" ||
|
|
1502
|
+
/No files matching/i.test(fail?.message ?? "")
|
|
1503
|
+
);
|
|
1504
|
+
};
|
|
1505
|
+
|
|
1199
1506
|
/**
|
|
1200
1507
|
* The end of `paperlint lint`: refuse an optional rule that reached no paper, print the findings, and
|
|
1201
1508
|
* decide the exit code. Pulled out of `run` so each question has its own function.
|
|
@@ -1211,22 +1518,24 @@ async function reportLint(
|
|
|
1211
1518
|
err,
|
|
1212
1519
|
where,
|
|
1213
1520
|
opts,
|
|
1521
|
+
projectPapers,
|
|
1214
1522
|
}: {
|
|
1215
1523
|
a: Args;
|
|
1216
1524
|
log: typeof console.log;
|
|
1217
1525
|
err: typeof console.error;
|
|
1218
1526
|
where: string;
|
|
1219
1527
|
opts: PaperlintConfig;
|
|
1528
|
+
projectPapers: readonly string[];
|
|
1220
1529
|
},
|
|
1221
1530
|
): Promise<number> {
|
|
1222
1531
|
const silent = await silentOptionalRules(
|
|
1223
1532
|
eslint,
|
|
1224
|
-
results.map((r) => r.filePath),
|
|
1533
|
+
[...results.map((r) => r.filePath), ...projectPapers],
|
|
1225
1534
|
opts,
|
|
1226
1535
|
);
|
|
1227
1536
|
if (silent.length > 0) {
|
|
1228
1537
|
err(
|
|
1229
|
-
`${silent.join(", ")} is turned on in "rules", but no
|
|
1538
|
+
`${silent.join(", ")} is turned on in "rules", but no paper.tex of the project gets it — check the block's ` +
|
|
1230
1539
|
`"files" (relative to ${where}). A rule that never runs reports exactly like a rule that passed.`,
|
|
1231
1540
|
);
|
|
1232
1541
|
return 1;
|