paperlint 2.0.0 → 2.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/.github/workflows/ci.yml +4 -4
- package/CLAUDE.md +3 -3
- package/CONTRIBUTING.md +21 -0
- package/LICENSE +1 -1
- package/README.md +81 -24
- package/action.harness.mjs +10 -10
- package/action.mutations.mjs +3 -3
- package/action.yml +11 -11
- package/bin/{rpp.mjs → paperlint.mjs} +2 -2
- package/dist/adapters/banal/invocation.js +3 -3
- package/dist/adapters/banal/invocation.js.map +1 -1
- package/dist/adapters/banal/run.d.ts +1 -1
- package/dist/adapters/banal/run.js +1 -1
- package/dist/adapters/banal/run.js.map +1 -1
- package/dist/adapters/banal/settings.d.ts +4 -4
- package/dist/adapters/banal/settings.d.ts.map +1 -1
- package/dist/adapters/banal/settings.js +4 -4
- package/dist/adapters/banal/settings.js.map +1 -1
- package/dist/adapters/banal/xml.js +1 -1
- package/dist/adapters/banal/xml.js.map +1 -1
- package/dist/adapters/curl/download.io.d.ts.map +1 -1
- package/dist/adapters/curl/download.io.js +2 -2
- package/dist/adapters/curl/download.io.js.map +1 -1
- package/dist/build-engine.d.ts +1 -1
- package/dist/build-engine.d.ts.map +1 -1
- package/dist/build-engine.js +6 -3
- package/dist/build-engine.js.map +1 -1
- package/dist/build.d.ts +1 -1
- package/dist/build.d.ts.map +1 -1
- package/dist/build.js +5 -4
- package/dist/build.js.map +1 -1
- package/dist/cli.d.ts +29 -30
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +174 -98
- package/dist/cli.js.map +1 -1
- package/dist/doctor.d.ts.map +1 -1
- package/dist/doctor.js +21 -6
- package/dist/doctor.js.map +1 -1
- package/dist/engine.d.ts +2 -2
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +1 -1
- package/dist/facts-file.d.ts +33 -9
- package/dist/facts-file.d.ts.map +1 -1
- package/dist/facts-file.js +117 -17
- package/dist/facts-file.js.map +1 -1
- package/dist/hooks-settings.d.ts +0 -18
- package/dist/hooks-settings.d.ts.map +1 -1
- package/dist/hooks-settings.js +70 -42
- package/dist/hooks-settings.js.map +1 -1
- package/dist/init.d.ts +13 -10
- package/dist/init.d.ts.map +1 -1
- package/dist/init.js +74 -42
- package/dist/init.js.map +1 -1
- package/dist/link-skills.js +2 -2
- package/dist/link-skills.js.map +1 -1
- package/dist/new-paper.d.ts +2 -2
- package/dist/new-paper.d.ts.map +1 -1
- package/dist/new-paper.js +17 -2
- package/dist/new-paper.js.map +1 -1
- package/dist/paper-settings.d.ts +68 -0
- package/dist/paper-settings.d.ts.map +1 -0
- package/dist/paper-settings.js +144 -0
- package/dist/paper-settings.js.map +1 -0
- package/dist/presets.d.ts +84 -0
- package/dist/presets.d.ts.map +1 -0
- package/dist/presets.js +186 -0
- package/dist/presets.js.map +1 -0
- package/dist/rules-config.d.ts +8 -1
- package/dist/rules-config.d.ts.map +1 -1
- package/dist/rules-config.js +13 -8
- package/dist/rules-config.js.map +1 -1
- package/dist/structure.d.ts.map +1 -1
- package/dist/structure.js +2 -1
- package/dist/structure.js.map +1 -1
- package/dist/tex-requirements.d.ts +63 -7
- package/dist/tex-requirements.d.ts.map +1 -1
- package/dist/tex-requirements.js +91 -24
- package/dist/tex-requirements.js.map +1 -1
- package/dist/toolchain.d.ts +2 -2
- package/dist/toolchain.d.ts.map +1 -1
- package/dist/toolchain.js +8 -8
- package/dist/toolchain.js.map +1 -1
- package/dist/types.d.ts +4 -4
- package/dist/types.d.ts.map +1 -1
- package/dist/venue-rules.d.ts +109 -0
- package/dist/venue-rules.d.ts.map +1 -0
- package/dist/venue-rules.js +387 -0
- package/dist/venue-rules.js.map +1 -0
- package/docs/configuration.md +73 -17
- package/docs/e2e.md +3 -4
- package/docs/install.md +11 -3
- package/docs/optional-rules.md +26 -25
- package/docs/rules.md +132 -3
- package/docs/toolchain.md +5 -5
- package/eslint-rules/paper-typography.mjs +1 -1
- package/eslint-rules/paper-typography.mutations.mjs +1 -1
- package/eslint-rules/papers.mjs +6 -2
- package/eslint-rules/pdf-last-page-balance.harness.mjs +5 -3
- package/eslint-rules/pdf-last-page-balance.mjs +23 -2
- package/fixtures/build-e2e/acmart/paperlint.json +1 -0
- package/fixtures/build-e2e/guards/paper.tex +1 -1
- package/fixtures/pdf-facts/README.md +1 -1
- package/fixtures/real-markdown-paper/baseline.json +1 -1
- package/fixtures/real-markdown-paper/baseline.mjs +2 -2
- package/fixtures/toolchain-mirror/install-tl +2 -2
- package/fixtures/toolchain-mirror/stub-pdflatex +1 -1
- package/fixtures/toolchain-mirror/stub-tlmgr +1 -1
- package/hooks/hooks.harness.mjs +1 -1
- package/lib/paper-config.d.mts +7 -0
- package/lib/paper-config.harness.mjs +3 -3
- package/lib/paper-config.mjs +34 -3
- package/lib/skill-trigger-cases.harness.mjs +1 -1
- package/package.json +4 -4
- package/plugin/hooks/hooks.json +3 -3
- package/scripts/check.mjs +1 -1
- package/scripts/harness-api.frozen.json +1 -1
- package/scripts/harness-api.test.ts +2 -2
- package/scripts/layer-legacy.frozen.json +1 -1
- package/scripts/mutation-batteries-frozen.harness.mjs +1 -1
- package/scripts/mutation-batteries.frozen.json +2 -2
- package/scripts/release-config.test.ts +1 -1
- package/skills/find-venue/SKILL.md +5 -1
- package/skills/find-venue/SKILL.md.spec.ts +4 -0
- package/skills/paper-pipeline/PIPELINE-MAP.md +14 -4
- package/skills/paper-pipeline/SKILL.md +4 -3
- package/skills/paper-pipeline/SKILL.md.spec.ts +3 -2
- package/skills/paper-pipeline/description-language.eval.mjs +1 -1
- package/skills/paper-pipeline/framing-vs-vocabulary.eval.mjs +1 -1
- package/skills/paper-pipeline/grade-paper-writing-ablation.eval.mjs +2 -2
- package/skills/paper-pipeline/pipeline-firing.eval.mjs +1 -1
- package/skills/paper-pipeline/pipeline-language.eval.mjs +1 -1
- package/skills/paper-pipeline/scripts/consumer.d.mts +1 -0
- package/skills/paper-pipeline/scripts/consumer.harness.mjs +10 -4
- package/skills/paper-pipeline/scripts/consumer.mjs +8 -1
- package/skills/paper-pipeline/scripts/consumer.mutations.mjs +1 -1
- package/skills/render-paper/SKILL.md +3 -3
- package/skills/render-paper/SKILL.md.spec.ts +2 -2
- package/skills/render-paper/extract-pdf-facts.harness.mjs +6 -6
- package/skills/render-paper/extract-pdf-facts.mjs +4 -4
- package/skills/study-accepted-papers/SKILL.md +2 -1
- package/skills/study-accepted-papers/SKILL.md.spec.ts +1 -0
- package/skills/submit-paper/SKILL.md +9 -1
- package/skills/submit-paper/SKILL.md.spec.ts +8 -0
- package/skills/submit-paper/references/venues/acm-sigconf.jsonc +86 -0
- package/skills/submit-paper/references/venues/agenticdev.jsonc +34 -102
- package/skills/submit-paper/references/venues/aisec.jsonc +21 -82
- package/skills/submit-paper/references/venues/realm.jsonc +45 -43
- package/skills/submit-paper/references/venues/tex-base.jsonc +1 -1
- package/skills/submit-paper/references/venues/venue-profile.schema.json +120 -30
- package/src/adapters/banal/index.test.ts +1 -1
- package/src/adapters/banal/invocation.test.ts +3 -3
- package/src/adapters/banal/invocation.ts +3 -3
- package/src/adapters/banal/locate.test.ts +4 -4
- package/src/adapters/banal/run.ts +2 -2
- package/src/adapters/banal/settings.test.ts +12 -6
- package/src/adapters/banal/settings.ts +9 -5
- package/src/adapters/banal/xml.test.ts +1 -1
- package/src/adapters/banal/xml.ts +1 -1
- package/src/adapters/curl/download.io.ts +4 -2
- package/src/adapters/curl/download.test.ts +3 -1
- package/src/adapters/node/files.test.ts +1 -1
- package/src/adapters/node/process.test.ts +2 -2
- package/src/adapters/node/workspace.test.ts +4 -4
- package/src/build-engine.harness.mjs +2 -2
- package/src/build-engine.ts +11 -3
- package/src/build.harness.mjs +27 -22
- package/src/build.mutations.mjs +3 -2
- package/src/build.ts +6 -5
- package/src/cli.harness.mjs +72 -131
- package/src/cli.mutations.mjs +7 -32
- package/src/cli.ts +204 -121
- package/src/doctor.harness.mjs +9 -25
- package/src/doctor.ts +28 -5
- package/src/engine.harness.mjs +4 -2
- package/src/engine.ts +2 -2
- package/src/facts-file.test.ts +54 -7
- package/src/facts-file.ts +145 -24
- package/src/hooks-settings.harness.mjs +24 -15
- package/src/hooks-settings.mutations.mjs +2 -4
- package/src/hooks-settings.ts +92 -49
- package/src/init.ts +88 -52
- package/src/latex-log.harness.mjs +1 -1
- package/src/link-skills.harness.mjs +3 -1
- package/src/link-skills.mutations.mjs +1 -1
- package/src/link-skills.ts +2 -2
- package/src/new-paper.harness.mjs +10 -7
- package/src/new-paper.test.ts +145 -0
- package/src/new-paper.ts +22 -2
- package/src/paper-settings-commands.test.ts +311 -0
- package/src/paper-settings.test.ts +220 -0
- package/src/paper-settings.ts +206 -0
- package/src/pdf-facts.harness.mjs +1 -1
- package/src/presets.test.ts +236 -0
- package/src/presets.ts +307 -0
- package/src/rules-config.ts +14 -9
- package/src/structure.harness.mjs +4 -4
- package/src/structure.ts +2 -1
- package/src/tex-requirements.harness.mjs +12 -18
- package/src/tex-requirements.ts +179 -29
- package/src/toolchain.harness.mjs +34 -25
- package/src/toolchain.ts +14 -8
- package/src/types.ts +4 -4
- package/src/venue-rules.test.ts +545 -0
- package/src/venue-rules.ts +573 -0
- package/templates/paper/paperlint.json +4 -0
- package/fixtures/build-e2e/acmart/venue.json +0 -1
- package/plugin/.claude-plugin/plugin.json +0 -8
package/src/cli.ts
CHANGED
|
@@ -10,13 +10,13 @@
|
|
|
10
10
|
*
|
|
11
11
|
* Two entry points into the tool, and both are whole now:
|
|
12
12
|
* npx paperlint lint ← here
|
|
13
|
-
* uses: zernie/
|
|
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
16
|
* The typography debt, the marker of the author-list check run, the field dictionary — all of that
|
|
17
17
|
* is about ONE corpus, and wiring it into the package would repeat the defect that put the path
|
|
18
18
|
* `.claude/skills/verify-citations/...` into a rule's message. So they live in the consumer's
|
|
19
|
-
* `
|
|
19
|
+
* `package.json`, under the `paperlint` key.
|
|
20
20
|
*
|
|
21
21
|
* 🔴 WHY THE COMMAND IS CALLED `lint` AND NOT `check`. It does exactly what everyone else calls by
|
|
22
22
|
* that word: reads files, changes nothing, prints findings, exits non-zero. `check` is taken in the
|
|
@@ -33,30 +33,34 @@ import { fileURLToPath } from "node:url";
|
|
|
33
33
|
import { join, dirname, resolve, relative, basename, sep } from "node:path";
|
|
34
34
|
import markdown from "@eslint/markdown";
|
|
35
35
|
// Types come from consumer.d.mts beside it, the same arrangement as lib/paper-config.d.mts.
|
|
36
|
-
import {
|
|
36
|
+
import {
|
|
37
|
+
isMain,
|
|
38
|
+
packageVenuesDir,
|
|
39
|
+
} from "../skills/paper-pipeline/scripts/consumer.mjs";
|
|
37
40
|
export { isMain };
|
|
38
|
-
import type { Args,
|
|
41
|
+
import type { Args, PaperlintConfig, ConfigRead } from "./types.ts";
|
|
39
42
|
import {
|
|
40
43
|
checkStructure,
|
|
41
44
|
formatStructure,
|
|
42
45
|
asEslintResults,
|
|
43
46
|
} from "./structure.ts";
|
|
44
|
-
import {
|
|
45
|
-
buildPapers,
|
|
46
|
-
papersIn,
|
|
47
|
-
anyFailed,
|
|
48
|
-
remedyFor,
|
|
49
|
-
readFacts,
|
|
50
|
-
MAIN,
|
|
51
|
-
} from "./build.ts";
|
|
47
|
+
import { buildPapers, papersIn, anyFailed, remedyFor, MAIN } from "./build.ts";
|
|
52
48
|
import { prepareEngine } from "./build-engine.ts";
|
|
53
49
|
import { runToolchain } from "./toolchain.ts";
|
|
54
50
|
import { banalInstaller, parseBanalSettings } from "./adapters/banal/index.ts";
|
|
55
51
|
import { curlDownload } from "./adapters/curl/index.ts";
|
|
56
|
-
import { hostDirs, nodeAdapters } from "./adapters/node/index.ts";
|
|
52
|
+
import { hostDirs, nodeAdapters, nodeFiles } from "./adapters/node/index.ts";
|
|
53
|
+
import { VENUE_RULE_LEVELS, venueRules } from "./venue-rules.ts";
|
|
54
|
+
import { paperRules } from "./paper-settings.ts";
|
|
55
|
+
import {
|
|
56
|
+
paperPreset,
|
|
57
|
+
paperPresetProblem,
|
|
58
|
+
type PaperPreset,
|
|
59
|
+
} from "./presets.ts";
|
|
57
60
|
import type { ToolInstaller } from "./ports/tool-installer.ts";
|
|
58
61
|
import {
|
|
59
62
|
mergeRequirements,
|
|
63
|
+
declaredUnion,
|
|
60
64
|
requirementsFor,
|
|
61
65
|
NO_REQUIREMENTS,
|
|
62
66
|
type TexRequirements,
|
|
@@ -84,9 +88,11 @@ import {
|
|
|
84
88
|
} from "../lib/paper-config.mjs";
|
|
85
89
|
import {
|
|
86
90
|
parseRuleBlocks,
|
|
91
|
+
parseRuleEntries,
|
|
87
92
|
shippedRuleIds,
|
|
88
93
|
unknownKeys,
|
|
89
94
|
type Parsed,
|
|
95
|
+
type RuleBlock,
|
|
90
96
|
} from "./rules-config.ts";
|
|
91
97
|
export { init };
|
|
92
98
|
export { nextSteps } from "./init.ts";
|
|
@@ -122,8 +128,8 @@ const USAGE = `paperlint — machine-checkable gates for a paper kept in git
|
|
|
122
128
|
Compiles with paperlint's TeX Live, else one on PATH that has every
|
|
123
129
|
package the venue declares; on a terminal it offers to install
|
|
124
130
|
one, without a terminal it stops and names \`npx paperlint toolchain\`
|
|
125
|
-
npx paperlint toolchain [--check] install TeX Live with every package the venue
|
|
126
|
-
into ~/.cache/
|
|
131
|
+
npx paperlint toolchain [--check] install TeX Live with every package the venue presets declare
|
|
132
|
+
into ~/.cache/paperlint/texlive (PAPERLINT_TEXLIVE_DIR overrides); a second
|
|
127
133
|
run does nothing. --check: report what is missing, change nothing
|
|
128
134
|
npx paperlint doctor say what is actually wired — and what only LOOKS wired
|
|
129
135
|
npx paperlint hook <name> run an editor hook (.claude/settings.json calls this)
|
|
@@ -149,8 +155,8 @@ lint:
|
|
|
149
155
|
most findings here are advisory and a gate that fails on advice gets muted
|
|
150
156
|
|
|
151
157
|
settings — the \`paperlint\` key of your package.json, found by walking up from the
|
|
152
|
-
current directory, the way every other tool in the stack finds its config. \`
|
|
153
|
-
|
|
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:
|
|
154
160
|
|
|
155
161
|
"paperlint": {
|
|
156
162
|
"papersDir": "papers",
|
|
@@ -164,14 +170,26 @@ read as a deprecated fallback and the run says so. \`papersDir\` is required; th
|
|
|
164
170
|
"rules": { "pdf/last-page-balance": "error" } } ]
|
|
165
171
|
}
|
|
166
172
|
|
|
167
|
-
"rules" takes ESLint flat-config blocks (files, ignores, rules), appended after
|
|
173
|
+
"rules" takes ESLint flat-config blocks (files, ignores, rules), appended after paperlint's own, with
|
|
168
174
|
files relative to the file holding the settings. Optional rules (off unless turned on there):
|
|
169
|
-
pdf/last-page-balance.
|
|
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.
|
|
178
|
+
|
|
179
|
+
per paper — <paper>/paperlint.json (it was venue.json before 2.1.0; \`npx paperlint init\` moves it):
|
|
180
|
+
|
|
181
|
+
{ "extends": "paperlint:aisec", "kind": "research", "rules": { "pdf/last-page-balance": "error" } }
|
|
182
|
+
|
|
183
|
+
"extends" names a venue preset: paperlint:<name> (shipped: acm-sigconf, agenticdev, aisec, realm)
|
|
184
|
+
or ./path.jsonc, relative to the paperlint.json. npm presets are not supported yet.
|
|
185
|
+
|
|
186
|
+
"rules" there applies to that paper alone, after its preset's rules and before the project's. An unknown key,
|
|
187
|
+
anywhere in the settings or in a paperlint.json, is an error.
|
|
170
188
|
`;
|
|
171
189
|
|
|
172
190
|
/** The config the user would otherwise write by hand. The data comes from `opts`, the mechanism is here. */
|
|
173
191
|
export function buildConfig(
|
|
174
|
-
opts:
|
|
192
|
+
opts: PaperlintConfig = {},
|
|
175
193
|
texLanguage: unknown,
|
|
176
194
|
): unknown[] {
|
|
177
195
|
const paperRules = { ...researchQuestion.rules, ...typography.rules };
|
|
@@ -188,11 +206,21 @@ export function buildConfig(
|
|
|
188
206
|
// default (only `node_modules/` and `.git/`), so without this block `paperlint lint` would lint the
|
|
189
207
|
// template as a paper — and a richer template with placeholder stages would fail the run.
|
|
190
208
|
{ ignores: ["**/.template/"] },
|
|
191
|
-
// The `pdf` plugin is registered for EVERY file
|
|
192
|
-
//
|
|
193
|
-
//
|
|
194
|
-
//
|
|
195
|
-
|
|
209
|
+
// The `pdf` plugin is registered for EVERY file. A consumer's block (`rules`, appended below)
|
|
210
|
+
// may name its rules for a glob that also matches markdown files; with the plugin defined only
|
|
211
|
+
// beside `paper.tex`, ESLint would refuse those files with "could not find plugin". Every rule
|
|
212
|
+
// in it acts on `paper.tex` only. `last-page-balance` is on for no file (optional); the venue
|
|
213
|
+
// rules are on for every `paper.tex`, in the block below.
|
|
214
|
+
{
|
|
215
|
+
plugins: {
|
|
216
|
+
pdf: {
|
|
217
|
+
rules: {
|
|
218
|
+
...pdfRules.rules,
|
|
219
|
+
...venueRules({ files: nodeFiles, venuesDir: packageVenuesDir() }),
|
|
220
|
+
},
|
|
221
|
+
},
|
|
222
|
+
},
|
|
223
|
+
},
|
|
196
224
|
{
|
|
197
225
|
files: ["**/PIPELINE-STATUS.md"],
|
|
198
226
|
plugins: { markdown, paper: paperStages },
|
|
@@ -263,6 +291,8 @@ export function buildConfig(
|
|
|
263
291
|
"paper/typography": typographyOpt,
|
|
264
292
|
"tex/future-promise": "warn",
|
|
265
293
|
"tex/acm-frontmatter-override": "error",
|
|
294
|
+
// Silent for a paper whose paperlint.json names no venue (src/venue-rules.ts).
|
|
295
|
+
...VENUE_RULE_LEVELS,
|
|
266
296
|
},
|
|
267
297
|
});
|
|
268
298
|
// The consumer's own blocks, LAST, so a later block wins — ESLint's rule. Parsed by
|
|
@@ -272,8 +302,8 @@ export function buildConfig(
|
|
|
272
302
|
}
|
|
273
303
|
|
|
274
304
|
/**
|
|
275
|
-
* The rule ids a consumer may name in `rules`: every rule
|
|
276
|
-
* config rather than listed again. `@eslint/markdown` is a dependency's plugin, not
|
|
305
|
+
* The rule ids a consumer may name in `rules`: every rule paperlint's own config defines, read off that
|
|
306
|
+
* config rather than listed again. `@eslint/markdown` is a dependency's plugin, not paperlint's.
|
|
277
307
|
*/
|
|
278
308
|
export const SHIPPED_RULES: ReadonlySet<string> = shippedRuleIds(
|
|
279
309
|
buildConfig({}, { sentinel: "tex language" }),
|
|
@@ -288,7 +318,7 @@ const isOn = (entry: unknown): boolean => {
|
|
|
288
318
|
|
|
289
319
|
/**
|
|
290
320
|
* Rules paperlint ships and turns on for no file itself — the ones a consumer opts into with `rules`.
|
|
291
|
-
* Derived: every shipped rule that no block of
|
|
321
|
+
* Derived: every shipped rule that no block of paperlint's own config names.
|
|
292
322
|
*/
|
|
293
323
|
export const OPTIONAL_RULES: ReadonlySet<string> = new Set(
|
|
294
324
|
[...SHIPPED_RULES].filter(
|
|
@@ -309,7 +339,7 @@ export const OPTIONAL_RULES: ReadonlySet<string> = new Set(
|
|
|
309
339
|
export async function silentOptionalRules(
|
|
310
340
|
eslint: ESLint,
|
|
311
341
|
lintedFiles: readonly string[],
|
|
312
|
-
opts:
|
|
342
|
+
opts: PaperlintConfig,
|
|
313
343
|
): Promise<string[]> {
|
|
314
344
|
const turnedOn = new Set(
|
|
315
345
|
(opts.rules ?? []).flatMap((b) =>
|
|
@@ -328,15 +358,75 @@ export async function silentOptionalRules(
|
|
|
328
358
|
return [...turnedOn].filter((id) => !reached.has(id));
|
|
329
359
|
}
|
|
330
360
|
|
|
361
|
+
/**
|
|
362
|
+
* Every linted paper's `rules` from its `paperlint.json`, as ESLint blocks scoped to that paper.
|
|
363
|
+
* A file that does not parse, or names a rule paperlint does not ship, stops the run with one line
|
|
364
|
+
* naming the file — the same strictness as the project's own `rules`. A leftover `venue.json` is
|
|
365
|
+
* not read here; `pdf/profile` and `paperlint doctor` name it.
|
|
366
|
+
*/
|
|
367
|
+
export function paperRuleBlocks(paths: readonly string[]): Parsed<RuleBlock[]> {
|
|
368
|
+
const papers = [...new Set(paths.flatMap((p) => [p, ...papersIn(p)]))];
|
|
369
|
+
const out: RuleBlock[] = [];
|
|
370
|
+
for (const dir of papers) {
|
|
371
|
+
const p = paperPreset(dir, PRESET_DEPS);
|
|
372
|
+
if (p.kind === "settings-problem" && p.problem.kind === "broken")
|
|
373
|
+
return { ok: false, error: paperPresetProblem(dir, p) ?? dir };
|
|
374
|
+
const block = rulesOfPaper(dir, p);
|
|
375
|
+
if (!block.ok) return block;
|
|
376
|
+
if (block.value) out.push(block.value);
|
|
377
|
+
}
|
|
378
|
+
return { ok: true, value: out };
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* One paper's block: its preset chain's `rules`, then its own — later wins per rule id. A preset
|
|
383
|
+
* that does not resolve contributes nothing here; `pdf/profile` reports it on the paper.
|
|
384
|
+
*/
|
|
385
|
+
function rulesOfPaper(dir: string, p: PaperPreset): Parsed<RuleBlock | null> {
|
|
386
|
+
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
|
+
const fromPreset =
|
|
391
|
+
p.kind === "resolved"
|
|
392
|
+
? parseRuleEntries(
|
|
393
|
+
p.preset.rules,
|
|
394
|
+
`the venue preset ${p.preset.chain.join(" → ")} → "rules"`,
|
|
395
|
+
SHIPPED_RULES,
|
|
396
|
+
)
|
|
397
|
+
: { ok: true as const, value: {} };
|
|
398
|
+
if (!fromPreset.ok) return fromPreset;
|
|
399
|
+
const rules = { ...fromPreset.value, ...(own.value ?? {}) };
|
|
400
|
+
return {
|
|
401
|
+
ok: true,
|
|
402
|
+
value: Object.keys(rules).length
|
|
403
|
+
? { basePath: dir, files: PAPER_FILE_PATTERNS, rules }
|
|
404
|
+
: null,
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* The files a paper's block may reach: exactly the ones paperlint's own blocks lint, read off its
|
|
410
|
+
* config. A wider glob (everything under the paper) would make ESLint lint files no block gives a
|
|
411
|
+
* language — `paperlint.json` itself would be parsed as JavaScript.
|
|
412
|
+
*/
|
|
413
|
+
const PAPER_FILE_PATTERNS: string[] = [
|
|
414
|
+
...new Set(
|
|
415
|
+
buildConfig({}, { sentinel: "tex language" }).flatMap(
|
|
416
|
+
(b) => (b as { files?: string[] }).files ?? [],
|
|
417
|
+
),
|
|
418
|
+
),
|
|
419
|
+
];
|
|
420
|
+
|
|
331
421
|
/**
|
|
332
422
|
* The settings after the boundary: an unknown key is refused by name, and `rules` becomes parsed
|
|
333
423
|
* config blocks. Nothing after this sees the raw object.
|
|
334
424
|
*/
|
|
335
425
|
export function parseSettings(
|
|
336
|
-
opts:
|
|
426
|
+
opts: PaperlintConfig,
|
|
337
427
|
where: string,
|
|
338
428
|
baseDir: string,
|
|
339
|
-
): Parsed<
|
|
429
|
+
): Parsed<PaperlintConfig> {
|
|
340
430
|
const raw = opts as Record<string, unknown>;
|
|
341
431
|
const unknown = unknownKeys(raw);
|
|
342
432
|
if (unknown.length > 0)
|
|
@@ -431,7 +521,6 @@ export function parseArgs(argv: readonly string[]): Args {
|
|
|
431
521
|
return out;
|
|
432
522
|
}
|
|
433
523
|
|
|
434
|
-
export const CONFIG_NAME = "rpp.json";
|
|
435
524
|
export const PKG_NAME = "package.json";
|
|
436
525
|
|
|
437
526
|
/**
|
|
@@ -450,36 +539,20 @@ function ownVersion(): string | undefined {
|
|
|
450
539
|
}
|
|
451
540
|
}
|
|
452
541
|
|
|
453
|
-
/** Where the consumer's settings were found, and in which of the two carriers. */
|
|
454
|
-
export interface Declaration {
|
|
455
|
-
readonly path: string;
|
|
456
|
-
readonly kind: "package.json" | "rpp.json";
|
|
457
|
-
}
|
|
458
|
-
|
|
459
542
|
/**
|
|
460
|
-
* 🔴 THE CLI
|
|
461
|
-
*
|
|
462
|
-
*
|
|
463
|
-
* would find no `rpp.json`, report "nothing to lint", and the consumer would be back to
|
|
464
|
-
* declaring the same directory twice — the defect the single declaration removes (issue #33,
|
|
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,
|
|
465
546
|
* `docs/install.md`).
|
|
466
547
|
*
|
|
467
|
-
* `rpp.json` stays readable as a DEPRECATED fallback, and the read says so out loud. Silently
|
|
468
|
-
* dropping a file this command used to write would break working setups on upgrade.
|
|
469
|
-
*
|
|
470
548
|
* The walk goes up to the filesystem root, the way eslint, prettier and tsc find theirs, so a run
|
|
471
|
-
* from inside one paper sees the same settings as a run from the repository root.
|
|
472
|
-
* `package.json` wins over `rpp.json`: it is the carrier every other reader uses, so preferring
|
|
473
|
-
* it is what keeps "one declaration" true rather than merely intended.
|
|
549
|
+
* from inside one paper sees the same settings as a run from the repository root.
|
|
474
550
|
*/
|
|
475
|
-
export function
|
|
551
|
+
export function findConfig(startDir: string): string | null {
|
|
476
552
|
let dir = resolve(startDir);
|
|
477
553
|
for (;;) {
|
|
478
554
|
const pkg = join(dir, PKG_NAME);
|
|
479
|
-
if (existsSync(pkg) && declaresSettings(pkg))
|
|
480
|
-
return { path: pkg, kind: "package.json" };
|
|
481
|
-
const rpp = join(dir, CONFIG_NAME);
|
|
482
|
-
if (existsSync(rpp)) return { path: rpp, kind: "rpp.json" };
|
|
555
|
+
if (existsSync(pkg) && declaresSettings(pkg)) return pkg;
|
|
483
556
|
const up = dirname(dir);
|
|
484
557
|
if (up === dir) return null;
|
|
485
558
|
dir = up;
|
|
@@ -500,11 +573,6 @@ const declaresSettings = (pkgPath: string): boolean => {
|
|
|
500
573
|
}
|
|
501
574
|
};
|
|
502
575
|
|
|
503
|
-
/** Kept as the one-line question "which file holds the settings" — callers that only need a path. */
|
|
504
|
-
export function findConfig(startDir: string): string | null {
|
|
505
|
-
return findDeclaration(startDir)?.path ?? null;
|
|
506
|
-
}
|
|
507
|
-
|
|
508
576
|
/**
|
|
509
577
|
* Reading the config, ONE reader for all commands. Pulled out of `run()` the moment a second
|
|
510
578
|
* command needed the same config (`build`): two copies of this block would have drifted apart on
|
|
@@ -525,24 +593,17 @@ export function readConfig(
|
|
|
525
593
|
} = {},
|
|
526
594
|
): ConfigRead {
|
|
527
595
|
// 🔴 THE CONFIG FINDS ITSELF. An explicit `--config` beats the discovered one — it was named out
|
|
528
|
-
// loud, and a substitution is never silent.
|
|
529
|
-
//
|
|
530
|
-
|
|
531
|
-
const decl: Declaration | null = a.config
|
|
532
|
-
? {
|
|
533
|
-
path: a.config,
|
|
534
|
-
kind: basename(a.config) === PKG_NAME ? "package.json" : "rpp.json",
|
|
535
|
-
}
|
|
536
|
-
: findDeclaration(cwd);
|
|
537
|
-
const configPath = decl?.path ?? null;
|
|
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);
|
|
538
599
|
if (a.config && !existsSync(a.config)) {
|
|
539
600
|
err(`config file not found: ${a.config}`);
|
|
540
601
|
return { code: 2 };
|
|
541
602
|
}
|
|
542
603
|
|
|
543
|
-
let opts:
|
|
604
|
+
let opts: PaperlintConfig = {};
|
|
544
605
|
let legacyKey = false;
|
|
545
|
-
if (
|
|
606
|
+
if (configPath) {
|
|
546
607
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- #49: replace with a real type
|
|
547
608
|
let parsed: any;
|
|
548
609
|
try {
|
|
@@ -551,15 +612,13 @@ export function readConfig(
|
|
|
551
612
|
err(`${configPath} is not valid JSON: ${(e as Error).message}`);
|
|
552
613
|
return { code: 2 };
|
|
553
614
|
}
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
legacyKey = d.legacy;
|
|
562
|
-
} else opts = parsed;
|
|
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;
|
|
563
622
|
// The discovered config is NAMED out loud. Otherwise a run from someone else's directory picks
|
|
564
623
|
// up someone else's file and does not say so — and a typography-debt mismatch looks like a finding.
|
|
565
624
|
//
|
|
@@ -568,30 +627,19 @@ export function readConfig(
|
|
|
568
627
|
// test but by an attempt to wire our own action to this output; in the harness I first WORKED
|
|
569
628
|
// AROUND this line (stripped the first line before JSON.parse) — that is, the workaround hid
|
|
570
629
|
// the defect exactly where it should have been shouting.
|
|
571
|
-
(a.json ? err : log)(`config: ${relative(cwd, configPath) ||
|
|
572
|
-
// 🔴 THE DEPRECATED CARRIER IS NAMED OUT LOUD, IT DOES NOT STOP BEING READ. The hooks read ONLY
|
|
573
|
-
// package.json, so a consumer whose settings stayed in rpp.json lints one directory and guards
|
|
574
|
-
// another — and both states look equally green.
|
|
575
|
-
if (decl.kind === "rpp.json")
|
|
576
|
-
(a.json ? err : log)(
|
|
577
|
-
` ⚠ ${CONFIG_NAME} is deprecated — move these keys under "${CONFIG_KEY}" in ${PKG_NAME}; ` +
|
|
578
|
-
`the hooks read only that file. \`npx paperlint init\` does it for you.`,
|
|
579
|
-
);
|
|
630
|
+
(a.json ? err : log)(`config: ${relative(cwd, configPath) || PKG_NAME}`);
|
|
580
631
|
if (legacyKey) (a.json ? err : log)(` ⚠ ${LEGACY_KEY_MESSAGE}`);
|
|
581
632
|
}
|
|
582
633
|
|
|
583
634
|
// The old field name is refused before anything else is read from the settings: falling back
|
|
584
635
|
// to it would keep it working forever, and this package has no released users to migrate.
|
|
585
|
-
const where =
|
|
586
|
-
|
|
587
|
-
? `${PKG_NAME} → "${legacyKey ? LEGACY_CONFIG_KEY : CONFIG_KEY}"`
|
|
588
|
-
: (decl?.path ?? CONFIG_NAME);
|
|
589
|
-
const renamed = decl ? renamedFieldMessage(opts, where) : null;
|
|
636
|
+
const where = `${configPath ? basename(configPath) : PKG_NAME} → "${legacyKey ? LEGACY_CONFIG_KEY : CONFIG_KEY}"`;
|
|
637
|
+
const renamed = configPath ? renamedFieldMessage(opts, where) : null;
|
|
590
638
|
if (renamed) {
|
|
591
639
|
err(renamed);
|
|
592
640
|
return { code: 2 };
|
|
593
641
|
}
|
|
594
|
-
if (
|
|
642
|
+
if (configPath) {
|
|
595
643
|
const parsed = parseSettings(
|
|
596
644
|
opts,
|
|
597
645
|
where,
|
|
@@ -607,15 +655,11 @@ export function readConfig(
|
|
|
607
655
|
// 🔴 THE PAPERS DIRECTORY IS A REQUIRED FIELD. The papers directory is the one thing without which the tool
|
|
608
656
|
// does not know what it works on, and the one thing that cannot be guessed: a default of "." runs
|
|
609
657
|
// the rules over the whole checkout and exits green over a scope nobody chose.
|
|
610
|
-
if (
|
|
658
|
+
if (configPath && !hasPapers(opts)) {
|
|
611
659
|
err(
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
`It is the one thing this tool cannot guess. \`npx paperlint init\` writes it for you.`
|
|
616
|
-
: `${decl.path} must declare \`${PAPERS_DIR_FIELD}\` — the directory your papers live in, e.g.\n` +
|
|
617
|
-
` { "${PAPERS_DIR_FIELD}": "papers" }\n` +
|
|
618
|
-
`It is the one thing this tool cannot guess.`,
|
|
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.`,
|
|
619
663
|
);
|
|
620
664
|
return { code: 2 };
|
|
621
665
|
}
|
|
@@ -623,7 +667,7 @@ export function readConfig(
|
|
|
623
667
|
}
|
|
624
668
|
|
|
625
669
|
/** The papers directory field of the settings, read by its one declared name. */
|
|
626
|
-
export function papersDirOf(opts:
|
|
670
|
+
export function papersDirOf(opts: PaperlintConfig): unknown {
|
|
627
671
|
return (opts as Record<string, unknown>)[PAPERS_DIR_FIELD];
|
|
628
672
|
}
|
|
629
673
|
|
|
@@ -635,7 +679,7 @@ export function toPaths(papers: unknown): string[] {
|
|
|
635
679
|
return [];
|
|
636
680
|
}
|
|
637
681
|
|
|
638
|
-
const hasPapers = (opts:
|
|
682
|
+
const hasPapers = (opts: PaperlintConfig): boolean =>
|
|
639
683
|
toPaths(papersDirOf(opts)).length > 0;
|
|
640
684
|
|
|
641
685
|
/**
|
|
@@ -703,7 +747,7 @@ export function runHook(
|
|
|
703
747
|
throw new Error(`resolved vigiles, but no cli.js beside it: ${runtime}`);
|
|
704
748
|
} catch {
|
|
705
749
|
err(
|
|
706
|
-
`
|
|
750
|
+
`paperlint: the hook runtime (vigiles) is not resolvable from ${fileURLToPath(new URL(".", import.meta.url))}.\n` +
|
|
707
751
|
`The \`${name}\` hook is NOT running. Everything else — \`paperlint lint\`, CI — is unaffected.\n` +
|
|
708
752
|
`Reinstall this package so its dependencies are present.`,
|
|
709
753
|
);
|
|
@@ -864,15 +908,40 @@ async function runBuild(
|
|
|
864
908
|
return anyFailed(out.results) ? 1 : 0;
|
|
865
909
|
}
|
|
866
910
|
|
|
867
|
-
/**
|
|
911
|
+
/** The presets' deps, wired to the disk and the package's own venues directory. */
|
|
912
|
+
const PRESET_DEPS = { files: nodeFiles, venuesDir: packageVenuesDir() };
|
|
913
|
+
|
|
914
|
+
/**
|
|
915
|
+
* What one paper needs from TeX Live: the base set plus its preset chain's `tex`. A paper whose
|
|
916
|
+
* settings or preset do not resolve gets the base set; the build reports why at its facts step.
|
|
917
|
+
*/
|
|
868
918
|
function paperRequirements(dir: string): TexRequirements {
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
919
|
+
const p = paperPreset(dir, PRESET_DEPS);
|
|
920
|
+
return requirementsFor(p.kind === "resolved" ? p.preset : null).tex;
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
/**
|
|
924
|
+
* What `paperlint toolchain` installs: every shipped preset's packages, plus the resolved chain of
|
|
925
|
+
* every paper under the project's papers directory — a project's own preset lives outside the
|
|
926
|
+
* package, so the shipped union alone would not see it.
|
|
927
|
+
*/
|
|
928
|
+
export function toolchainTex(cwd: string): TexRequirements {
|
|
929
|
+
const cfg = readConfig(parseArgs(["toolchain"]), {
|
|
930
|
+
log: () => {},
|
|
931
|
+
err: () => {},
|
|
932
|
+
cwd,
|
|
933
|
+
});
|
|
934
|
+
const roots =
|
|
935
|
+
cfg.code === undefined
|
|
936
|
+
? toPaths(papersDirOf(cfg.opts)).map((rel) =>
|
|
937
|
+
resolve(dirname(cfg.configPath ?? cwd), rel),
|
|
938
|
+
)
|
|
939
|
+
: [];
|
|
940
|
+
const chains = roots
|
|
941
|
+
.flatMap((r) => papersIn(r))
|
|
942
|
+
.map((dir) => paperPreset(dir, PRESET_DEPS))
|
|
943
|
+
.flatMap((p) => (p.kind === "resolved" ? [p.preset.tex] : []));
|
|
944
|
+
return declaredUnion(undefined, chains).tex;
|
|
876
945
|
}
|
|
877
946
|
|
|
878
947
|
/**
|
|
@@ -914,8 +983,14 @@ const SIMPLE: Readonly<
|
|
|
914
983
|
hook: (a, { err }) => runHook(a.paths[0], { err }),
|
|
915
984
|
new: (a, io) => runNew(a, io),
|
|
916
985
|
build: (a, io) => runBuild(a, io),
|
|
917
|
-
toolchain: (a, { log, err }) =>
|
|
918
|
-
runToolchain({
|
|
986
|
+
toolchain: (a, { log, err, cwd }) =>
|
|
987
|
+
runToolchain({
|
|
988
|
+
check: a.check,
|
|
989
|
+
log,
|
|
990
|
+
err,
|
|
991
|
+
banal: hostBanalInstaller(),
|
|
992
|
+
tex: toolchainTex(cwd),
|
|
993
|
+
}),
|
|
919
994
|
};
|
|
920
995
|
|
|
921
996
|
/** banal's installer, wired from this process's environment: the composition root's work. */
|
|
@@ -1048,7 +1123,7 @@ export async function run(
|
|
|
1048
1123
|
);
|
|
1049
1124
|
if (paths.length === 0) {
|
|
1050
1125
|
err(
|
|
1051
|
-
`nothing to lint: no path was given and no ${
|
|
1126
|
+
`nothing to lint: no path was given and no "${CONFIG_KEY}" key was found in a ${PKG_NAME}.\n` +
|
|
1052
1127
|
`Run \`npx paperlint init\` here, or pass the directory: \`paperlint lint papers\`.`,
|
|
1053
1128
|
);
|
|
1054
1129
|
return 2;
|
|
@@ -1059,6 +1134,14 @@ export async function run(
|
|
|
1059
1134
|
// a directory without `PIPELINE-STATUS.md` simply gets not a single rule and reports clean. The
|
|
1060
1135
|
// analysis of why a structure plugin for ESLint does not cure this is in `structure.mjs`.
|
|
1061
1136
|
const structure = checkStructure(paths, opts.structure, { cwd });
|
|
1137
|
+
// Each paper's own `rules` (its paperlint.json) go after paperlint's blocks and BEFORE the
|
|
1138
|
+
// project's, so the project's package.json still has the last word.
|
|
1139
|
+
const papers = paperRuleBlocks(paths);
|
|
1140
|
+
if (!papers.ok) return (err(papers.error), 2);
|
|
1141
|
+
const withPapers = {
|
|
1142
|
+
...opts,
|
|
1143
|
+
rules: [...papers.value, ...(opts.rules ?? [])],
|
|
1144
|
+
};
|
|
1062
1145
|
|
|
1063
1146
|
let texLanguage: unknown = null;
|
|
1064
1147
|
try {
|
|
@@ -1072,7 +1155,7 @@ export async function run(
|
|
|
1072
1155
|
const eslint = new ESLint({
|
|
1073
1156
|
cwd: lintRoot(configPath ? dirname(resolve(cwd, configPath)) : cwd, paths),
|
|
1074
1157
|
overrideConfigFile: true,
|
|
1075
|
-
overrideConfig: buildConfig(
|
|
1158
|
+
overrideConfig: buildConfig(withPapers, texLanguage) as Linter.Config[],
|
|
1076
1159
|
});
|
|
1077
1160
|
|
|
1078
1161
|
// 🔴 ESLint THROWS on an empty set (`NoFilesFoundError`) — the guard below simply never got
|
|
@@ -1109,7 +1192,7 @@ export async function run(
|
|
|
1109
1192
|
log,
|
|
1110
1193
|
err,
|
|
1111
1194
|
where: relative(cwd, dirname(resolve(cwd, configPath ?? "."))) || ".",
|
|
1112
|
-
opts,
|
|
1195
|
+
opts: withPapers,
|
|
1113
1196
|
});
|
|
1114
1197
|
}
|
|
1115
1198
|
|
|
@@ -1133,7 +1216,7 @@ async function reportLint(
|
|
|
1133
1216
|
log: typeof console.log;
|
|
1134
1217
|
err: typeof console.error;
|
|
1135
1218
|
where: string;
|
|
1136
|
-
opts:
|
|
1219
|
+
opts: PaperlintConfig;
|
|
1137
1220
|
},
|
|
1138
1221
|
): Promise<number> {
|
|
1139
1222
|
const silent = await silentOptionalRules(
|
|
@@ -1180,7 +1263,7 @@ async function reportLint(
|
|
|
1180
1263
|
|
|
1181
1264
|
// 🔴 `isMain`, NOT A STRING COMPARISON. The first version wrote
|
|
1182
1265
|
// if (import.meta.url === `file://${process.argv[1]}`)
|
|
1183
|
-
// and the utility, launched via `node_modules/.bin/
|
|
1266
|
+
// and the utility, launched via `node_modules/.bin/paperlint`, SILENTLY EXITED WITH ZERO: npm puts a
|
|
1184
1267
|
// SYMLINK there, `process.argv[1]` stays the symlink's path while `import.meta.url` is the real
|
|
1185
1268
|
// path, and the condition is false. That is, the only way a real consumer launches the utility did
|
|
1186
1269
|
// not work at all — and it looked like a clean run.
|
package/src/doctor.harness.mjs
CHANGED
|
@@ -41,9 +41,9 @@ const check = (label, cond) => {
|
|
|
41
41
|
assert.ok(cond, label);
|
|
42
42
|
};
|
|
43
43
|
|
|
44
|
-
/** A consumer on disk: a papers directory,
|
|
45
|
-
function consumer({ papersDir, pkgKey,
|
|
46
|
-
const dir = realpathSync(mkdtempSync(join(tmpdir(), "
|
|
44
|
+
/** A consumer on disk: a papers directory, and maybe a declaration in package.json. */
|
|
45
|
+
function consumer({ papersDir, pkgKey, makeDir = true }) {
|
|
46
|
+
const dir = realpathSync(mkdtempSync(join(tmpdir(), "paperlint-doctor-")));
|
|
47
47
|
if (makeDir && papersDir) {
|
|
48
48
|
mkdirSync(join(dir, papersDir, "some-paper"), { recursive: true });
|
|
49
49
|
writeFileSync(
|
|
@@ -54,11 +54,6 @@ function consumer({ papersDir, pkgKey, rppJson, makeDir = true }) {
|
|
|
54
54
|
const pkg = { name: "consumer", version: "1.0.0" };
|
|
55
55
|
if (pkgKey !== undefined) pkg["paperlint"] = { [PAPERS_DIR_FIELD]: pkgKey };
|
|
56
56
|
writeFileSync(join(dir, "package.json"), JSON.stringify(pkg, null, 2));
|
|
57
|
-
if (rppJson !== undefined)
|
|
58
|
-
writeFileSync(
|
|
59
|
-
join(dir, "rpp.json"),
|
|
60
|
-
JSON.stringify({ [PAPERS_DIR_FIELD]: rppJson }, null, 2),
|
|
61
|
-
);
|
|
62
57
|
return dir;
|
|
63
58
|
}
|
|
64
59
|
|
|
@@ -113,13 +108,10 @@ const runDoctor = (
|
|
|
113
108
|
}
|
|
114
109
|
|
|
115
110
|
// ── II. THE VERY DEFECT: AN INSTALL FOLLOWING THE DOCS ─────────────────────────────────────
|
|
116
|
-
// `
|
|
111
|
+
// The old `init` wrote its own config file and did not touch package.json; the hook reads package.json.
|
|
117
112
|
// Measured 09-18.
|
|
118
113
|
{
|
|
119
|
-
const dir = consumer({
|
|
120
|
-
papersDir: "writing/drafts",
|
|
121
|
-
rppJson: "writing/drafts",
|
|
122
|
-
});
|
|
114
|
+
const dir = consumer({ papersDir: "writing/drafts" });
|
|
123
115
|
const r = runDoctor(dir, { cliPapers: "writing/drafts" });
|
|
124
116
|
check(
|
|
125
117
|
"an install that follows the docs — a FAILURE, not a cheerful report",
|
|
@@ -150,7 +142,7 @@ const runDoctor = (
|
|
|
150
142
|
// It must not fail here (an error-level false positive costs more than a miss), but it must
|
|
151
143
|
// not stay silent either.
|
|
152
144
|
{
|
|
153
|
-
const dir = consumer({ papersDir: "papers"
|
|
145
|
+
const dir = consumer({ papersDir: "papers" });
|
|
154
146
|
const r = runDoctor(dir, { cliPapers: "papers" });
|
|
155
147
|
check("an install that works by coincidence does NOT crash", r.code === 0);
|
|
156
148
|
check(
|
|
@@ -220,18 +212,10 @@ const runDoctor = (
|
|
|
220
212
|
|
|
221
213
|
// ── III. TWO DECLARATIONS HAVE DRIFTED APART ────────────────────────────────────────────────
|
|
222
214
|
{
|
|
223
|
-
const dir = consumer({
|
|
224
|
-
papersDir: "writing/drafts",
|
|
225
|
-
pkgKey: "papers",
|
|
226
|
-
rppJson: "writing/drafts",
|
|
227
|
-
});
|
|
215
|
+
const dir = consumer({ papersDir: "writing/drafts", pkgKey: "papers" });
|
|
228
216
|
mkdirSync(join(dir, "papers"), { recursive: true });
|
|
229
217
|
const r = runDoctor(dir, { cliPapers: "writing/drafts" });
|
|
230
218
|
check("both declarations exist, but differ — a FAILURE", r.code === 2);
|
|
231
|
-
check(
|
|
232
|
-
"the stale rpp.json is called out ⚠",
|
|
233
|
-
/rpp\.json is present/.test(r.out),
|
|
234
|
-
);
|
|
235
219
|
rmSync(dir, { recursive: true, force: true });
|
|
236
220
|
}
|
|
237
221
|
|
|
@@ -307,7 +291,7 @@ const runDoctor = (
|
|
|
307
291
|
|
|
308
292
|
// ── VII. DETECTING THE PAPERS DIRECTORY ─────────────────────────────────────────────────────
|
|
309
293
|
{
|
|
310
|
-
const dir = realpathSync(mkdtempSync(join(tmpdir(), "
|
|
294
|
+
const dir = realpathSync(mkdtempSync(join(tmpdir(), "paperlint-detect-")));
|
|
311
295
|
mkdirSync(join(dir, "writing", "drafts", "p1"), { recursive: true });
|
|
312
296
|
writeFileSync(join(dir, "writing", "drafts", "p1", "paper.tex"), "x");
|
|
313
297
|
mkdirSync(join(dir, "node_modules", "pkg", "papers", "p"), {
|
|
@@ -387,7 +371,7 @@ const runDoctor = (
|
|
|
387
371
|
check("a program that really exists is found", found("node") === true);
|
|
388
372
|
check(
|
|
389
373
|
"a made-up one is not (otherwise the check answers the form, not the subject)",
|
|
390
|
-
found("
|
|
374
|
+
found("paperlint-definitely-not-a-real-binary-xyz") === false,
|
|
391
375
|
);
|
|
392
376
|
}
|
|
393
377
|
|