paperlint 2.0.1 → 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/CONTRIBUTING.md +21 -0
- package/README.md +56 -14
- package/dist/build-engine.d.ts.map +1 -1
- package/dist/build-engine.js +5 -2
- 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 +4 -3
- package/dist/build.js.map +1 -1
- package/dist/cli.d.ts +15 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +138 -24
- package/dist/cli.js.map +1 -1
- package/dist/doctor.d.ts.map +1 -1
- package/dist/doctor.js +21 -4
- package/dist/doctor.js.map +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/init.d.ts +11 -0
- package/dist/init.d.ts.map +1 -1
- package/dist/init.js +67 -3
- package/dist/init.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 +7 -0
- package/dist/rules-config.d.ts.map +1 -1
- package/dist/rules-config.js +11 -6
- 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/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 +67 -7
- package/docs/optional-rules.md +25 -24
- package/docs/rules.md +131 -2
- package/eslint-rules/papers.mjs +6 -2
- package/eslint-rules/pdf-last-page-balance.mjs +23 -2
- package/fixtures/build-e2e/acmart/paperlint.json +1 -0
- package/lib/paper-config.d.mts +7 -0
- package/lib/paper-config.mjs +31 -0
- package/package.json +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/render-paper/extract-pdf-facts.harness.mjs +5 -5
- 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 +119 -29
- package/src/build-engine.ts +10 -2
- package/src/build.harness.mjs +23 -18
- package/src/build.ts +5 -4
- package/src/cli.harness.mjs +4 -2
- package/src/cli.ts +158 -29
- package/src/doctor.ts +28 -3
- package/src/facts-file.test.ts +54 -7
- package/src/facts-file.ts +145 -24
- package/src/init.ts +81 -3
- package/src/new-paper.harness.mjs +9 -6
- 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/presets.test.ts +236 -0
- package/src/presets.ts +307 -0
- package/src/rules-config.ts +12 -7
- package/src/structure.harness.mjs +3 -3
- package/src/structure.ts +2 -1
- package/src/tex-requirements.harness.mjs +11 -17
- package/src/tex-requirements.ts +179 -29
- 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/src/build.harness.mjs
CHANGED
|
@@ -137,6 +137,24 @@ const fakeRead = async (pdf) => {
|
|
|
137
137
|
const CLEAN_TEX = "\\documentclass{article}\\begin{document}x\\end{document}";
|
|
138
138
|
|
|
139
139
|
try {
|
|
140
|
+
// ── inputs: paperlint's own venues directory, no configuration ───────────────────────────────
|
|
141
|
+
// First: the facts below resolve a paper's venue preset in this same directory, so a wrong one
|
|
142
|
+
// must be named HERE, not as a missing venue label two checks later.
|
|
143
|
+
const venues = packageVenuesDir();
|
|
144
|
+
check(
|
|
145
|
+
"the venues directory is paperlint's own and holds paper-guards.tex",
|
|
146
|
+
existsSync(join(venues, "paper-guards.tex")),
|
|
147
|
+
);
|
|
148
|
+
check(
|
|
149
|
+
"🔴 with no TEXINPUTS set, the value ENDS in the separator — otherwise the system tree stops resolving",
|
|
150
|
+
withTexInputs({}, [venues]).TEXINPUTS === `${venues}${delimiter}`,
|
|
151
|
+
);
|
|
152
|
+
check(
|
|
153
|
+
"an existing TEXINPUTS is kept, after ours",
|
|
154
|
+
withTexInputs({ TEXINPUTS: `/mine${delimiter}` }, [venues]).TEXINPUTS ===
|
|
155
|
+
`${venues}${delimiter}/mine${delimiter}`,
|
|
156
|
+
);
|
|
157
|
+
|
|
140
158
|
// ── facts: parsed from the paper, not configured ──────────────────────────────────────
|
|
141
159
|
check(
|
|
142
160
|
"documentclass and options come from the parser",
|
|
@@ -157,33 +175,20 @@ try {
|
|
|
157
175
|
);
|
|
158
176
|
const venued = paper("venued", {
|
|
159
177
|
"paper.tex": CLEAN_TEX,
|
|
160
|
-
"
|
|
178
|
+
"paperlint.json": JSON.stringify({
|
|
179
|
+
extends: "paperlint:agenticdev",
|
|
180
|
+
kind: "short",
|
|
181
|
+
}),
|
|
161
182
|
"build.sh": "exit 0\n",
|
|
162
183
|
});
|
|
163
184
|
const facts = readFacts(venued);
|
|
164
|
-
check("the venue comes from
|
|
185
|
+
check("the venue comes from paperlint.json", facts.venue === "agenticdev");
|
|
165
186
|
check(
|
|
166
187
|
"a leftover build.sh is a FACT (reported), not a step",
|
|
167
188
|
JSON.stringify(facts.ignoredScripts) === JSON.stringify(["build.sh"]) &&
|
|
168
189
|
IGNORED_SCRIPTS.includes("repro/build-submission.sh"),
|
|
169
190
|
);
|
|
170
191
|
|
|
171
|
-
// ── inputs: paperlint's own venues directory, no configuration ───────────────────────────────
|
|
172
|
-
const venues = packageVenuesDir();
|
|
173
|
-
check(
|
|
174
|
-
"the venues directory is paperlint's own and holds paper-guards.tex",
|
|
175
|
-
existsSync(join(venues, "paper-guards.tex")),
|
|
176
|
-
);
|
|
177
|
-
check(
|
|
178
|
-
"🔴 with no TEXINPUTS set, the value ENDS in the separator — otherwise the system tree stops resolving",
|
|
179
|
-
withTexInputs({}, [venues]).TEXINPUTS === `${venues}${delimiter}`,
|
|
180
|
-
);
|
|
181
|
-
check(
|
|
182
|
-
"an existing TEXINPUTS is kept, after ours",
|
|
183
|
-
withTexInputs({ TEXINPUTS: `/mine${delimiter}` }, [venues]).TEXINPUTS ===
|
|
184
|
-
`${venues}${delimiter}/mine${delimiter}`,
|
|
185
|
-
);
|
|
186
|
-
|
|
187
192
|
// ── the plan ──────────────────────────────────────────────────────────────────────────
|
|
188
193
|
const plan = planFor(facts);
|
|
189
194
|
check(
|
package/src/build.ts
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
*
|
|
13
13
|
* ── THE SHAPE: an ordered list of STEPS, composed, not configured ────────────
|
|
14
14
|
* Each step says whether it applies to THIS paper and why — decided from FACTS parsed out of the
|
|
15
|
-
* paper (its `\documentclass` and options, the venue named in `
|
|
15
|
+
* paper (its `\documentclass` and options, the venue named in `paperlint.json`), never from a config
|
|
16
16
|
* flag. `paperlint build` prints that plan before running anything, and `--dry-run` prints only the
|
|
17
17
|
* plan. Adding a step is one entry in `STEPS`.
|
|
18
18
|
*
|
|
@@ -45,6 +45,7 @@ import { delimiter, join, relative } from "node:path";
|
|
|
45
45
|
// eslint-disable-next-line boundaries/dependencies -- legacy layer, moves behind a port in #76
|
|
46
46
|
import { getParser } from "@unified-latex/unified-latex-util-parse";
|
|
47
47
|
import { packageVenuesDir } from "../skills/paper-pipeline/scripts/consumer.mjs";
|
|
48
|
+
import { PAPER_SETTINGS_FILE } from "../lib/paper-config.mjs";
|
|
48
49
|
import {
|
|
49
50
|
declaredVenue,
|
|
50
51
|
factsPath,
|
|
@@ -86,7 +87,7 @@ export const PAPER_MARKERS = [
|
|
|
86
87
|
"PIPELINE-STATUS.md",
|
|
87
88
|
"paper.tex",
|
|
88
89
|
"paper.md",
|
|
89
|
-
|
|
90
|
+
PAPER_SETTINGS_FILE,
|
|
90
91
|
];
|
|
91
92
|
|
|
92
93
|
/** The source paperlint compiles, and the job name every output file carries. */
|
|
@@ -114,7 +115,7 @@ export interface PaperFacts {
|
|
|
114
115
|
readonly name: string;
|
|
115
116
|
readonly options: readonly string[];
|
|
116
117
|
} | null;
|
|
117
|
-
/** The
|
|
118
|
+
/** The label of the venue preset `paperlint.json` extends, or null. */
|
|
118
119
|
readonly venue: string | null;
|
|
119
120
|
/** Paper-supplied build scripts found on disk — reported as ignored. */
|
|
120
121
|
readonly ignoredScripts: readonly string[];
|
|
@@ -222,7 +223,7 @@ export function readFacts(paperDir: string): PaperFacts {
|
|
|
222
223
|
return {
|
|
223
224
|
main,
|
|
224
225
|
documentclass: documentclassOf(ast),
|
|
225
|
-
venue: venue?.
|
|
226
|
+
venue: venue?.label ?? null,
|
|
226
227
|
ignoredScripts: IGNORED_SCRIPTS.filter((s) =>
|
|
227
228
|
existsSync(join(paperDir, s)),
|
|
228
229
|
),
|
package/src/cli.harness.mjs
CHANGED
|
@@ -1795,11 +1795,13 @@ console.log(
|
|
|
1795
1795
|
);
|
|
1796
1796
|
const r = await cli(["new", "demo"], root);
|
|
1797
1797
|
check(
|
|
1798
|
-
"🔴 `paperlint new demo` creates writing/demo/ and its lint is the verdict — exit 0, no
|
|
1798
|
+
"🔴 `paperlint new demo` creates writing/demo/ and its lint is the verdict — exit 0, and only the warning that no venue is chosen yet",
|
|
1799
1799
|
r.code === 0 &&
|
|
1800
1800
|
existsSync(join(root, "writing", "demo", "PIPELINE-STATUS.md")) &&
|
|
1801
1801
|
existsSync(join(root, "writing", "demo", "paper.tex")) &&
|
|
1802
|
-
|
|
1802
|
+
existsSync(join(root, "writing", "demo", "paperlint.json")) &&
|
|
1803
|
+
/names no venue preset yet/.test(r.out) &&
|
|
1804
|
+
/\(0 errors, 1 warning\)/.test(r.out),
|
|
1803
1805
|
);
|
|
1804
1806
|
const again = await cli(["new", "demo", "--format", "md"], root);
|
|
1805
1807
|
check(
|
package/src/cli.ts
CHANGED
|
@@ -33,7 +33,10 @@ 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
41
|
import type { Args, PaperlintConfig, ConfigRead } from "./types.ts";
|
|
39
42
|
import {
|
|
@@ -41,22 +44,23 @@ import {
|
|
|
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,7 +128,7 @@ 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
|
|
131
|
+
npx paperlint toolchain [--check] install TeX Live with every package the venue presets declare
|
|
126
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
|
|
@@ -166,7 +172,19 @@ another file of the same shape. \`papersDir\` is required; the rest is optional:
|
|
|
166
172
|
|
|
167
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. */
|
|
@@ -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
|
|
@@ -328,6 +358,66 @@ 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.
|
|
@@ -818,15 +908,40 @@ async function runBuild(
|
|
|
818
908
|
return anyFailed(out.results) ? 1 : 0;
|
|
819
909
|
}
|
|
820
910
|
|
|
821
|
-
/**
|
|
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
|
+
*/
|
|
822
918
|
function paperRequirements(dir: string): TexRequirements {
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
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;
|
|
830
945
|
}
|
|
831
946
|
|
|
832
947
|
/**
|
|
@@ -868,8 +983,14 @@ const SIMPLE: Readonly<
|
|
|
868
983
|
hook: (a, { err }) => runHook(a.paths[0], { err }),
|
|
869
984
|
new: (a, io) => runNew(a, io),
|
|
870
985
|
build: (a, io) => runBuild(a, io),
|
|
871
|
-
toolchain: (a, { log, err }) =>
|
|
872
|
-
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
|
+
}),
|
|
873
994
|
};
|
|
874
995
|
|
|
875
996
|
/** banal's installer, wired from this process's environment: the composition root's work. */
|
|
@@ -1013,6 +1134,14 @@ export async function run(
|
|
|
1013
1134
|
// a directory without `PIPELINE-STATUS.md` simply gets not a single rule and reports clean. The
|
|
1014
1135
|
// analysis of why a structure plugin for ESLint does not cure this is in `structure.mjs`.
|
|
1015
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
|
+
};
|
|
1016
1145
|
|
|
1017
1146
|
let texLanguage: unknown = null;
|
|
1018
1147
|
try {
|
|
@@ -1026,7 +1155,7 @@ export async function run(
|
|
|
1026
1155
|
const eslint = new ESLint({
|
|
1027
1156
|
cwd: lintRoot(configPath ? dirname(resolve(cwd, configPath)) : cwd, paths),
|
|
1028
1157
|
overrideConfigFile: true,
|
|
1029
|
-
overrideConfig: buildConfig(
|
|
1158
|
+
overrideConfig: buildConfig(withPapers, texLanguage) as Linter.Config[],
|
|
1030
1159
|
});
|
|
1031
1160
|
|
|
1032
1161
|
// 🔴 ESLint THROWS on an empty set (`NoFilesFoundError`) — the guard below simply never got
|
|
@@ -1063,7 +1192,7 @@ export async function run(
|
|
|
1063
1192
|
log,
|
|
1064
1193
|
err,
|
|
1065
1194
|
where: relative(cwd, dirname(resolve(cwd, configPath ?? "."))) || ".",
|
|
1066
|
-
opts,
|
|
1195
|
+
opts: withPapers,
|
|
1067
1196
|
});
|
|
1068
1197
|
}
|
|
1069
1198
|
|
package/src/doctor.ts
CHANGED
|
@@ -37,9 +37,11 @@ import {
|
|
|
37
37
|
import {
|
|
38
38
|
LEGACY_CONFIG_KEY,
|
|
39
39
|
LEGACY_KEY_MESSAGE,
|
|
40
|
+
LEGACY_PAPER_SETTINGS_FILE,
|
|
41
|
+
PAPER_SETTINGS_FILE,
|
|
40
42
|
declaredSettings,
|
|
41
43
|
} from "../lib/paper-config.mjs";
|
|
42
|
-
import { PAPER_MARKERS } from "./build.ts";
|
|
44
|
+
import { PAPER_MARKERS, papersIn } from "./build.ts";
|
|
43
45
|
import { linkSkills, SKILLS_HOME, type LinkReport } from "./link-skills.ts";
|
|
44
46
|
import { doctorHooks } from "./hooks-settings.ts";
|
|
45
47
|
|
|
@@ -233,6 +235,28 @@ function papersVerdict(
|
|
|
233
235
|
return { lines: out, bad };
|
|
234
236
|
}
|
|
235
237
|
|
|
238
|
+
/**
|
|
239
|
+
* A pre-2.1.0 `venue.json` left in a paper: it is no longer read, so that paper's venue checks
|
|
240
|
+
* silently would not run if nothing named it. `init` moves it.
|
|
241
|
+
*/
|
|
242
|
+
function leftoverSettingsVerdict(
|
|
243
|
+
root: string,
|
|
244
|
+
papersDir: string | null,
|
|
245
|
+
): { lines: string[]; bad: number } {
|
|
246
|
+
if (papersDir === null) return { lines: [], bad: 0 };
|
|
247
|
+
const lines = papersIn(resolve(root, papersDir), [
|
|
248
|
+
...PAPER_MARKERS,
|
|
249
|
+
LEGACY_PAPER_SETTINGS_FILE,
|
|
250
|
+
])
|
|
251
|
+
.map((dir) => join(dir, LEGACY_PAPER_SETTINGS_FILE))
|
|
252
|
+
.filter((f) => existsSync(f))
|
|
253
|
+
.map(
|
|
254
|
+
(f) =>
|
|
255
|
+
` ✗ ${relative(root, f)} is no longer read — paperlint 2.1.0 renamed it ${PAPER_SETTINGS_FILE}, so this paper's venue checks do not run. \`npx paperlint init\` moves it`,
|
|
256
|
+
);
|
|
257
|
+
return { lines, bad: lines.length };
|
|
258
|
+
}
|
|
259
|
+
|
|
236
260
|
export interface DoctorOptions {
|
|
237
261
|
log?: typeof console.log;
|
|
238
262
|
cwd?: string;
|
|
@@ -293,8 +317,9 @@ export function doctor({
|
|
|
293
317
|
` the hooks will guard ${hookSays ?? "(nothing — the guard refuses and says why on first use)"}`,
|
|
294
318
|
);
|
|
295
319
|
const verdict = papersVerdict(root, cliPapers, hookSays);
|
|
296
|
-
|
|
297
|
-
|
|
320
|
+
const leftover = leftoverSettingsVerdict(root, cliPapers ?? hookSays);
|
|
321
|
+
out.push(...verdict.lines, ...leftover.lines);
|
|
322
|
+
bad += verdict.bad + leftover.bad;
|
|
298
323
|
|
|
299
324
|
// A skill that is not linked is ADVISORY, like a missing program: `paperlint lint`, the hooks and CI
|
|
300
325
|
// work without it, and an entry of the same name that `init` refused to replace is the
|
package/src/facts-file.test.ts
CHANGED
|
@@ -8,6 +8,9 @@
|
|
|
8
8
|
* real banal is `test/e2e/banal.mjs`.
|
|
9
9
|
*/
|
|
10
10
|
import assert from "node:assert/strict";
|
|
11
|
+
import { readdirSync, readFileSync } from "node:fs";
|
|
12
|
+
import { join } from "node:path";
|
|
13
|
+
import { packageVenuesDir } from "../skills/paper-pipeline/scripts/consumer.mjs";
|
|
11
14
|
import { test } from "vitest";
|
|
12
15
|
import { memoryFiles } from "./adapters/memory/index.ts";
|
|
13
16
|
import {
|
|
@@ -147,12 +150,23 @@ const good: PdfReader = async () => ({
|
|
|
147
150
|
ok: true,
|
|
148
151
|
facts: { pages: 2, fonts: FONTS, last: lastPage(60, 60), layout: [] },
|
|
149
152
|
});
|
|
150
|
-
/**
|
|
153
|
+
/** The shipped presets at their real paths: `extends` resolves through the same Files port. */
|
|
154
|
+
const SHIPPED_PRESETS = Object.fromEntries(
|
|
155
|
+
readdirSync(packageVenuesDir())
|
|
156
|
+
.filter((f) => /\.jsonc?$/.test(f))
|
|
157
|
+
.map((f) => [
|
|
158
|
+
join(packageVenuesDir(), f),
|
|
159
|
+
readFileSync(join(packageVenuesDir(), f)),
|
|
160
|
+
]),
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
/** Options over in-memory files: the PDF and paperlint.json on "disk", a measurer answering `geometry`. */
|
|
151
164
|
function setup(geometry: Geometry = NONE) {
|
|
152
165
|
const files = memoryFiles({
|
|
153
166
|
[PDF]: PDF_BYTES,
|
|
154
|
-
|
|
155
|
-
|
|
167
|
+
...SHIPPED_PRESETS,
|
|
168
|
+
[`${PAPER}/paperlint.json`]: JSON.stringify({
|
|
169
|
+
extends: "paperlint:agenticdev",
|
|
156
170
|
kind: "short",
|
|
157
171
|
}),
|
|
158
172
|
});
|
|
@@ -170,7 +184,7 @@ function setup(geometry: Geometry = NONE) {
|
|
|
170
184
|
return { files, measured, o };
|
|
171
185
|
}
|
|
172
186
|
|
|
173
|
-
test("measure: venue and kind from
|
|
187
|
+
test("measure: venue and kind from paperlint.json, pdf relative to the paper, the file's sha256", async () => {
|
|
174
188
|
const { o } = setup();
|
|
175
189
|
const m = await measurePaper(PAPER, PDF, o);
|
|
176
190
|
assert.ok(m.ok);
|
|
@@ -233,8 +247,41 @@ test("writeFactsFile: <paper>/_build/paper.facts.json holds exactly the document
|
|
|
233
247
|
assert.deepEqual(JSON.parse(text), m.value.facts);
|
|
234
248
|
});
|
|
235
249
|
|
|
236
|
-
test("declaredVenue: none without
|
|
250
|
+
test("declaredVenue: none without paperlint.json; no label when it extends no preset", () => {
|
|
237
251
|
assert.equal(declaredVenue(memoryFiles(), PAPER), null);
|
|
238
|
-
const files = memoryFiles({
|
|
239
|
-
|
|
252
|
+
const files = memoryFiles({
|
|
253
|
+
[`${PAPER}/paperlint.json`]: '{"kind":"x","pdf":"b/p.pdf"}',
|
|
254
|
+
});
|
|
255
|
+
assert.deepEqual(declaredVenue(files, PAPER), {
|
|
256
|
+
label: null,
|
|
257
|
+
kind: "x",
|
|
258
|
+
pdf: "b/p.pdf",
|
|
259
|
+
});
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
test("declaredVenue: the label is the preset's, and an extends that resolves nowhere is refused", () => {
|
|
263
|
+
const ok = memoryFiles({
|
|
264
|
+
...SHIPPED_PRESETS,
|
|
265
|
+
[`${PAPER}/paperlint.json`]: '{"extends":"paperlint:aisec"}',
|
|
266
|
+
});
|
|
267
|
+
assert.equal(declaredVenue(ok, PAPER)?.label, "aisec");
|
|
268
|
+
const typo = memoryFiles({
|
|
269
|
+
...SHIPPED_PRESETS,
|
|
270
|
+
[`${PAPER}/paperlint.json`]: '{"extends":"paperlint:aisek"}',
|
|
271
|
+
});
|
|
272
|
+
assert.throws(() => declaredVenue(typo, PAPER), /shipped presets/);
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
test("declaredVenue: a venue.json alone is refused with the command that moves it, not read", () => {
|
|
276
|
+
const files = memoryFiles({
|
|
277
|
+
[`${PAPER}/venue.json`]: '{"venue":"agenticdev"}',
|
|
278
|
+
});
|
|
279
|
+
assert.throws(() => declaredVenue(files, PAPER), /npx paperlint init/);
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
test("declaredVenue: a paperlint.json with an unknown key is refused, naming the key", () => {
|
|
283
|
+
const files = memoryFiles({
|
|
284
|
+
[`${PAPER}/paperlint.json`]: '{"venu":"agenticdev"}',
|
|
285
|
+
});
|
|
286
|
+
assert.throws(() => declaredVenue(files, PAPER), /unknown key "venu"/);
|
|
240
287
|
});
|