paperlint 2.1.0 → 3.0.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 +233 -224
- 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.d.ts +15 -0
- package/dist/build.d.ts.map +1 -1
- package/dist/build.js +38 -4
- package/dist/build.js.map +1 -1
- package/dist/cli.d.ts +28 -25
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +277 -265
- 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/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 +143 -172
- 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.map +1 -1
- package/dist/new-paper.js +2 -13
- package/dist/new-paper.js.map +1 -1
- package/dist/paper-files.d.ts +63 -0
- package/dist/paper-files.d.ts.map +1 -0
- package/dist/paper-files.js +69 -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/types.d.ts +20 -18
- package/dist/types.d.ts.map +1 -1
- package/dist/venue-rules.d.ts.map +1 -1
- package/dist/venue-rules.js +11 -9
- package/dist/venue-rules.js.map +1 -1
- package/docs/configuration.md +119 -104
- package/docs/e2e.md +1 -1
- package/docs/install.md +25 -40
- package/docs/optional-rules.md +5 -4
- package/docs/rules.md +60 -23
- 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 +267 -192
- package/eslint-rules/paper-typography.test.mjs +297 -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/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 +4 -13
- 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.harness.mjs +8 -4
- package/src/build.ts +62 -6
- package/src/cli.harness.mjs +147 -292
- package/src/cli.mutations.mjs +35 -44
- package/src/cli.ts +344 -304
- package/src/doctor.harness.mjs +41 -84
- package/src/doctor.mutations.mjs +9 -9
- package/src/doctor.ts +44 -89
- 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 +186 -230
- 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/new-paper.test.ts +1 -13
- package/src/new-paper.ts +2 -17
- package/src/paper-files.test.ts +290 -0
- package/src/paper-files.ts +144 -0
- package/src/paper-settings-commands.test.ts +84 -121
- package/src/paper-settings.test.ts +93 -64
- package/src/paper-settings.ts +99 -97
- 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/structure.harness.mjs +3 -1
- package/src/structure.ts +3 -3
- package/src/tex-requirements.ts +2 -2
- package/src/types.ts +30 -18
- package/src/venue-rules.test.ts +20 -11
- package/src/venue-rules.ts +11 -13
- 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,144 @@
|
|
|
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
|
+
* ESLint's semantics that this leans on, each measured against ESLint 10 before writing it:
|
|
28
|
+
* - a global-ignore pattern that matches a DIRECTORY hides everything inside it, so directories
|
|
29
|
+
* must be un-ignored (`!` + a pattern ending in a slash) before owned files can be;
|
|
30
|
+
* - that un-ignores `node_modules/` and `.git/` too — ESLint's own defaults — so they are
|
|
31
|
+
* re-ignored after, or a `paper.md` inside a dependency would be linted;
|
|
32
|
+
* - later global-ignore patterns win, across blocks, in config order;
|
|
33
|
+
* - `files: [["a", "b"]]` matches a file only when BOTH patterns match.
|
|
34
|
+
*/
|
|
35
|
+
import type { RuleBlock, RuleEntry } from "./rules-config.ts";
|
|
36
|
+
|
|
37
|
+
/** What this module reads of a config block. */
|
|
38
|
+
interface Block {
|
|
39
|
+
readonly files?: readonly unknown[];
|
|
40
|
+
readonly plugins?: Readonly<
|
|
41
|
+
Record<string, { readonly rules?: Readonly<Record<string, unknown>> }>
|
|
42
|
+
>;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** A `files` entry: one glob, or several that must ALL match. */
|
|
46
|
+
export type FilesEntry = string | readonly string[];
|
|
47
|
+
|
|
48
|
+
/** A consumer block after narrowing: its `files` may carry ANDed pairs. */
|
|
49
|
+
export interface ScopedBlock {
|
|
50
|
+
readonly basePath: string;
|
|
51
|
+
readonly files?: readonly FilesEntry[];
|
|
52
|
+
readonly ignores?: readonly string[];
|
|
53
|
+
readonly rules: Readonly<Record<string, RuleEntry>>;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const globsOf = (b: Block): string[] =>
|
|
57
|
+
(b.files ?? []).filter((f): f is string => typeof f === "string");
|
|
58
|
+
|
|
59
|
+
/** Every glob paperlint's own blocks lint, once, in config order. */
|
|
60
|
+
export function ownedPatterns(own: readonly unknown[]): string[] {
|
|
61
|
+
return [...new Set(own.flatMap((b) => globsOf(b as Block)))];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** ESLint's default ignores, re-applied after directories are un-ignored. */
|
|
65
|
+
const ESLINT_DEFAULT_IGNORES = ["**/node_modules/", "**/.git/"];
|
|
66
|
+
|
|
67
|
+
/** A global-ignore block that leaves ESLint nothing to lint but `owned`. */
|
|
68
|
+
export function scopeToOwned(owned: readonly string[]): {
|
|
69
|
+
readonly ignores: string[];
|
|
70
|
+
} {
|
|
71
|
+
return {
|
|
72
|
+
ignores: [
|
|
73
|
+
"**/*",
|
|
74
|
+
"!**/*/",
|
|
75
|
+
...owned.map((p) => `!${p}`),
|
|
76
|
+
...ESLINT_DEFAULT_IGNORES,
|
|
77
|
+
],
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Every `<plugin>/<rule>` a block registers. */
|
|
82
|
+
const ruleIdsOf = (b: Block): string[] =>
|
|
83
|
+
Object.entries(b.plugins ?? {}).flatMap(([name, plugin]) =>
|
|
84
|
+
Object.keys(plugin.rules ?? {}).map((rule) => `${name}/${rule}`),
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
/** An owner seen so far, widened by one more block's globs; `null` (every file) absorbs all. */
|
|
88
|
+
const widen = (
|
|
89
|
+
had: readonly string[] | null | undefined,
|
|
90
|
+
files: readonly string[] | null,
|
|
91
|
+
): readonly string[] | null =>
|
|
92
|
+
had === null || files === null
|
|
93
|
+
? null
|
|
94
|
+
: [...new Set([...(had ?? []), ...files])];
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Rule id → the globs of the blocks that register its plugin with that rule. `null`: the plugin
|
|
98
|
+
* is registered for every file (a block without `files`), so the rule resolves anywhere.
|
|
99
|
+
*/
|
|
100
|
+
export function ruleOwners(
|
|
101
|
+
own: readonly unknown[],
|
|
102
|
+
): Map<string, readonly string[] | null> {
|
|
103
|
+
const owners = new Map<string, readonly string[] | null>();
|
|
104
|
+
for (const raw of own) {
|
|
105
|
+
const b = raw as Block;
|
|
106
|
+
const files = b.files ? globsOf(b) : null;
|
|
107
|
+
for (const id of ruleIdsOf(b)) owners.set(id, widen(owners.get(id), files));
|
|
108
|
+
}
|
|
109
|
+
return owners;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* One consumer block → one block per owner, each carrying only that owner's rules, its `files`
|
|
114
|
+
* ANDed with the owner's globs. A rule no block of this config registers (the LaTeX block is
|
|
115
|
+
* absent when its parser did not load) gets no block: it has no file to reach in this run, and
|
|
116
|
+
* naming it to ESLint would only crash.
|
|
117
|
+
*/
|
|
118
|
+
export function narrowToOwners(
|
|
119
|
+
block: RuleBlock,
|
|
120
|
+
owners: ReadonlyMap<string, readonly string[] | null>,
|
|
121
|
+
): ScopedBlock[] {
|
|
122
|
+
const groups = new Map<string, Record<string, RuleEntry>>();
|
|
123
|
+
for (const [id, entry] of Object.entries(block.rules)) {
|
|
124
|
+
const owner = owners.get(id);
|
|
125
|
+
if (owner === undefined) continue;
|
|
126
|
+
const key = JSON.stringify(owner);
|
|
127
|
+
groups.set(key, { ...groups.get(key), [id]: entry });
|
|
128
|
+
}
|
|
129
|
+
return [...groups].map(([key, rules]) => {
|
|
130
|
+
const owner = JSON.parse(key) as string[] | null;
|
|
131
|
+
const files: readonly FilesEntry[] | undefined =
|
|
132
|
+
owner === null
|
|
133
|
+
? block.files
|
|
134
|
+
: block.files
|
|
135
|
+
? block.files.flatMap((f) => owner.map((o) => [f, o]))
|
|
136
|
+
: owner;
|
|
137
|
+
return {
|
|
138
|
+
basePath: block.basePath,
|
|
139
|
+
...(files ? { files } : {}),
|
|
140
|
+
...(block.ignores ? { ignores: block.ignores } : {}),
|
|
141
|
+
rules,
|
|
142
|
+
};
|
|
143
|
+
});
|
|
144
|
+
}
|
|
@@ -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",
|
|
@@ -80,26 +71,18 @@ describe("paperlint lint — `rules` in a paper's paperlint.json", () => {
|
|
|
80
71
|
expect(rulesIn(r.out, "b")).not.toContain("pdf/last-page-balance");
|
|
81
72
|
});
|
|
82
73
|
|
|
83
|
-
it("the
|
|
74
|
+
it("🔴 the paper's own `rules` come after the root paperlint.json's, so they win", async () => {
|
|
84
75
|
const root = project({
|
|
85
|
-
"
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
files: ["papers/a/**"],
|
|
93
|
-
rules: { "pdf/last-page-balance": "off" },
|
|
94
|
-
},
|
|
95
|
-
],
|
|
96
|
-
},
|
|
76
|
+
"paperlint.json": JSON.stringify({
|
|
77
|
+
rules: [
|
|
78
|
+
{
|
|
79
|
+
files: ["papers/*/**"],
|
|
80
|
+
rules: { "pdf/last-page-balance": "error" },
|
|
81
|
+
},
|
|
82
|
+
],
|
|
97
83
|
}),
|
|
98
84
|
"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" },
|
|
85
|
+
rules: { "pdf/last-page-balance": "off" },
|
|
103
86
|
}),
|
|
104
87
|
});
|
|
105
88
|
const r = await lint(root);
|
|
@@ -125,6 +108,61 @@ describe("paperlint lint — `rules` in a paper's paperlint.json", () => {
|
|
|
125
108
|
});
|
|
126
109
|
});
|
|
127
110
|
|
|
111
|
+
describe("papersDir — optional, `papers` by default", () => {
|
|
112
|
+
it("no paperlint.json, papers/x/paper.tex: that paper is linted", async () => {
|
|
113
|
+
const r = await lint(project());
|
|
114
|
+
expect(r.code).toBe(0);
|
|
115
|
+
expect(
|
|
116
|
+
(JSON.parse(r.out) as { filePath: string }[]).map((f) =>
|
|
117
|
+
f.filePath.slice(f.filePath.indexOf("papers/")),
|
|
118
|
+
),
|
|
119
|
+
).toEqual(
|
|
120
|
+
expect.arrayContaining(["papers/a/paper.tex", "papers/b/paper.tex"]),
|
|
121
|
+
);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it("🔴 no paperlint.json and no papers/: exit 2 and the one clear sentence", async () => {
|
|
125
|
+
const root = realpathSync(mkdtempSync(join(tmpdir(), "paperlint-empty-")));
|
|
126
|
+
dirs.push(root);
|
|
127
|
+
writeFileSync(join(root, "package.json"), "{}");
|
|
128
|
+
const r = await lint(root);
|
|
129
|
+
expect(r.code).toBe(2);
|
|
130
|
+
expect(r.err).toBe(
|
|
131
|
+
'no papers in papers/ — create one with `npx paperlint new <name>`, or set "papersDir" in paperlint.json if your papers live elsewhere',
|
|
132
|
+
);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it("an explicit papersDir is honored", async () => {
|
|
136
|
+
const root = project({
|
|
137
|
+
"paperlint.json": JSON.stringify({ papersDir: "docs/drafts" }),
|
|
138
|
+
"docs/drafts/c/paper.tex":
|
|
139
|
+
"\\documentclass{article}\n\\begin{document}x\\end{document}\n",
|
|
140
|
+
"docs/drafts/c/PIPELINE-STATUS.md": "---\nstages: []\n---\n",
|
|
141
|
+
});
|
|
142
|
+
const r = await lint(root);
|
|
143
|
+
const linted = (JSON.parse(r.out) as { filePath: string }[]).map((f) =>
|
|
144
|
+
f.filePath.slice(root.length + 1),
|
|
145
|
+
);
|
|
146
|
+
expect(linted).toContain("docs/drafts/c/paper.tex");
|
|
147
|
+
expect(linted.some((f) => f.startsWith("papers/"))).toBe(false);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it("run from inside a paper, the root file is still found and papersDir is relative to it", async () => {
|
|
151
|
+
const root = project({
|
|
152
|
+
"paperlint.json": JSON.stringify({ papersDir: "papers" }),
|
|
153
|
+
"papers/a/paperlint.json": JSON.stringify({ kind: "short" }),
|
|
154
|
+
});
|
|
155
|
+
const out: string[] = [];
|
|
156
|
+
const code = await run(["lint", "--json"], {
|
|
157
|
+
cwd: join(root, "papers/a"),
|
|
158
|
+
log: (x: string) => out.push(x),
|
|
159
|
+
err: () => {},
|
|
160
|
+
});
|
|
161
|
+
expect(code).toBe(0);
|
|
162
|
+
expect(out.join("\n")).toMatch(/papers\/b\/paper\.tex/);
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
|
|
128
166
|
describe("paperlint lint — the venue preset's rules", () => {
|
|
129
167
|
it("agenticdev's preset turns pdf/last-page-balance on for its paper alone", async () => {
|
|
130
168
|
const root = project({
|
|
@@ -136,16 +174,19 @@ describe("paperlint lint — the venue preset's rules", () => {
|
|
|
136
174
|
const cfgOf = await import("./cli.ts").then((m) =>
|
|
137
175
|
m.paperRuleBlocks([join(root, "papers")]),
|
|
138
176
|
);
|
|
139
|
-
expect(cfgOf.ok && cfgOf.value).toEqual(
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
177
|
+
expect(cfgOf.ok && cfgOf.value).toEqual({
|
|
178
|
+
preset: [
|
|
179
|
+
{
|
|
180
|
+
basePath: join(root, "papers/a"),
|
|
181
|
+
files: expect.arrayContaining([
|
|
182
|
+
"**/paper.tex",
|
|
183
|
+
"**/PIPELINE-STATUS.md",
|
|
184
|
+
]),
|
|
185
|
+
rules: { "pdf/last-page-balance": ["error", { tolerancePt: 120 }] },
|
|
186
|
+
},
|
|
187
|
+
],
|
|
188
|
+
own: [],
|
|
189
|
+
});
|
|
149
190
|
});
|
|
150
191
|
|
|
151
192
|
it("🔴 an unbuilt agenticdev paper: ONE warning (pdf/measured), and no balance error", async () => {
|
|
@@ -172,7 +213,11 @@ describe("paperlint lint — the paper over its preset, and the project's own pr
|
|
|
172
213
|
const blocks = await import("./cli.ts").then((m) =>
|
|
173
214
|
m.paperRuleBlocks([join(root, "papers")]),
|
|
174
215
|
);
|
|
175
|
-
|
|
216
|
+
// The preset's block comes first and the paper's own after it: the later block wins.
|
|
217
|
+
expect(blocks.ok && blocks.value.preset[0]?.rules).toEqual({
|
|
218
|
+
"pdf/last-page-balance": ["error", { tolerancePt: 120 }],
|
|
219
|
+
});
|
|
220
|
+
expect(blocks.ok && blocks.value.own[0]?.rules).toEqual({
|
|
176
221
|
"pdf/last-page-balance": "off",
|
|
177
222
|
});
|
|
178
223
|
});
|
|
@@ -227,85 +272,3 @@ describe("paperlint toolchain — installs what the project's own presets need",
|
|
|
227
272
|
expect("acmart" in tex.packages).toBe(true);
|
|
228
273
|
});
|
|
229
274
|
});
|
|
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
|
-
});
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `<paper>/paperlint.json` — the per-paper settings file
|
|
3
|
-
*
|
|
4
|
-
* block for that paper alone.
|
|
2
|
+
* `<paper>/paperlint.json` — the per-paper settings file: parsed strictly, merged over the root
|
|
3
|
+
* `paperlint.json`'s defaults, and `rules` turned into an ESLint block for that paper alone.
|
|
5
4
|
*/
|
|
6
5
|
import { describe, expect, it } from "vitest";
|
|
7
6
|
import { memoryFiles } from "./adapters/memory/index.ts";
|
|
7
|
+
import type { AbsolutePath } from "./domain/paths.ts";
|
|
8
|
+
import { findProjectRoot } from "../lib/paper-config.mjs";
|
|
8
9
|
import {
|
|
9
|
-
migrationOf,
|
|
10
10
|
paperRules,
|
|
11
11
|
parsePaperSettings,
|
|
12
12
|
readPaperSettings,
|
|
@@ -16,9 +16,8 @@ const PAPER = "/work/papers/p";
|
|
|
16
16
|
const SHIPPED = new Set([
|
|
17
17
|
"pdf/last-page-balance",
|
|
18
18
|
"pdf/profile",
|
|
19
|
-
"paper/
|
|
19
|
+
"paper/section-word",
|
|
20
20
|
]);
|
|
21
|
-
const enc = (s: string) => new TextEncoder().encode(s);
|
|
22
21
|
|
|
23
22
|
describe("parsePaperSettings", () => {
|
|
24
23
|
it("reads extends, kind, pdf and rules; every absent field is null", () => {
|
|
@@ -58,10 +57,10 @@ describe("parsePaperSettings — optional keys", () => {
|
|
|
58
57
|
[
|
|
59
58
|
"an unknown key (a typo is not silent)",
|
|
60
59
|
{ venu: "aisec" },
|
|
61
|
-
/unknown key "venu"
|
|
60
|
+
/unknown key "venu" — known keys: papersDir, structure, rules, extends, kind, pdf/,
|
|
62
61
|
],
|
|
63
62
|
[
|
|
64
|
-
"
|
|
63
|
+
"a comment key other than `$comment`",
|
|
65
64
|
{ extends: "paperlint:aisec", _: "note" },
|
|
66
65
|
/unknown key "_"/,
|
|
67
66
|
],
|
|
@@ -76,10 +75,15 @@ describe("parsePaperSettings — optional keys", () => {
|
|
|
76
75
|
/"extends" must be a non-empty string/,
|
|
77
76
|
],
|
|
78
77
|
[
|
|
79
|
-
"rules that are
|
|
80
|
-
{ rules:
|
|
78
|
+
"rules that are neither an object nor a list of blocks",
|
|
79
|
+
{ rules: "pdf/profile" },
|
|
81
80
|
/"rules" must be an object/,
|
|
82
81
|
],
|
|
82
|
+
[
|
|
83
|
+
"🔴 papersDir — a project setting, refused in a paper's file",
|
|
84
|
+
{ papersDir: "papers" },
|
|
85
|
+
/"papersDir" is a project setting — set it in the root paperlint\.json/,
|
|
86
|
+
],
|
|
83
87
|
["not an object at all", ["aisec"], /must be a JSON object/],
|
|
84
88
|
])("refuses %s", (_, json, why) => {
|
|
85
89
|
const r = parsePaperSettings(json);
|
|
@@ -104,21 +108,6 @@ describe("readPaperSettings", () => {
|
|
|
104
108
|
expect(r.ok && r.value?.extends).toBe("paperlint:aisec");
|
|
105
109
|
});
|
|
106
110
|
|
|
107
|
-
it("🔴 a venue.json alone is NOT read — it is named, with the command that moves it", () => {
|
|
108
|
-
const files = memoryFiles({ [`${PAPER}/venue.json`]: '{"venue":"aisec"}' });
|
|
109
|
-
const r = readPaperSettings(files, PAPER);
|
|
110
|
-
expect(r).toEqual({ ok: false, error: { kind: "legacy" } });
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
it("paperlint.json wins when both exist — the leftover is doctor's to report", () => {
|
|
114
|
-
const files = memoryFiles({
|
|
115
|
-
[`${PAPER}/venue.json`]: '{"venue":"realm"}',
|
|
116
|
-
[`${PAPER}/paperlint.json`]: '{"extends":"paperlint:aisec"}',
|
|
117
|
-
});
|
|
118
|
-
const r = readPaperSettings(files, PAPER);
|
|
119
|
-
expect(r.ok && r.value?.extends).toBe("paperlint:aisec");
|
|
120
|
-
});
|
|
121
|
-
|
|
122
111
|
it("not JSON: broken, with the parser's reason", () => {
|
|
123
112
|
const files = memoryFiles({
|
|
124
113
|
[`${PAPER}/paperlint.json`]: "{ extends: aisec",
|
|
@@ -129,52 +118,92 @@ describe("readPaperSettings", () => {
|
|
|
129
118
|
});
|
|
130
119
|
});
|
|
131
120
|
|
|
132
|
-
describe("
|
|
133
|
-
const
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
);
|
|
121
|
+
describe("readPaperSettings — the root paperlint.json's defaults, the paper's file over them", () => {
|
|
122
|
+
const ROOT = "/work";
|
|
123
|
+
const withFiles = (f: Record<string, string>) =>
|
|
124
|
+
memoryFiles({ [`${ROOT}/package.json`]: "{}", ...f });
|
|
125
|
+
const read = (f: Record<string, string>) => {
|
|
126
|
+
const r = readPaperSettings(withFiles(f), PAPER);
|
|
127
|
+
if (!r.ok) throw new Error(r.error.why);
|
|
128
|
+
return r.value;
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
it("no file at either level: null", () => {
|
|
132
|
+
expect(read({})).toBeNull();
|
|
133
|
+
});
|
|
138
134
|
|
|
139
|
-
it
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
'{ "extends": "paperlint:a", "kind": "short" }\n',
|
|
152
|
-
"drop-legacy",
|
|
153
|
-
],
|
|
154
|
-
[
|
|
155
|
-
"both, different",
|
|
156
|
-
'{"venue":"a"}',
|
|
157
|
-
'{"extends":"paperlint:b"}',
|
|
158
|
-
"conflict",
|
|
159
|
-
],
|
|
160
|
-
["venue.json that is not JSON", "{", null, "broken"],
|
|
161
|
-
["venue.json that is not an object", "[]", null, "broken"],
|
|
162
|
-
])("%s → %s", (_, legacy, current, want) => {
|
|
163
|
-
expect(plan(legacy, current).kind).toBe(want);
|
|
135
|
+
it("root only: its extends and kind are this paper's", () => {
|
|
136
|
+
expect(
|
|
137
|
+
read({
|
|
138
|
+
[`${ROOT}/paperlint.json`]:
|
|
139
|
+
'{"papersDir":"papers","extends":"paperlint:agenticdev","kind":"short"}',
|
|
140
|
+
}),
|
|
141
|
+
).toEqual({
|
|
142
|
+
extends: "paperlint:agenticdev",
|
|
143
|
+
kind: "short",
|
|
144
|
+
pdf: null,
|
|
145
|
+
rules: null,
|
|
146
|
+
});
|
|
164
147
|
});
|
|
165
148
|
|
|
166
|
-
it(
|
|
167
|
-
|
|
168
|
-
'{"
|
|
169
|
-
|
|
170
|
-
);
|
|
171
|
-
expect(r.kind === "move" && JSON.parse(r.text)).toEqual({
|
|
149
|
+
it("paper only: its own values", () => {
|
|
150
|
+
expect(
|
|
151
|
+
read({ [`${PAPER}/paperlint.json`]: '{"extends":"paperlint:aisec"}' }),
|
|
152
|
+
).toEqual({
|
|
172
153
|
extends: "paperlint:aisec",
|
|
154
|
+
kind: null,
|
|
155
|
+
pdf: null,
|
|
156
|
+
rules: null,
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it("🔴 both: the paper's value wins, an absent one falls back to the root's, and `\"extends\": null` is absent", () => {
|
|
161
|
+
expect(
|
|
162
|
+
read({
|
|
163
|
+
[`${ROOT}/paperlint.json`]:
|
|
164
|
+
'{"extends":"paperlint:agenticdev","kind":"short","rules":{"pdf/profile":"off"}}',
|
|
165
|
+
[`${PAPER}/paperlint.json`]:
|
|
166
|
+
'{"extends":null,"kind":"research","rules":{"pdf/fonts":"off"}}',
|
|
167
|
+
}),
|
|
168
|
+
).toEqual({
|
|
169
|
+
extends: "paperlint:agenticdev",
|
|
173
170
|
kind: "research",
|
|
174
|
-
pdf:
|
|
175
|
-
|
|
171
|
+
pdf: null,
|
|
172
|
+
// The root's rules are the project's blocks (cli.ts), not this paper's.
|
|
173
|
+
rules: { "pdf/fonts": "off" },
|
|
176
174
|
});
|
|
177
175
|
});
|
|
176
|
+
|
|
177
|
+
it("papersDir in a paper's file: broken, naming the root file as its place", () => {
|
|
178
|
+
const r = readPaperSettings(
|
|
179
|
+
withFiles({ [`${PAPER}/paperlint.json`]: '{"papersDir":"x"}' }),
|
|
180
|
+
PAPER,
|
|
181
|
+
);
|
|
182
|
+
expect(r.ok).toBe(false);
|
|
183
|
+
if (!r.ok) expect(r.error.why).toMatch(/project setting/);
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
describe("findProjectRoot — where the root paperlint.json is looked for", () => {
|
|
188
|
+
const ROOT = "/work";
|
|
189
|
+
const withFiles = (f: Record<string, string>) =>
|
|
190
|
+
memoryFiles({ [`${ROOT}/package.json`]: "{}", ...f });
|
|
191
|
+
|
|
192
|
+
it("🔴 from INSIDE a paper, the project root is not the paper: its paperlint.json sits beside paper.tex", () => {
|
|
193
|
+
const files = withFiles({
|
|
194
|
+
[`${PAPER}/paper.tex`]: "x",
|
|
195
|
+
[`${PAPER}/paperlint.json`]: '{"kind":"research"}',
|
|
196
|
+
[`${ROOT}/paperlint.json`]: '{"extends":"paperlint:aisec"}',
|
|
197
|
+
});
|
|
198
|
+
const isFile = (p: string) => files.isFile(p as AbsolutePath);
|
|
199
|
+
expect(findProjectRoot(PAPER, isFile)).toBe(ROOT);
|
|
200
|
+
// No root file anywhere: the package.json directory.
|
|
201
|
+
expect(findProjectRoot(PAPER, (p) => p === `${ROOT}/package.json`)).toBe(
|
|
202
|
+
ROOT,
|
|
203
|
+
);
|
|
204
|
+
// Neither: where the walk started.
|
|
205
|
+
expect(findProjectRoot(PAPER, () => false)).toBe(PAPER);
|
|
206
|
+
});
|
|
178
207
|
});
|
|
179
208
|
|
|
180
209
|
describe("paperRules — `rules` in paperlint.json", () => {
|