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
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE FILES PAPERLINT OWNS — read off its own config, and made the only thing ESLint may reach.
|
|
3
|
+
*
|
|
4
|
+
* paperlint's blocks name the files they lint (`PIPELINE-STATUS.md`, `paper.md`, `paper.tex`,
|
|
5
|
+
* `reviews/*.md`, …). That list is the whole scope of `paperlint lint`, and this module turns it
|
|
6
|
+
* into two things ESLint understands:
|
|
7
|
+
*
|
|
8
|
+
* 1. **A global ignore of everything else** (`scopeToOwned`). Handing ESLint a directory is not
|
|
9
|
+
* enough: its built-in defaults lint every JavaScript file it finds, whatever our config says.
|
|
10
|
+
* Measured on a consumer project with vendored code under a paper's `repro/` folder: 1,877
|
|
11
|
+
* files, 74 errors ("'return' outside of function", "Definition for rule … was not found"),
|
|
12
|
+
* zero of them on a paper file, exit 1 — and nothing in the settings could turn it off. With
|
|
13
|
+
* the ignore, a stray script under the papers directory is never enumerated at all, and a file
|
|
14
|
+
* named on the command line can be asked about (`ESLint#isPathIgnored`) with ESLint's own
|
|
15
|
+
* matcher instead of a hand-written one.
|
|
16
|
+
*
|
|
17
|
+
* 2. **Each rule scoped to the files it was written for** (`ruleOwners` + `narrowToOwners`).
|
|
18
|
+
* One plugin NAME is bound to different plugin objects in different blocks: `paper` holds
|
|
19
|
+
* `stages`/`source`/`author-list` beside `PIPELINE-STATUS.md` and `research-question`/
|
|
20
|
+
* `typography` beside `paper.md`/`paper.tex`. So a consumer block that named `paper/source`
|
|
21
|
+
* for every file under the papers directory reached `paper.tex`, where `paper` has no `source`
|
|
22
|
+
* — and ESLint threw `Could not find "source" in plugin "paper"` from inside `lintFiles`,
|
|
23
|
+
* after paperlint's own validation had (correctly) accepted the id. A block is therefore split
|
|
24
|
+
* per owner and its `files` ANDed with the owner's (ESLint's nested-array form), so a rule can
|
|
25
|
+
* only land where its plugin is registered.
|
|
26
|
+
*
|
|
27
|
+
* Both read ONE definition of "the files paperlint lints": `ownedScopes`, each own block's `files`
|
|
28
|
+
* together with its `ignores`. The globs alone are not that definition. The sibling-card block
|
|
29
|
+
* claims every markdown file under `siblings/` EXCEPT the index, and a rule whose plugin is
|
|
30
|
+
* registered for every file (`pdf`) used to keep the consumer block's `files` as they were — so a
|
|
31
|
+
* venue preset's rules reached `siblings/README.md`, a block with no `language` matched it, and
|
|
32
|
+
* ESLint parsed the markdown as JavaScript (#101). Now every generated block narrows to scopes,
|
|
33
|
+
* exceptions included, and a plugin registered everywhere owns exactly what paperlint lints.
|
|
34
|
+
*
|
|
35
|
+
* ESLint's semantics that this leans on, each measured against ESLint 10 before writing it:
|
|
36
|
+
* - a global-ignore pattern that matches a DIRECTORY hides everything inside it, so directories
|
|
37
|
+
* must be un-ignored (`!` + a pattern ending in a slash) before owned files can be;
|
|
38
|
+
* - that un-ignores `node_modules/` and `.git/` too — ESLint's own defaults — so they are
|
|
39
|
+
* re-ignored after, or a `paper.md` inside a dependency would be linted;
|
|
40
|
+
* - later global-ignore patterns win, across blocks, in config order;
|
|
41
|
+
* - `files: [["a", "b"]]` matches a file only when BOTH patterns match.
|
|
42
|
+
*/
|
|
43
|
+
import type { RuleBlock, RuleEntry } from "./rules-config.ts";
|
|
44
|
+
|
|
45
|
+
/** What this module reads of a config block. */
|
|
46
|
+
interface Block {
|
|
47
|
+
readonly files?: readonly unknown[];
|
|
48
|
+
readonly ignores?: readonly unknown[];
|
|
49
|
+
readonly plugins?: Readonly<
|
|
50
|
+
Record<string, { readonly rules?: Readonly<Record<string, unknown>> }>
|
|
51
|
+
>;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* A set of files paperlint lints: an own block's globs and the exceptions that block makes. A file is
|
|
56
|
+
* in it when it matches one of `files` and none of `ignores`.
|
|
57
|
+
*/
|
|
58
|
+
export interface OwnedScope {
|
|
59
|
+
readonly files: readonly string[];
|
|
60
|
+
readonly ignores: readonly string[];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** A `files` entry: one glob, or several that must ALL match. */
|
|
64
|
+
export type FilesEntry = string | readonly string[];
|
|
65
|
+
|
|
66
|
+
/** A consumer block after narrowing: its `files` may carry ANDed pairs. */
|
|
67
|
+
export interface ScopedBlock {
|
|
68
|
+
readonly basePath: string;
|
|
69
|
+
readonly files?: readonly FilesEntry[];
|
|
70
|
+
readonly ignores?: readonly string[];
|
|
71
|
+
readonly rules: Readonly<Record<string, RuleEntry>>;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const globsOf = (b: Block): string[] =>
|
|
75
|
+
(b.files ?? []).filter((f): f is string => typeof f === "string");
|
|
76
|
+
|
|
77
|
+
const stringsOf = (xs: readonly unknown[] | undefined): string[] =>
|
|
78
|
+
(xs ?? []).filter((x): x is string => typeof x === "string");
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Scopes with the same exceptions, merged into one: their globs united, once, in order. Keeps a
|
|
82
|
+
* generated block per distinct set of exceptions rather than one per own block.
|
|
83
|
+
*/
|
|
84
|
+
const merged = (scopes: readonly OwnedScope[]): OwnedScope[] => {
|
|
85
|
+
const byIgnores = new Map<string, OwnedScope>();
|
|
86
|
+
for (const s of scopes) {
|
|
87
|
+
const key = JSON.stringify(s.ignores);
|
|
88
|
+
const had = byIgnores.get(key);
|
|
89
|
+
byIgnores.set(key, {
|
|
90
|
+
files: [...new Set([...(had?.files ?? []), ...s.files])],
|
|
91
|
+
ignores: s.ignores,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
return [...byIgnores.values()];
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* THE FILES PAPERLINT LINTS: every own block that names `files`, with its `ignores`. A block without
|
|
99
|
+
* `files` (a global ignore, or the block registering `pdf` for every file) claims nothing.
|
|
100
|
+
*/
|
|
101
|
+
export function ownedScopes(own: readonly unknown[]): OwnedScope[] {
|
|
102
|
+
return merged(
|
|
103
|
+
own
|
|
104
|
+
.map((raw) => raw as Block)
|
|
105
|
+
.filter((b) => b.files !== undefined)
|
|
106
|
+
.map((b) => ({ files: globsOf(b), ignores: stringsOf(b.ignores) })),
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Every glob paperlint's own blocks lint, once, in config order. */
|
|
111
|
+
export function ownedPatterns(own: readonly unknown[]): string[] {
|
|
112
|
+
return [...new Set(ownedScopes(own).flatMap((s) => s.files))];
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** ESLint's default ignores, re-applied after directories are un-ignored. */
|
|
116
|
+
const ESLINT_DEFAULT_IGNORES = ["**/node_modules/", "**/.git/"];
|
|
117
|
+
|
|
118
|
+
/** A global-ignore block that leaves ESLint nothing to lint but `owned`. */
|
|
119
|
+
export function scopeToOwned(owned: readonly string[]): {
|
|
120
|
+
readonly ignores: string[];
|
|
121
|
+
} {
|
|
122
|
+
return {
|
|
123
|
+
ignores: [
|
|
124
|
+
"**/*",
|
|
125
|
+
"!**/*/",
|
|
126
|
+
...owned.map((p) => `!${p}`),
|
|
127
|
+
...ESLINT_DEFAULT_IGNORES,
|
|
128
|
+
],
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** Every `<plugin>/<rule>` a block registers. */
|
|
133
|
+
const ruleIdsOf = (b: Block): string[] =>
|
|
134
|
+
Object.entries(b.plugins ?? {}).flatMap(([name, plugin]) =>
|
|
135
|
+
Object.keys(plugin.rules ?? {}).map((rule) => `${name}/${rule}`),
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Rule id → the scopes of the blocks that register its plugin with that rule. A plugin registered
|
|
140
|
+
* for every file (a block without `files`) owns every scope — everything paperlint lints, and not
|
|
141
|
+
* one file more.
|
|
142
|
+
*/
|
|
143
|
+
export function ruleOwners(
|
|
144
|
+
own: readonly unknown[],
|
|
145
|
+
): Map<string, readonly OwnedScope[]> {
|
|
146
|
+
const all = ownedScopes(own);
|
|
147
|
+
const found = new Map<string, OwnedScope[]>();
|
|
148
|
+
for (const raw of own) {
|
|
149
|
+
const b = raw as Block;
|
|
150
|
+
const scopes =
|
|
151
|
+
b.files === undefined
|
|
152
|
+
? all
|
|
153
|
+
: [{ files: globsOf(b), ignores: stringsOf(b.ignores) }];
|
|
154
|
+
for (const id of ruleIdsOf(b))
|
|
155
|
+
found.set(id, [...(found.get(id) ?? []), ...scopes]);
|
|
156
|
+
}
|
|
157
|
+
return new Map([...found].map(([id, scopes]) => [id, merged(scopes)]));
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* One consumer block → one block per owner scope, each carrying only that owner's rules, its `files`
|
|
162
|
+
* ANDed with the scope's globs and its `ignores` joined by the scope's exceptions. A rule no block
|
|
163
|
+
* of this config registers (the LaTeX block is absent when its parser did not load) gets no block:
|
|
164
|
+
* it has no file to reach in this run, and naming it to ESLint would only crash.
|
|
165
|
+
*/
|
|
166
|
+
export function narrowToOwners(
|
|
167
|
+
block: RuleBlock,
|
|
168
|
+
owners: ReadonlyMap<string, readonly OwnedScope[]>,
|
|
169
|
+
): ScopedBlock[] {
|
|
170
|
+
const groups = new Map<string, Record<string, RuleEntry>>();
|
|
171
|
+
for (const [id, entry] of Object.entries(block.rules)) {
|
|
172
|
+
const owner = owners.get(id);
|
|
173
|
+
if (owner === undefined) continue;
|
|
174
|
+
const key = JSON.stringify(owner);
|
|
175
|
+
groups.set(key, { ...groups.get(key), [id]: entry });
|
|
176
|
+
}
|
|
177
|
+
return [...groups].flatMap(([key, rules]) =>
|
|
178
|
+
(JSON.parse(key) as OwnedScope[]).map((scope) =>
|
|
179
|
+
within(block, scope, rules),
|
|
180
|
+
),
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** `block`'s rules over the files both it and `scope` claim, minus both sets of exceptions. */
|
|
185
|
+
function within(
|
|
186
|
+
block: RuleBlock,
|
|
187
|
+
scope: OwnedScope,
|
|
188
|
+
rules: Readonly<Record<string, RuleEntry>>,
|
|
189
|
+
): ScopedBlock {
|
|
190
|
+
const files: readonly FilesEntry[] = block.files
|
|
191
|
+
? block.files.flatMap((f) => scope.files.map((o) => [f, o]))
|
|
192
|
+
: scope.files;
|
|
193
|
+
const ignores = [...(block.ignores ?? []), ...scope.ignores];
|
|
194
|
+
return {
|
|
195
|
+
basePath: block.basePath,
|
|
196
|
+
files,
|
|
197
|
+
...(ignores.length ? { ignores } : {}),
|
|
198
|
+
rules,
|
|
199
|
+
};
|
|
200
|
+
}
|
|
@@ -1,13 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `paperlint.json` through the commands, on a real directory: `paperlint lint` applies a paper's
|
|
3
|
-
* `rules` to that paper alone and refuses an unknown rule id
|
|
4
|
-
* `venue.json` and refuses when both files exist and differ; `paperlint doctor` names a leftover.
|
|
3
|
+
* `rules` to that paper alone and refuses an unknown rule id.
|
|
5
4
|
*/
|
|
6
5
|
import {
|
|
7
|
-
existsSync,
|
|
8
6
|
mkdirSync,
|
|
9
7
|
mkdtempSync,
|
|
10
|
-
readFileSync,
|
|
11
8
|
realpathSync,
|
|
12
9
|
rmSync,
|
|
13
10
|
writeFileSync,
|
|
@@ -16,8 +13,6 @@ import { tmpdir } from "node:os";
|
|
|
16
13
|
import { join } from "node:path";
|
|
17
14
|
import { afterEach, describe, expect, it } from "vitest";
|
|
18
15
|
import { run, toolchainTex } from "./cli.ts";
|
|
19
|
-
import { migratePaperSettings } from "./init.ts";
|
|
20
|
-
import { doctor } from "./doctor.ts";
|
|
21
16
|
|
|
22
17
|
const dirs: string[] = [];
|
|
23
18
|
afterEach(() => {
|
|
@@ -29,11 +24,7 @@ function project(files: Record<string, string> = {}): string {
|
|
|
29
24
|
const root = realpathSync(mkdtempSync(join(tmpdir(), "paperlint-settings-")));
|
|
30
25
|
dirs.push(root);
|
|
31
26
|
const all: Record<string, string> = {
|
|
32
|
-
"package.json": JSON.stringify({
|
|
33
|
-
name: "c",
|
|
34
|
-
private: true,
|
|
35
|
-
paperlint: { papersDir: "papers" },
|
|
36
|
-
}),
|
|
27
|
+
"package.json": JSON.stringify({ name: "c", private: true }),
|
|
37
28
|
"papers/a/paper.tex":
|
|
38
29
|
"\\documentclass{article}\n\\begin{document}x\\end{document}\n",
|
|
39
30
|
"papers/a/PIPELINE-STATUS.md": "---\nstages: []\n---\n",
|
|
@@ -51,10 +42,11 @@ function project(files: Record<string, string> = {}): string {
|
|
|
51
42
|
|
|
52
43
|
async function lint(
|
|
53
44
|
root: string,
|
|
45
|
+
args: string[] = [],
|
|
54
46
|
): Promise<{ code: number; out: string; err: string }> {
|
|
55
47
|
const out: string[] = [];
|
|
56
48
|
const err: string[] = [];
|
|
57
|
-
const code = await run(["lint", "--json"], {
|
|
49
|
+
const code = await run(["lint", "--json", ...args], {
|
|
58
50
|
cwd: root,
|
|
59
51
|
log: (s: string) => out.push(s),
|
|
60
52
|
err: (s: string) => err.push(s),
|
|
@@ -67,6 +59,43 @@ const rulesIn = (stdout: string, paper: string): string[] =>
|
|
|
67
59
|
.filter((r) => r.filePath.endsWith(join(paper, "paper.tex")))
|
|
68
60
|
.flatMap((r) => r.messages.map((m) => m.ruleId));
|
|
69
61
|
|
|
62
|
+
describe("🔴 an optional rule that reaches no paper — judged against the PROJECT, not the run (#103)", () => {
|
|
63
|
+
const THIRD = {
|
|
64
|
+
"papers/c/paper.tex":
|
|
65
|
+
"\\documentclass{article}\n\\begin{document}x\\end{document}\n",
|
|
66
|
+
"papers/c/PIPELINE-STATUS.md": "---\nstages: []\n---\n",
|
|
67
|
+
};
|
|
68
|
+
const rootRules = (files: string[]) => ({
|
|
69
|
+
"paperlint.json": JSON.stringify({
|
|
70
|
+
rules: [{ files, rules: { "pdf/last-page-balance": "error" } }],
|
|
71
|
+
}),
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("linting paper c alone passes when the block reaches papers a and b", async () => {
|
|
75
|
+
const root = project({
|
|
76
|
+
...THIRD,
|
|
77
|
+
...rootRules(["papers/a/**", "papers/b/**"]),
|
|
78
|
+
});
|
|
79
|
+
const r = await lint(root, ["papers/c"]);
|
|
80
|
+
// Guards: the defect — the guard counted only the paper.tex files this run linted.
|
|
81
|
+
expect(r.err).not.toMatch(/no linted paper\.tex gets it/);
|
|
82
|
+
expect(r.code).toBe(0);
|
|
83
|
+
expect(rulesIn(r.out, "c")).not.toContain("pdf/last-page-balance");
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("…and a glob that reaches no paper of the project still fails, from a subset too", async () => {
|
|
87
|
+
const root = project({ ...THIRD, ...rootRules(["papers/typo/**"]) });
|
|
88
|
+
for (const args of [["papers/c"], []]) {
|
|
89
|
+
const r = await lint(root, args);
|
|
90
|
+
// Guards: the other half — widening the judged set did not turn the guard off.
|
|
91
|
+
expect(r.err).toMatch(
|
|
92
|
+
/pdf\/last-page-balance is turned on in "rules", but no paper\.tex/,
|
|
93
|
+
);
|
|
94
|
+
expect(r.code).toBe(1);
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
70
99
|
describe("paperlint lint — `rules` in a paper's paperlint.json", () => {
|
|
71
100
|
it("turns an optional rule on for that paper alone", async () => {
|
|
72
101
|
const root = project({
|
|
@@ -80,26 +109,18 @@ describe("paperlint lint — `rules` in a paper's paperlint.json", () => {
|
|
|
80
109
|
expect(rulesIn(r.out, "b")).not.toContain("pdf/last-page-balance");
|
|
81
110
|
});
|
|
82
111
|
|
|
83
|
-
it("the
|
|
112
|
+
it("🔴 the paper's own `rules` come after the root paperlint.json's, so they win", async () => {
|
|
84
113
|
const root = project({
|
|
85
|
-
"
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
files: ["papers/a/**"],
|
|
93
|
-
rules: { "pdf/last-page-balance": "off" },
|
|
94
|
-
},
|
|
95
|
-
],
|
|
96
|
-
},
|
|
114
|
+
"paperlint.json": JSON.stringify({
|
|
115
|
+
rules: [
|
|
116
|
+
{
|
|
117
|
+
files: ["papers/*/**"],
|
|
118
|
+
rules: { "pdf/last-page-balance": "error" },
|
|
119
|
+
},
|
|
120
|
+
],
|
|
97
121
|
}),
|
|
98
122
|
"papers/a/paperlint.json": JSON.stringify({
|
|
99
|
-
rules: { "pdf/last-page-balance": "
|
|
100
|
-
}),
|
|
101
|
-
"papers/b/paperlint.json": JSON.stringify({
|
|
102
|
-
rules: { "pdf/last-page-balance": "error" },
|
|
123
|
+
rules: { "pdf/last-page-balance": "off" },
|
|
103
124
|
}),
|
|
104
125
|
});
|
|
105
126
|
const r = await lint(root);
|
|
@@ -125,6 +146,61 @@ describe("paperlint lint — `rules` in a paper's paperlint.json", () => {
|
|
|
125
146
|
});
|
|
126
147
|
});
|
|
127
148
|
|
|
149
|
+
describe("papersDir — optional, `papers` by default", () => {
|
|
150
|
+
it("no paperlint.json, papers/x/paper.tex: that paper is linted", async () => {
|
|
151
|
+
const r = await lint(project());
|
|
152
|
+
expect(r.code).toBe(0);
|
|
153
|
+
expect(
|
|
154
|
+
(JSON.parse(r.out) as { filePath: string }[]).map((f) =>
|
|
155
|
+
f.filePath.slice(f.filePath.indexOf("papers/")),
|
|
156
|
+
),
|
|
157
|
+
).toEqual(
|
|
158
|
+
expect.arrayContaining(["papers/a/paper.tex", "papers/b/paper.tex"]),
|
|
159
|
+
);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it("🔴 no paperlint.json and no papers/: exit 2 and the one clear sentence", async () => {
|
|
163
|
+
const root = realpathSync(mkdtempSync(join(tmpdir(), "paperlint-empty-")));
|
|
164
|
+
dirs.push(root);
|
|
165
|
+
writeFileSync(join(root, "package.json"), "{}");
|
|
166
|
+
const r = await lint(root);
|
|
167
|
+
expect(r.code).toBe(2);
|
|
168
|
+
expect(r.err).toBe(
|
|
169
|
+
'no papers in papers/ — create one with `npx paperlint new <name>`, or set "papersDir" in paperlint.json if your papers live elsewhere',
|
|
170
|
+
);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it("an explicit papersDir is honored", async () => {
|
|
174
|
+
const root = project({
|
|
175
|
+
"paperlint.json": JSON.stringify({ papersDir: "docs/drafts" }),
|
|
176
|
+
"docs/drafts/c/paper.tex":
|
|
177
|
+
"\\documentclass{article}\n\\begin{document}x\\end{document}\n",
|
|
178
|
+
"docs/drafts/c/PIPELINE-STATUS.md": "---\nstages: []\n---\n",
|
|
179
|
+
});
|
|
180
|
+
const r = await lint(root);
|
|
181
|
+
const linted = (JSON.parse(r.out) as { filePath: string }[]).map((f) =>
|
|
182
|
+
f.filePath.slice(root.length + 1),
|
|
183
|
+
);
|
|
184
|
+
expect(linted).toContain("docs/drafts/c/paper.tex");
|
|
185
|
+
expect(linted.some((f) => f.startsWith("papers/"))).toBe(false);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it("run from inside a paper, the root file is still found and papersDir is relative to it", async () => {
|
|
189
|
+
const root = project({
|
|
190
|
+
"paperlint.json": JSON.stringify({ papersDir: "papers" }),
|
|
191
|
+
"papers/a/paperlint.json": JSON.stringify({ kind: "short" }),
|
|
192
|
+
});
|
|
193
|
+
const out: string[] = [];
|
|
194
|
+
const code = await run(["lint", "--json"], {
|
|
195
|
+
cwd: join(root, "papers/a"),
|
|
196
|
+
log: (x: string) => out.push(x),
|
|
197
|
+
err: () => {},
|
|
198
|
+
});
|
|
199
|
+
expect(code).toBe(0);
|
|
200
|
+
expect(out.join("\n")).toMatch(/papers\/b\/paper\.tex/);
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
|
|
128
204
|
describe("paperlint lint — the venue preset's rules", () => {
|
|
129
205
|
it("agenticdev's preset turns pdf/last-page-balance on for its paper alone", async () => {
|
|
130
206
|
const root = project({
|
|
@@ -136,16 +212,17 @@ describe("paperlint lint — the venue preset's rules", () => {
|
|
|
136
212
|
const cfgOf = await import("./cli.ts").then((m) =>
|
|
137
213
|
m.paperRuleBlocks([join(root, "papers")]),
|
|
138
214
|
);
|
|
139
|
-
expect(cfgOf.ok && cfgOf.value).toEqual(
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
"
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
215
|
+
expect(cfgOf.ok && cfgOf.value).toEqual({
|
|
216
|
+
preset: [
|
|
217
|
+
{
|
|
218
|
+
// Scoped to the paper by `basePath` alone: which of its files the rule reaches is decided
|
|
219
|
+
// once, by narrowing to paperlint's owned scopes (#101), not by a copy of their globs.
|
|
220
|
+
basePath: join(root, "papers/a"),
|
|
221
|
+
rules: { "pdf/last-page-balance": ["error", { tolerancePt: 120 }] },
|
|
222
|
+
},
|
|
223
|
+
],
|
|
224
|
+
own: [],
|
|
225
|
+
});
|
|
149
226
|
});
|
|
150
227
|
|
|
151
228
|
it("🔴 an unbuilt agenticdev paper: ONE warning (pdf/measured), and no balance error", async () => {
|
|
@@ -172,7 +249,11 @@ describe("paperlint lint — the paper over its preset, and the project's own pr
|
|
|
172
249
|
const blocks = await import("./cli.ts").then((m) =>
|
|
173
250
|
m.paperRuleBlocks([join(root, "papers")]),
|
|
174
251
|
);
|
|
175
|
-
|
|
252
|
+
// The preset's block comes first and the paper's own after it: the later block wins.
|
|
253
|
+
expect(blocks.ok && blocks.value.preset[0]?.rules).toEqual({
|
|
254
|
+
"pdf/last-page-balance": ["error", { tolerancePt: 120 }],
|
|
255
|
+
});
|
|
256
|
+
expect(blocks.ok && blocks.value.own[0]?.rules).toEqual({
|
|
176
257
|
"pdf/last-page-balance": "off",
|
|
177
258
|
});
|
|
178
259
|
});
|
|
@@ -227,85 +308,3 @@ describe("paperlint toolchain — installs what the project's own presets need",
|
|
|
227
308
|
expect("acmart" in tex.packages).toBe(true);
|
|
228
309
|
});
|
|
229
310
|
});
|
|
230
|
-
|
|
231
|
-
describe("paperlint init — moves venue.json to paperlint.json", () => {
|
|
232
|
-
const legacy = '{ "venue": "aisec", "kind": "research" }\n';
|
|
233
|
-
|
|
234
|
-
it('moves it, "venue" becoming "extends": "paperlint:<name>", and says so', () => {
|
|
235
|
-
const root = project({ "papers/a/venue.json": legacy });
|
|
236
|
-
const r = migratePaperSettings(join(root, "papers"));
|
|
237
|
-
expect(r.code).toBe(0);
|
|
238
|
-
expect(existsSync(join(root, "papers/a/venue.json"))).toBe(false);
|
|
239
|
-
expect(
|
|
240
|
-
JSON.parse(readFileSync(join(root, "papers/a/paperlint.json"), "utf8")),
|
|
241
|
-
).toEqual({ extends: "paperlint:aisec", kind: "research" });
|
|
242
|
-
expect(r.lines.join("\n")).toMatch(/a\/venue\.json → a\/paperlint\.json/);
|
|
243
|
-
});
|
|
244
|
-
|
|
245
|
-
it("both, with the same content: the leftover is removed", () => {
|
|
246
|
-
const root = project({
|
|
247
|
-
"papers/a/venue.json": legacy,
|
|
248
|
-
"papers/a/paperlint.json":
|
|
249
|
-
'{"extends":"paperlint:aisec","kind":"research"}',
|
|
250
|
-
});
|
|
251
|
-
expect(migratePaperSettings(join(root, "papers")).code).toBe(0);
|
|
252
|
-
expect(existsSync(join(root, "papers/a/venue.json"))).toBe(false);
|
|
253
|
-
});
|
|
254
|
-
|
|
255
|
-
it("🔴 both, and they differ: refused, naming both, and nothing is touched", () => {
|
|
256
|
-
const root = project({
|
|
257
|
-
"papers/a/venue.json": legacy,
|
|
258
|
-
"papers/a/paperlint.json": '{"venue":"realm"}',
|
|
259
|
-
});
|
|
260
|
-
const r = migratePaperSettings(join(root, "papers"));
|
|
261
|
-
expect(r.code).toBe(2);
|
|
262
|
-
expect(r.lines.join("\n")).toMatch(/a\/venue\.json.*a\/paperlint\.json/);
|
|
263
|
-
expect(readFileSync(join(root, "papers/a/venue.json"), "utf8")).toBe(
|
|
264
|
-
legacy,
|
|
265
|
-
);
|
|
266
|
-
expect(readFileSync(join(root, "papers/a/paperlint.json"), "utf8")).toBe(
|
|
267
|
-
'{"venue":"realm"}',
|
|
268
|
-
);
|
|
269
|
-
});
|
|
270
|
-
|
|
271
|
-
it("no venue.json anywhere: nothing to say", () => {
|
|
272
|
-
const root = project();
|
|
273
|
-
expect(migratePaperSettings(join(root, "papers"))).toEqual({
|
|
274
|
-
code: 0,
|
|
275
|
-
lines: [],
|
|
276
|
-
});
|
|
277
|
-
});
|
|
278
|
-
});
|
|
279
|
-
|
|
280
|
-
describe("paperlint doctor — names a leftover venue.json", () => {
|
|
281
|
-
it("✗ with the command that moves it, and a failing exit", () => {
|
|
282
|
-
const root = project({ "papers/a/venue.json": '{"venue":"aisec"}' });
|
|
283
|
-
const out: string[] = [];
|
|
284
|
-
const code = doctor({
|
|
285
|
-
log: (s: string) => out.push(s),
|
|
286
|
-
cwd: root,
|
|
287
|
-
projectDir: root,
|
|
288
|
-
cliPapers: "papers",
|
|
289
|
-
run: (() => ({ status: 0, stdout: "", stderr: "" })) as never,
|
|
290
|
-
skillLinks: () => ({ ok: false, error: "not checked in this test" }),
|
|
291
|
-
});
|
|
292
|
-
const text = out.join("\n");
|
|
293
|
-
expect(text).toMatch(/✗ papers\/a\/venue\.json is no longer read/);
|
|
294
|
-
expect(text).toMatch(/npx paperlint init/);
|
|
295
|
-
expect(code).not.toBe(0);
|
|
296
|
-
});
|
|
297
|
-
|
|
298
|
-
it("says nothing about it when there is none", () => {
|
|
299
|
-
const root = project();
|
|
300
|
-
const out: string[] = [];
|
|
301
|
-
doctor({
|
|
302
|
-
log: (s: string) => out.push(s),
|
|
303
|
-
cwd: root,
|
|
304
|
-
projectDir: root,
|
|
305
|
-
cliPapers: "papers",
|
|
306
|
-
run: (() => ({ status: 0, stdout: "", stderr: "" })) as never,
|
|
307
|
-
skillLinks: () => ({ ok: false, error: "not checked in this test" }),
|
|
308
|
-
});
|
|
309
|
-
expect(out.join("\n")).not.toMatch(/venue\.json/);
|
|
310
|
-
});
|
|
311
|
-
});
|