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
|
@@ -84,13 +84,13 @@ process.exit(
|
|
|
84
84
|
edits: [
|
|
85
85
|
[
|
|
86
86
|
SRC,
|
|
87
|
-
'
|
|
88
|
-
'
|
|
87
|
+
' symlinkSync,\n} from "node:fs";',
|
|
88
|
+
' symlinkSync,\n rmSync,\n} from "node:fs";',
|
|
89
89
|
],
|
|
90
90
|
[
|
|
91
91
|
SRC,
|
|
92
|
-
|
|
93
|
-
' if (!write || seen.status === "present")
|
|
92
|
+
' if (!write || seen.status !== "missing")',
|
|
93
|
+
' if (!write || seen.status === "present")',
|
|
94
94
|
],
|
|
95
95
|
[
|
|
96
96
|
SRC,
|
|
@@ -123,7 +123,11 @@ process.exit(
|
|
|
123
123
|
"the difference between looking and changing. `paperlint doctor` would quietly repair the " +
|
|
124
124
|
"state it is supposed to report, so the report can never show the gap",
|
|
125
125
|
edits: [
|
|
126
|
-
[
|
|
126
|
+
[
|
|
127
|
+
SRC,
|
|
128
|
+
' if (!write || seen.status !== "missing")',
|
|
129
|
+
' if (seen.status !== "missing")',
|
|
130
|
+
],
|
|
127
131
|
[
|
|
128
132
|
SRC,
|
|
129
133
|
" if (write && shipped.names.length > 0) {",
|
package/src/link-skills.ts
CHANGED
|
@@ -34,15 +34,13 @@ import {
|
|
|
34
34
|
readlinkSync,
|
|
35
35
|
realpathSync,
|
|
36
36
|
symlinkSync,
|
|
37
|
-
unlinkSync,
|
|
38
37
|
} from "node:fs";
|
|
39
38
|
/* eslint-enable boundaries/dependencies */
|
|
40
39
|
import { createRequire } from "node:module";
|
|
41
|
-
import { dirname, join, relative, resolve
|
|
40
|
+
import { dirname, join, relative, resolve } from "node:path";
|
|
42
41
|
import { pathToFileURL } from "node:url";
|
|
43
42
|
import {
|
|
44
43
|
installedSkills,
|
|
45
|
-
LEGACY_PACKAGE_NAME,
|
|
46
44
|
PACKAGE_NAME,
|
|
47
45
|
SHIPPED_SKILLS_DIR,
|
|
48
46
|
} from "../skills/paper-pipeline/scripts/consumer.mjs";
|
|
@@ -109,8 +107,7 @@ export function shippedSkills(
|
|
|
109
107
|
}
|
|
110
108
|
}
|
|
111
109
|
|
|
112
|
-
export type LinkStatus =
|
|
113
|
-
"created" | "replaced" | "present" | "missing" | "foreign";
|
|
110
|
+
export type LinkStatus = "created" | "present" | "missing" | "foreign";
|
|
114
111
|
|
|
115
112
|
export interface SkillLink {
|
|
116
113
|
readonly name: string;
|
|
@@ -129,27 +126,17 @@ export type LinkReport =
|
|
|
129
126
|
}
|
|
130
127
|
| { readonly ok: false; readonly error: string };
|
|
131
128
|
|
|
132
|
-
/**
|
|
133
|
-
* What occupies `entry`, judged against the directory it should lead to. A link spelled exactly as
|
|
134
|
-
* an install under the package's old name wrote it (`legacyTarget`) is `stale`: ours, from before
|
|
135
|
-
* the rename, and replaced on the next `init`.
|
|
136
|
-
*/
|
|
129
|
+
/** What occupies `entry`, judged against the directory it should lead to. */
|
|
137
130
|
function inspect(
|
|
138
131
|
entry: string,
|
|
139
132
|
want: string,
|
|
140
|
-
|
|
141
|
-
): { status: "present" | "missing" | "foreign" | "stale"; reason?: string } {
|
|
133
|
+
): { status: "present" | "missing" | "foreign"; reason?: string } {
|
|
142
134
|
let st;
|
|
143
135
|
try {
|
|
144
136
|
st = lstatSync(entry);
|
|
145
137
|
} catch {
|
|
146
138
|
return { status: "missing" };
|
|
147
139
|
}
|
|
148
|
-
if (st.isSymbolicLink() && readlinkSync(entry) === legacyTarget)
|
|
149
|
-
return {
|
|
150
|
-
status: "stale",
|
|
151
|
-
reason: `a link into ${LEGACY_PACKAGE_NAME}, the package's old name — \`npx ${PACKAGE_NAME} init\` replaces it`,
|
|
152
|
-
};
|
|
153
140
|
try {
|
|
154
141
|
if (realpathSync(entry) === want) return { status: "present" };
|
|
155
142
|
} catch {
|
|
@@ -168,10 +155,7 @@ function inspect(
|
|
|
168
155
|
};
|
|
169
156
|
}
|
|
170
157
|
|
|
171
|
-
/**
|
|
172
|
-
* One skill: inspect its entry and, when writing, create the link — or replace one an install
|
|
173
|
-
* under the package's old name made.
|
|
174
|
-
*/
|
|
158
|
+
/** One skill: inspect its entry and, when writing, create the link. */
|
|
175
159
|
function linkOne(
|
|
176
160
|
name: string,
|
|
177
161
|
{
|
|
@@ -181,22 +165,14 @@ function linkOne(
|
|
|
181
165
|
write,
|
|
182
166
|
}: { entry: string; target: string; want: string; write: boolean },
|
|
183
167
|
): SkillLink {
|
|
184
|
-
const
|
|
185
|
-
|
|
186
|
-
`node_modules${sep}${LEGACY_PACKAGE_NAME}${sep}`,
|
|
187
|
-
);
|
|
188
|
-
const seen = inspect(entry, want, legacyTarget);
|
|
189
|
-
const fixable = seen.status === "missing" || seen.status === "stale";
|
|
190
|
-
if (!write || !fixable) {
|
|
191
|
-
const status = seen.status === "stale" ? "foreign" : seen.status;
|
|
168
|
+
const seen = inspect(entry, want);
|
|
169
|
+
if (!write || seen.status !== "missing")
|
|
192
170
|
return seen.reason
|
|
193
|
-
? { name, status, reason: seen.reason }
|
|
194
|
-
: { name, status };
|
|
195
|
-
}
|
|
171
|
+
? { name, status: seen.status, reason: seen.reason }
|
|
172
|
+
: { name, status: seen.status };
|
|
196
173
|
try {
|
|
197
|
-
if (seen.status === "stale") unlinkSync(entry);
|
|
198
174
|
symlinkSync(target, entry, "dir");
|
|
199
|
-
return { name, status:
|
|
175
|
+
return { name, status: "created" };
|
|
200
176
|
} catch (e) {
|
|
201
177
|
return {
|
|
202
178
|
name,
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `paperlint lint --fix`, and the settings that replaced the ratchets, through the command.
|
|
3
|
+
*/
|
|
4
|
+
import {
|
|
5
|
+
mkdirSync,
|
|
6
|
+
mkdtempSync,
|
|
7
|
+
readFileSync,
|
|
8
|
+
realpathSync,
|
|
9
|
+
rmSync,
|
|
10
|
+
writeFileSync,
|
|
11
|
+
} from "node:fs";
|
|
12
|
+
import { tmpdir } from "node:os";
|
|
13
|
+
import { join } from "node:path";
|
|
14
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
15
|
+
import { run } from "./cli.ts";
|
|
16
|
+
|
|
17
|
+
const dirs: string[] = [];
|
|
18
|
+
afterEach(() => {
|
|
19
|
+
for (const d of dirs.splice(0)) rmSync(d, { recursive: true, force: true });
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
function project(
|
|
23
|
+
settings: Record<string, unknown>,
|
|
24
|
+
files: Record<string, string> = {},
|
|
25
|
+
): string {
|
|
26
|
+
const root = realpathSync(mkdtempSync(join(tmpdir(), "paperlint-fix-")));
|
|
27
|
+
dirs.push(root);
|
|
28
|
+
const all: Record<string, string> = {
|
|
29
|
+
"package.json": JSON.stringify({ name: "c", private: true }),
|
|
30
|
+
"paperlint.json": JSON.stringify({ papersDir: "papers", ...settings }),
|
|
31
|
+
"papers/a/PIPELINE-STATUS.md": "---\nstages: []\n---\n",
|
|
32
|
+
"papers/a/paper.md": "# Intro\n\nSee §5, §6 and §7; p < .05.\n",
|
|
33
|
+
...files,
|
|
34
|
+
};
|
|
35
|
+
for (const [p, text] of Object.entries(all)) {
|
|
36
|
+
mkdirSync(join(root, p, ".."), { recursive: true });
|
|
37
|
+
writeFileSync(join(root, p), text);
|
|
38
|
+
}
|
|
39
|
+
return root;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function lint(root: string, args: string[] = []) {
|
|
43
|
+
const out: string[] = [];
|
|
44
|
+
const err: string[] = [];
|
|
45
|
+
const code = await run(["lint", "--json", ...args], {
|
|
46
|
+
cwd: root,
|
|
47
|
+
log: (s: string) => out.push(s),
|
|
48
|
+
err: (s: string) => err.push(s),
|
|
49
|
+
});
|
|
50
|
+
return { code, out: out.join("\n"), err: err.join("\n") };
|
|
51
|
+
}
|
|
52
|
+
const ruleIds = (stdout: string): string[] =>
|
|
53
|
+
(JSON.parse(stdout) as { messages: { ruleId: string }[] }[]).flatMap((r) =>
|
|
54
|
+
r.messages.map((m) => m.ruleId),
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
describe("paperlint lint --fix", () => {
|
|
58
|
+
it("🔴 three `§` and a bare decimal → --fix → zero findings, and the file says Section", async () => {
|
|
59
|
+
const root = project({});
|
|
60
|
+
const before = ruleIds((await lint(root)).out);
|
|
61
|
+
expect(before.filter((r) => r === "paper/section-word")).toHaveLength(3);
|
|
62
|
+
expect(before).toContain("paper/leading-zero");
|
|
63
|
+
const fixed = await lint(root, ["--fix"]);
|
|
64
|
+
expect(ruleIds(fixed.out)).not.toContain("paper/section-word");
|
|
65
|
+
expect(ruleIds(fixed.out)).not.toContain("paper/leading-zero");
|
|
66
|
+
expect(readFileSync(join(root, "papers/a/paper.md"), "utf8")).toBe(
|
|
67
|
+
"# Intro\n\nSee Section 5, Section 6 and Section 7; p < 0.05.\n",
|
|
68
|
+
);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("without --fix nothing is written", async () => {
|
|
72
|
+
const root = project({});
|
|
73
|
+
await lint(root);
|
|
74
|
+
expect(readFileSync(join(root, "papers/a/paper.md"), "utf8")).toMatch(/§5/);
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
describe("sibling cards: `read:` is required, from the package's own schema", () => {
|
|
79
|
+
it("a card without it is a warning; with it, silent; the index is not a card", async () => {
|
|
80
|
+
const root = project(
|
|
81
|
+
{},
|
|
82
|
+
{
|
|
83
|
+
"papers/a/siblings/README.md": "# Siblings\n",
|
|
84
|
+
"papers/a/siblings/smith2025.md": "# Smith 2025\n",
|
|
85
|
+
"papers/a/siblings/jones2024.md": "---\nread: abstract\n---\n# Jones\n",
|
|
86
|
+
},
|
|
87
|
+
);
|
|
88
|
+
const results = JSON.parse((await lint(root)).out) as {
|
|
89
|
+
filePath: string;
|
|
90
|
+
messages: { ruleId: string; severity: number }[];
|
|
91
|
+
}[];
|
|
92
|
+
const of = (name: string) =>
|
|
93
|
+
results.find((r) => r.filePath.endsWith(name))?.messages ?? null;
|
|
94
|
+
expect(of("smith2025.md")?.map((m) => [m.ruleId, m.severity])).toEqual([
|
|
95
|
+
["sibling/frontmatter", 1],
|
|
96
|
+
]);
|
|
97
|
+
expect(of("jones2024.md")).toEqual([]);
|
|
98
|
+
expect(of("siblings/README.md")).toBeNull();
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
describe("settings: what 3.0.0 removed is an ordinary unknown key or rule", () => {
|
|
103
|
+
it.each([
|
|
104
|
+
"typographyDebt",
|
|
105
|
+
"authorListCommand",
|
|
106
|
+
"docFields",
|
|
107
|
+
"causeMarker",
|
|
108
|
+
"reviewSchema",
|
|
109
|
+
])("%s → the generic unknown-key error", async (key) => {
|
|
110
|
+
const r = await lint(project({ [key]: 1 }));
|
|
111
|
+
expect(r.code).toBe(2);
|
|
112
|
+
expect(r.err).toMatch(new RegExp(`unknown key "${key}"`));
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("paper/typography → not a rule paperlint ships", async () => {
|
|
116
|
+
const r = await lint(project({ rules: { "paper/typography": "off" } }));
|
|
117
|
+
expect(r.code).toBe(2);
|
|
118
|
+
expect(r.err).toMatch(/"paper\/typography" is not a rule paperlint ships/);
|
|
119
|
+
});
|
|
120
|
+
});
|
package/src/new-paper.test.ts
CHANGED
|
@@ -83,14 +83,6 @@ describe("paperlint new — paperlint.json", () => {
|
|
|
83
83
|
extends: "paperlint:aisec",
|
|
84
84
|
});
|
|
85
85
|
});
|
|
86
|
-
|
|
87
|
-
it("is not added beside a pre-2.1.0 venue.json — `paperlint init` moves that one", () => {
|
|
88
|
-
const papers = join(tmp(), "papers");
|
|
89
|
-
mkdirSync(join(papers, "old"), { recursive: true });
|
|
90
|
-
writeFileSync(join(papers, "old", "venue.json"), '{"venue":"aisec"}');
|
|
91
|
-
const r = newPaper(papers, "old", "tex");
|
|
92
|
-
expect(r.ok && r.files.map((f) => f.file)).not.toContain("paperlint.json");
|
|
93
|
-
});
|
|
94
86
|
});
|
|
95
87
|
|
|
96
88
|
describe("paperlint lint — a paper with no venue preset chosen", () => {
|
|
@@ -98,11 +90,7 @@ describe("paperlint lint — a paper with no venue preset chosen", () => {
|
|
|
98
90
|
const root = tmp();
|
|
99
91
|
writeFileSync(
|
|
100
92
|
join(root, "package.json"),
|
|
101
|
-
JSON.stringify({
|
|
102
|
-
name: "c",
|
|
103
|
-
private: true,
|
|
104
|
-
paperlint: { papersDir: "papers" },
|
|
105
|
-
}),
|
|
93
|
+
JSON.stringify({ name: "c", private: true }),
|
|
106
94
|
);
|
|
107
95
|
newPaper(join(root, "papers"), "demo", "tex");
|
|
108
96
|
if (extendsValue !== null)
|
package/src/new-paper.ts
CHANGED
|
@@ -25,10 +25,7 @@ import {
|
|
|
25
25
|
/* eslint-enable boundaries/dependencies */
|
|
26
26
|
import { join } from "node:path";
|
|
27
27
|
import { fileURLToPath } from "node:url";
|
|
28
|
-
import {
|
|
29
|
-
LEGACY_PAPER_SETTINGS_FILE,
|
|
30
|
-
PAPER_SETTINGS_FILE,
|
|
31
|
-
} from "../lib/paper-config.mjs";
|
|
28
|
+
import { CONFIG_FILE } from "../lib/paper-config.mjs";
|
|
32
29
|
|
|
33
30
|
export type PaperFormat = "tex" | "md";
|
|
34
31
|
export const FORMATS: readonly PaperFormat[] = ["tex", "md"];
|
|
@@ -91,7 +88,7 @@ export type NewPaperResult =
|
|
|
91
88
|
* already has a source in EITHER format, in which case that one stands — and `paperlint.json`.
|
|
92
89
|
*/
|
|
93
90
|
export function wantedFiles(dir: string, format: PaperFormat): string[] {
|
|
94
|
-
return [...scorecardAndSource(dir, format),
|
|
91
|
+
return [...scorecardAndSource(dir, format), CONFIG_FILE];
|
|
95
92
|
}
|
|
96
93
|
|
|
97
94
|
function scorecardAndSource(dir: string, format: PaperFormat): string[] {
|
|
@@ -101,18 +98,6 @@ function scorecardAndSource(dir: string, format: PaperFormat): string[] {
|
|
|
101
98
|
return hasSource ? [STATUS_FILE] : [STATUS_FILE, SOURCE_FILE[format]];
|
|
102
99
|
}
|
|
103
100
|
|
|
104
|
-
/**
|
|
105
|
-
* `paperlint.json`, so that nobody has to know the file exists: its template says what to put in
|
|
106
|
-
* it, and until a venue preset is named lint says so in one warning. Not beside a pre-2.1.0
|
|
107
|
-
* `venue.json`: `paperlint init` moves that file, and a second, different one would make the move
|
|
108
|
-
* refuse.
|
|
109
|
-
*/
|
|
110
|
-
function settingsFile(dir: string): string[] {
|
|
111
|
-
return existsSync(join(dir, LEGACY_PAPER_SETTINGS_FILE))
|
|
112
|
-
? []
|
|
113
|
-
: [PAPER_SETTINGS_FILE];
|
|
114
|
-
}
|
|
115
|
-
|
|
116
101
|
// Documented in README.md#starting-a-paper — update it when this changes.
|
|
117
102
|
export function newPaper(
|
|
118
103
|
papersRoot: string,
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WHICH FILES `paperlint lint` LINTS — only the ones its own blocks claim, and a rule named in
|
|
3
|
+
* `rules` reaches only the files it was written for.
|
|
4
|
+
*
|
|
5
|
+
* The defect this closes (found migrating a consumer project with vendored JS under a paper's
|
|
6
|
+
* `repro/` folder): paperlint handed whole directories to ESLint, and ESLint's built-in defaults
|
|
7
|
+
* then linted every `.js`/`.mjs`/`.cjs` under the papers directory. 1,877 vendored files produced
|
|
8
|
+
* 74 errors — parse errors and "Definition for rule … was not found" — none of them on a paper
|
|
9
|
+
* file, and the run failed. Nothing in the settings could turn that off.
|
|
10
|
+
*/
|
|
11
|
+
import { ESLint, type Linter } from "eslint";
|
|
12
|
+
import {
|
|
13
|
+
mkdirSync,
|
|
14
|
+
mkdtempSync,
|
|
15
|
+
realpathSync,
|
|
16
|
+
rmSync,
|
|
17
|
+
writeFileSync,
|
|
18
|
+
} from "node:fs";
|
|
19
|
+
import { tmpdir } from "node:os";
|
|
20
|
+
import { join } from "node:path";
|
|
21
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
22
|
+
import { buildConfig, run } from "./cli.ts";
|
|
23
|
+
import {
|
|
24
|
+
narrowToOwners,
|
|
25
|
+
ownedPatterns,
|
|
26
|
+
ruleOwners,
|
|
27
|
+
scopeToOwned,
|
|
28
|
+
} from "./paper-files.ts";
|
|
29
|
+
|
|
30
|
+
const dirs: string[] = [];
|
|
31
|
+
afterEach(() => {
|
|
32
|
+
for (const d of dirs.splice(0)) rmSync(d, { recursive: true, force: true });
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
const TEX = "\\documentclass{article}\n\\begin{document}x\\end{document}\n";
|
|
36
|
+
|
|
37
|
+
/** A project with one paper `a` (paper.tex + PIPELINE-STATUS.md), plus `files`. */
|
|
38
|
+
function project(files: Record<string, string> = {}): string {
|
|
39
|
+
const root = realpathSync(mkdtempSync(join(tmpdir(), "paperlint-scope-")));
|
|
40
|
+
dirs.push(root);
|
|
41
|
+
const all: Record<string, string> = {
|
|
42
|
+
"package.json": JSON.stringify({ name: "c", private: true }),
|
|
43
|
+
"papers/a/paper.tex": TEX,
|
|
44
|
+
"papers/a/PIPELINE-STATUS.md": "---\nstages: []\n---\n",
|
|
45
|
+
...files,
|
|
46
|
+
};
|
|
47
|
+
for (const [p, text] of Object.entries(all)) {
|
|
48
|
+
mkdirSync(join(root, p, ".."), { recursive: true });
|
|
49
|
+
writeFileSync(join(root, p), text);
|
|
50
|
+
}
|
|
51
|
+
return root;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function lint(
|
|
55
|
+
root: string,
|
|
56
|
+
args: string[] = [],
|
|
57
|
+
): Promise<{ code: number; out: string; err: string }> {
|
|
58
|
+
const out: string[] = [];
|
|
59
|
+
const err: string[] = [];
|
|
60
|
+
const code = await run(["lint", "--json", ...args], {
|
|
61
|
+
cwd: root,
|
|
62
|
+
log: (s: string) => out.push(s),
|
|
63
|
+
err: (s: string) => err.push(s),
|
|
64
|
+
});
|
|
65
|
+
return { code, out: out.join("\n"), err: err.join("\n") };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const linted = (stdout: string): string[] =>
|
|
69
|
+
(JSON.parse(stdout) as { filePath: string }[]).map((r) => r.filePath);
|
|
70
|
+
|
|
71
|
+
/** Vendored code under a paper: a syntax error, an unknown rule in a directive, plain CommonJS. */
|
|
72
|
+
const VENDORED = {
|
|
73
|
+
"papers/a/repro/x.js": "return 1;\n",
|
|
74
|
+
"papers/a/repro/y.mjs":
|
|
75
|
+
"// eslint-disable-next-line import/no-nodejs-modules\nimport fs from 'node:fs';\n",
|
|
76
|
+
"papers/a/repro/lib/z.cjs": "module.exports = 1;\n",
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
describe("paperlint lint — lints only the files paperlint owns", () => {
|
|
80
|
+
it("🔴 vendored JS under a paper is never linted, and the run stays green", async () => {
|
|
81
|
+
const r = await lint(project(VENDORED));
|
|
82
|
+
expect(r.err).not.toMatch(/Parsing error|was not found/);
|
|
83
|
+
const files = linted(r.out);
|
|
84
|
+
expect(files.filter((f) => /\.[cm]?js$/.test(f))).toEqual([]);
|
|
85
|
+
expect(r.code).toBe(0);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("…while the paper's own files ARE linted (the other half)", async () => {
|
|
89
|
+
const root = project({
|
|
90
|
+
...VENDORED,
|
|
91
|
+
"papers/a/reviews/r1.md": "# review\n",
|
|
92
|
+
});
|
|
93
|
+
const files = linted((await lint(root)).out);
|
|
94
|
+
for (const f of [
|
|
95
|
+
"papers/a/paper.tex",
|
|
96
|
+
"papers/a/PIPELINE-STATUS.md",
|
|
97
|
+
"papers/a/reviews/r1.md",
|
|
98
|
+
])
|
|
99
|
+
expect(files).toContain(join(root, f));
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("a papers directory holding only vendored JS lints NOTHING — and says so", async () => {
|
|
103
|
+
const root = project({
|
|
104
|
+
"papers/a/paper.tex": "",
|
|
105
|
+
...VENDORED,
|
|
106
|
+
});
|
|
107
|
+
rmSync(join(root, "papers/a/paper.tex"));
|
|
108
|
+
rmSync(join(root, "papers/a/PIPELINE-STATUS.md"));
|
|
109
|
+
// From the config: the directory holds no paper, which is said before ESLint runs.
|
|
110
|
+
const fromConfig = await lint(root);
|
|
111
|
+
expect(fromConfig.code).toBe(2);
|
|
112
|
+
expect(fromConfig.err).toMatch(/^no papers in papers\//);
|
|
113
|
+
// Named on the command line: ESLint gets it, lints nothing, and the run says so.
|
|
114
|
+
const named = await lint(root, ["papers"]);
|
|
115
|
+
expect(named.code).toBe(1);
|
|
116
|
+
expect(named.err).toMatch(/nothing was linted/);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("a file named on the command line that paperlint does not lint is refused by name", async () => {
|
|
120
|
+
const root = project(VENDORED);
|
|
121
|
+
const r = await lint(root, ["papers/a/repro/x.js"]);
|
|
122
|
+
expect(r.code).toBe(2);
|
|
123
|
+
expect(r.err).toMatch(
|
|
124
|
+
/papers\/a\/repro\/x\.js is not a file paperlint lints/,
|
|
125
|
+
);
|
|
126
|
+
expect(r.err).toMatch(/PIPELINE-STATUS\.md/);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it("a paper file named on the command line is still linted", async () => {
|
|
130
|
+
const root = project(VENDORED);
|
|
131
|
+
const r = await lint(root, ["papers/a/paper.tex"]);
|
|
132
|
+
expect(linted(r.out)).toEqual([join(root, "papers/a/paper.tex")]);
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
describe("`rules` blocks reach only the files each rule is written for", () => {
|
|
137
|
+
it.each(["paper/source", "paper/section-word", "review/frontmatter"])(
|
|
138
|
+
"🔴 %s over papers/** runs, instead of crashing inside ESLint",
|
|
139
|
+
async (id) => {
|
|
140
|
+
const root = project({
|
|
141
|
+
"paperlint.json": JSON.stringify({
|
|
142
|
+
rules: [{ files: ["papers/**"], rules: { [id]: "warn" } }],
|
|
143
|
+
}),
|
|
144
|
+
});
|
|
145
|
+
const r = await lint(root);
|
|
146
|
+
expect(r.err).not.toMatch(/Could not find/);
|
|
147
|
+
expect(r.code).toBe(0);
|
|
148
|
+
},
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
it("the same, from a paper's own paperlint.json", async () => {
|
|
152
|
+
const root = project({
|
|
153
|
+
"papers/a/paperlint.json": JSON.stringify({
|
|
154
|
+
rules: { "paper/source": "warn", "paper/section-word": "off" },
|
|
155
|
+
}),
|
|
156
|
+
});
|
|
157
|
+
const r = await lint(root);
|
|
158
|
+
expect(r.err).not.toMatch(/Could not find/);
|
|
159
|
+
expect(r.code).toBe(0);
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
describe("`rules` blocks — where the rule lands", () => {
|
|
164
|
+
it("on its own files and on no other", async () => {
|
|
165
|
+
const root = project();
|
|
166
|
+
const eslint = new ESLint({
|
|
167
|
+
cwd: root,
|
|
168
|
+
overrideConfigFile: true,
|
|
169
|
+
overrideConfig: buildConfig(
|
|
170
|
+
{
|
|
171
|
+
rules: [
|
|
172
|
+
{
|
|
173
|
+
basePath: root,
|
|
174
|
+
files: ["papers/**"],
|
|
175
|
+
rules: { "paper/source": "warn", "paper/section-word": "off" },
|
|
176
|
+
},
|
|
177
|
+
],
|
|
178
|
+
},
|
|
179
|
+
null,
|
|
180
|
+
) as Linter.Config[],
|
|
181
|
+
});
|
|
182
|
+
const on = async (file: string) =>
|
|
183
|
+
(
|
|
184
|
+
(await eslint.calculateConfigForFile(join(root, file))) as {
|
|
185
|
+
rules: Record<string, unknown>;
|
|
186
|
+
}
|
|
187
|
+
).rules;
|
|
188
|
+
const status = await on("papers/a/PIPELINE-STATUS.md");
|
|
189
|
+
expect(status["paper/source"]).toEqual([1]);
|
|
190
|
+
expect(status["paper/section-word"]).toBeUndefined();
|
|
191
|
+
const draft = await on("papers/a/paper.md");
|
|
192
|
+
expect(draft["paper/section-word"]).toEqual([0]);
|
|
193
|
+
expect(draft["paper/source"]).toBeUndefined();
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
describe("paper-files — the pieces", () => {
|
|
198
|
+
const own = [
|
|
199
|
+
{ plugins: { g: { rules: { any: {} } } } },
|
|
200
|
+
{ files: ["**/a.md"], plugins: { p: { rules: { one: {} } } } },
|
|
201
|
+
{ files: ["**/b.md", "**/c.md"], plugins: { p: { rules: { two: {} } } } },
|
|
202
|
+
{ files: ["**/c.md"], plugins: { p: { rules: { two: {} } } } },
|
|
203
|
+
];
|
|
204
|
+
|
|
205
|
+
it("owned patterns are every own block's files, once", () => {
|
|
206
|
+
expect(ownedPatterns(own)).toEqual(["**/a.md", "**/b.md", "**/c.md"]);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it("a rule's owner is where its plugin is registered; a global plugin owns every file", () => {
|
|
210
|
+
const owners = ruleOwners(own);
|
|
211
|
+
expect(owners.get("g/any")).toBeNull();
|
|
212
|
+
expect(owners.get("p/one")).toEqual(["**/a.md"]);
|
|
213
|
+
expect(owners.get("p/two")).toEqual(["**/b.md", "**/c.md"]);
|
|
214
|
+
});
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
describe("paper-files — narrowing a block", () => {
|
|
218
|
+
const own = [
|
|
219
|
+
{ plugins: { g: { rules: { any: {} } } } },
|
|
220
|
+
{ files: ["**/a.md"], plugins: { p: { rules: { one: {} } } } },
|
|
221
|
+
{ files: ["**/b.md", "**/c.md"], plugins: { p: { rules: { two: {} } } } },
|
|
222
|
+
];
|
|
223
|
+
|
|
224
|
+
it("a block is split per owner, its files ANDed with the owner's", () => {
|
|
225
|
+
const blocks = narrowToOwners(
|
|
226
|
+
{
|
|
227
|
+
basePath: "/x",
|
|
228
|
+
files: ["papers/**"],
|
|
229
|
+
ignores: ["papers/old/**"],
|
|
230
|
+
rules: { "p/one": "warn", "p/two": "error", "g/any": "off" },
|
|
231
|
+
},
|
|
232
|
+
ruleOwners(own),
|
|
233
|
+
);
|
|
234
|
+
expect(blocks).toEqual([
|
|
235
|
+
{
|
|
236
|
+
basePath: "/x",
|
|
237
|
+
files: [["papers/**", "**/a.md"]],
|
|
238
|
+
ignores: ["papers/old/**"],
|
|
239
|
+
rules: { "p/one": "warn" },
|
|
240
|
+
},
|
|
241
|
+
{
|
|
242
|
+
basePath: "/x",
|
|
243
|
+
files: [
|
|
244
|
+
["papers/**", "**/b.md"],
|
|
245
|
+
["papers/**", "**/c.md"],
|
|
246
|
+
],
|
|
247
|
+
ignores: ["papers/old/**"],
|
|
248
|
+
rules: { "p/two": "error" },
|
|
249
|
+
},
|
|
250
|
+
{
|
|
251
|
+
basePath: "/x",
|
|
252
|
+
files: ["papers/**"],
|
|
253
|
+
ignores: ["papers/old/**"],
|
|
254
|
+
rules: { "g/any": "off" },
|
|
255
|
+
},
|
|
256
|
+
]);
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
it("without `files`, the owner's patterns are the block's files", () => {
|
|
260
|
+
expect(
|
|
261
|
+
narrowToOwners(
|
|
262
|
+
{ basePath: "/x", rules: { "p/one": "warn" } },
|
|
263
|
+
ruleOwners(own),
|
|
264
|
+
),
|
|
265
|
+
).toEqual([
|
|
266
|
+
{ basePath: "/x", files: ["**/a.md"], rules: { "p/one": "warn" } },
|
|
267
|
+
]);
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it("a rule registered nowhere in this config reaches nothing — no block, no crash", () => {
|
|
271
|
+
expect(
|
|
272
|
+
narrowToOwners(
|
|
273
|
+
{ basePath: "/x", rules: { "tex/x": "warn" } },
|
|
274
|
+
ruleOwners(own),
|
|
275
|
+
),
|
|
276
|
+
).toEqual([]);
|
|
277
|
+
});
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
describe("paper-files — the scope", () => {
|
|
281
|
+
it("ignores everything, then un-ignores directories and owned files, then re-ignores ESLint's defaults", () => {
|
|
282
|
+
expect(scopeToOwned(["**/a.md"]).ignores).toEqual([
|
|
283
|
+
"**/*",
|
|
284
|
+
"!**/*/",
|
|
285
|
+
"!**/a.md",
|
|
286
|
+
"**/node_modules/",
|
|
287
|
+
"**/.git/",
|
|
288
|
+
]);
|
|
289
|
+
});
|
|
290
|
+
});
|