paperlint 2.0.1 → 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/CONTRIBUTING.md +21 -0
- package/README.md +231 -180
- 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 +5 -2
- package/dist/build-engine.js.map +1 -1
- package/dist/build.d.ts +16 -1
- package/dist/build.d.ts.map +1 -1
- package/dist/build.js +40 -5
- package/dist/build.js.map +1 -1
- package/dist/cli.d.ts +37 -20
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +377 -251
- package/dist/cli.js.map +1 -1
- package/dist/doctor.d.ts.map +1 -1
- package/dist/doctor.js +37 -52
- package/dist/doctor.js.map +1 -1
- package/dist/facts-file.d.ts +33 -9
- package/dist/facts-file.d.ts.map +1 -1
- package/dist/facts-file.js +117 -17
- package/dist/facts-file.js.map +1 -1
- package/dist/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 +40 -19
- package/dist/init.d.ts.map +1 -1
- package/dist/init.js +144 -109
- 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 +2 -2
- package/dist/new-paper.d.ts.map +1 -1
- package/dist/new-paper.js +6 -2
- 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 +40 -0
- package/dist/paper-settings.d.ts.map +1 -0
- package/dist/paper-settings.js +144 -0
- package/dist/paper-settings.js.map +1 -0
- package/dist/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 +84 -0
- package/dist/presets.d.ts.map +1 -0
- package/dist/presets.js +184 -0
- package/dist/presets.js.map +1 -0
- 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 +11 -2
- package/dist/rules-config.d.ts.map +1 -1
- package/dist/rules-config.js +25 -11
- package/dist/rules-config.js.map +1 -1
- package/dist/structure.d.ts.map +1 -1
- package/dist/structure.js +3 -2
- package/dist/structure.js.map +1 -1
- package/dist/tex-requirements.d.ts +63 -7
- package/dist/tex-requirements.d.ts.map +1 -1
- package/dist/tex-requirements.js +91 -24
- package/dist/tex-requirements.js.map +1 -1
- package/dist/types.d.ts +20 -18
- package/dist/types.d.ts.map +1 -1
- package/dist/venue-rules.d.ts +109 -0
- package/dist/venue-rules.d.ts.map +1 -0
- package/dist/venue-rules.js +389 -0
- package/dist/venue-rules.js.map +1 -0
- package/docs/configuration.md +152 -77
- package/docs/e2e.md +1 -1
- package/docs/install.md +25 -40
- package/docs/optional-rules.md +26 -24
- package/docs/rules.md +181 -15
- 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 -27
- package/eslint-rules/pdf-last-page-balance.mjs +23 -5
- 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/build-e2e/acmart/paperlint.json +1 -0
- 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 -18
- package/lib/paper-config.harness.mjs +18 -35
- package/lib/paper-config.mjs +90 -81
- 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/find-venue/SKILL.md +5 -1
- package/skills/find-venue/SKILL.md.spec.ts +4 -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/PIPELINE-MAP.md +14 -4
- package/skills/paper-pipeline/SKILL.md +4 -3
- package/skills/paper-pipeline/SKILL.md.spec.ts +3 -2
- package/skills/paper-pipeline/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/extract-pdf-facts.harness.mjs +5 -5
- package/skills/render-paper/extract-pdf-facts.mjs +4 -4
- package/skills/render-paper/render-paper.harness.mjs +2 -2
- package/skills/study-accepted-papers/SKILL.md +2 -1
- package/skills/study-accepted-papers/SKILL.md.spec.ts +1 -0
- package/skills/submit-paper/SKILL.md +9 -1
- package/skills/submit-paper/SKILL.md.spec.ts +8 -0
- package/skills/submit-paper/references/venues/acm-sigconf.jsonc +86 -0
- package/skills/submit-paper/references/venues/agenticdev.jsonc +34 -102
- package/skills/submit-paper/references/venues/aisec.jsonc +21 -82
- package/skills/submit-paper/references/venues/realm.jsonc +45 -43
- package/skills/submit-paper/references/venues/tex-base.jsonc +1 -1
- package/skills/submit-paper/references/venues/venue-profile.schema.json +119 -29
- package/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 +10 -2
- package/src/build.harness.mjs +31 -22
- package/src/build.ts +65 -8
- package/src/cli.harness.mjs +151 -294
- package/src/cli.mutations.mjs +35 -44
- package/src/cli.ts +457 -288
- package/src/doctor.harness.mjs +41 -84
- package/src/doctor.mutations.mjs +9 -9
- package/src/doctor.ts +41 -61
- package/src/facts-file.test.ts +47 -7
- package/src/facts-file.ts +145 -24
- 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 +193 -159
- 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.harness.mjs +9 -6
- package/src/new-paper.test.ts +133 -0
- package/src/new-paper.ts +7 -2
- package/src/paper-files.test.ts +290 -0
- package/src/paper-files.ts +144 -0
- package/src/paper-settings-commands.test.ts +274 -0
- package/src/paper-settings.test.ts +249 -0
- package/src/paper-settings.ts +208 -0
- package/src/ports/check-references.ts +28 -0
- package/src/presets.test.ts +236 -0
- package/src/presets.ts +297 -0
- 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 +30 -13
- package/src/structure.harness.mjs +6 -4
- package/src/structure.ts +3 -2
- package/src/tex-requirements.harness.mjs +11 -17
- package/src/tex-requirements.ts +179 -29
- package/src/types.ts +30 -18
- package/src/venue-rules.test.ts +554 -0
- package/src/venue-rules.ts +571 -0
- package/templates/paper/paperlint.json +4 -0
- 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/build-e2e/acmart/venue.json +0 -1
- 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,274 @@
|
|
|
1
|
+
/**
|
|
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
|
+
*/
|
|
5
|
+
import {
|
|
6
|
+
mkdirSync,
|
|
7
|
+
mkdtempSync,
|
|
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, toolchainTex } 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
|
+
/** A project with two papers, `a` and `b`, each with paper.tex and PIPELINE-STATUS.md. */
|
|
23
|
+
function project(files: Record<string, string> = {}): string {
|
|
24
|
+
const root = realpathSync(mkdtempSync(join(tmpdir(), "paperlint-settings-")));
|
|
25
|
+
dirs.push(root);
|
|
26
|
+
const all: Record<string, string> = {
|
|
27
|
+
"package.json": JSON.stringify({ name: "c", private: true }),
|
|
28
|
+
"papers/a/paper.tex":
|
|
29
|
+
"\\documentclass{article}\n\\begin{document}x\\end{document}\n",
|
|
30
|
+
"papers/a/PIPELINE-STATUS.md": "---\nstages: []\n---\n",
|
|
31
|
+
"papers/b/paper.tex":
|
|
32
|
+
"\\documentclass{article}\n\\begin{document}x\\end{document}\n",
|
|
33
|
+
"papers/b/PIPELINE-STATUS.md": "---\nstages: []\n---\n",
|
|
34
|
+
...files,
|
|
35
|
+
};
|
|
36
|
+
for (const [p, text] of Object.entries(all)) {
|
|
37
|
+
mkdirSync(join(root, p, ".."), { recursive: true });
|
|
38
|
+
writeFileSync(join(root, p), text);
|
|
39
|
+
}
|
|
40
|
+
return root;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function lint(
|
|
44
|
+
root: string,
|
|
45
|
+
): Promise<{ code: number; out: string; err: string }> {
|
|
46
|
+
const out: string[] = [];
|
|
47
|
+
const err: string[] = [];
|
|
48
|
+
const code = await run(["lint", "--json"], {
|
|
49
|
+
cwd: root,
|
|
50
|
+
log: (s: string) => out.push(s),
|
|
51
|
+
err: (s: string) => err.push(s),
|
|
52
|
+
});
|
|
53
|
+
return { code, out: out.join("\n"), err: err.join("\n") };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const rulesIn = (stdout: string, paper: string): string[] =>
|
|
57
|
+
(JSON.parse(stdout) as { filePath: string; messages: { ruleId: string }[] }[])
|
|
58
|
+
.filter((r) => r.filePath.endsWith(join(paper, "paper.tex")))
|
|
59
|
+
.flatMap((r) => r.messages.map((m) => m.ruleId));
|
|
60
|
+
|
|
61
|
+
describe("paperlint lint — `rules` in a paper's paperlint.json", () => {
|
|
62
|
+
it("turns an optional rule on for that paper alone", async () => {
|
|
63
|
+
const root = project({
|
|
64
|
+
"papers/a/paperlint.json": JSON.stringify({
|
|
65
|
+
rules: { "pdf/last-page-balance": "error" },
|
|
66
|
+
}),
|
|
67
|
+
});
|
|
68
|
+
const r = await lint(root);
|
|
69
|
+
// The rule is on for `a` (no facts file: it says so, loudly) and for `b` it does not exist.
|
|
70
|
+
expect(rulesIn(r.out, "a")).toContain("pdf/last-page-balance");
|
|
71
|
+
expect(rulesIn(r.out, "b")).not.toContain("pdf/last-page-balance");
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("🔴 the paper's own `rules` come after the root paperlint.json's, so they win", async () => {
|
|
75
|
+
const root = project({
|
|
76
|
+
"paperlint.json": JSON.stringify({
|
|
77
|
+
rules: [
|
|
78
|
+
{
|
|
79
|
+
files: ["papers/*/**"],
|
|
80
|
+
rules: { "pdf/last-page-balance": "error" },
|
|
81
|
+
},
|
|
82
|
+
],
|
|
83
|
+
}),
|
|
84
|
+
"papers/a/paperlint.json": JSON.stringify({
|
|
85
|
+
rules: { "pdf/last-page-balance": "off" },
|
|
86
|
+
}),
|
|
87
|
+
});
|
|
88
|
+
const r = await lint(root);
|
|
89
|
+
expect(rulesIn(r.out, "a")).not.toContain("pdf/last-page-balance");
|
|
90
|
+
expect(rulesIn(r.out, "b")).toContain("pdf/last-page-balance");
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it.each([
|
|
94
|
+
[
|
|
95
|
+
"an unknown rule id",
|
|
96
|
+
{ rules: { "pdf/no-such-rule": "error" } },
|
|
97
|
+
/not a rule paperlint ships/,
|
|
98
|
+
],
|
|
99
|
+
["an unknown key", { venu: "aisec" }, /unknown key "venu"/],
|
|
100
|
+
])("refuses %s before linting, naming the file", async (_, settings, why) => {
|
|
101
|
+
const root = project({
|
|
102
|
+
"papers/a/paperlint.json": JSON.stringify(settings),
|
|
103
|
+
});
|
|
104
|
+
const r = await lint(root);
|
|
105
|
+
expect(r.code).toBe(2);
|
|
106
|
+
expect(r.err).toMatch(why);
|
|
107
|
+
expect(r.err).toMatch(/papers\/a\/paperlint\.json/);
|
|
108
|
+
});
|
|
109
|
+
});
|
|
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
|
+
|
|
166
|
+
describe("paperlint lint — the venue preset's rules", () => {
|
|
167
|
+
it("agenticdev's preset turns pdf/last-page-balance on for its paper alone", async () => {
|
|
168
|
+
const root = project({
|
|
169
|
+
"papers/a/paperlint.json": JSON.stringify({
|
|
170
|
+
extends: "paperlint:agenticdev",
|
|
171
|
+
kind: "short",
|
|
172
|
+
}),
|
|
173
|
+
});
|
|
174
|
+
const cfgOf = await import("./cli.ts").then((m) =>
|
|
175
|
+
m.paperRuleBlocks([join(root, "papers")]),
|
|
176
|
+
);
|
|
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
|
+
});
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it("🔴 an unbuilt agenticdev paper: ONE warning (pdf/measured), and no balance error", async () => {
|
|
193
|
+
const root = project({
|
|
194
|
+
"papers/a/paperlint.json": JSON.stringify({
|
|
195
|
+
extends: "paperlint:agenticdev",
|
|
196
|
+
kind: "short",
|
|
197
|
+
}),
|
|
198
|
+
});
|
|
199
|
+
const r = await lint(root);
|
|
200
|
+
expect(r.code).toBe(0);
|
|
201
|
+
expect(rulesIn(r.out, "a")).toEqual(["pdf/measured"]);
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
describe("paperlint lint — the paper over its preset, and the project's own presets", () => {
|
|
206
|
+
it("the paper's own rules win over its preset's", async () => {
|
|
207
|
+
const root = project({
|
|
208
|
+
"papers/a/paperlint.json": JSON.stringify({
|
|
209
|
+
extends: "paperlint:agenticdev",
|
|
210
|
+
rules: { "pdf/last-page-balance": "off" },
|
|
211
|
+
}),
|
|
212
|
+
});
|
|
213
|
+
const blocks = await import("./cli.ts").then((m) =>
|
|
214
|
+
m.paperRuleBlocks([join(root, "papers")]),
|
|
215
|
+
);
|
|
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({
|
|
221
|
+
"pdf/last-page-balance": "off",
|
|
222
|
+
});
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it("a project's own preset, by relative path: its rules apply, an unknown rule id is refused", async () => {
|
|
226
|
+
const root = project({
|
|
227
|
+
"venues/usenix-sec.jsonc": JSON.stringify({
|
|
228
|
+
extends: "paperlint:acm-sigconf",
|
|
229
|
+
rules: { "pdf/no-such-rule": "error" },
|
|
230
|
+
}),
|
|
231
|
+
"papers/a/paperlint.json": JSON.stringify({
|
|
232
|
+
extends: "../../venues/usenix-sec.jsonc",
|
|
233
|
+
}),
|
|
234
|
+
});
|
|
235
|
+
const r = await lint(root);
|
|
236
|
+
expect(r.code).toBe(2);
|
|
237
|
+
expect(r.err).toMatch(/usenix-sec\.jsonc.*not a rule paperlint ships/);
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
it("a typo in extends is a pdf/profile error listing the shipped presets", async () => {
|
|
241
|
+
const root = project({
|
|
242
|
+
"papers/a/paperlint.json": JSON.stringify({
|
|
243
|
+
extends: "paperlint:agenticdve",
|
|
244
|
+
}),
|
|
245
|
+
});
|
|
246
|
+
const r = await lint(root);
|
|
247
|
+
expect(r.code).toBe(1);
|
|
248
|
+
expect(r.out).toMatch(/acm-sigconf, agenticdev, aisec, realm/);
|
|
249
|
+
});
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
describe("paperlint toolchain — installs what the project's own presets need", () => {
|
|
253
|
+
it("the shipped union plus a relative preset's tex packages", () => {
|
|
254
|
+
const root = project({
|
|
255
|
+
"venues/usenix-sec.jsonc": JSON.stringify({
|
|
256
|
+
extends: "paperlint:acm-sigconf",
|
|
257
|
+
tex: { packages: { usenix: ["usenix.sty"] } },
|
|
258
|
+
}),
|
|
259
|
+
"papers/a/paperlint.json": JSON.stringify({
|
|
260
|
+
extends: "../../venues/usenix-sec.jsonc",
|
|
261
|
+
}),
|
|
262
|
+
});
|
|
263
|
+
const tex = toolchainTex(root);
|
|
264
|
+
expect(tex.packages["usenix"]).toEqual(["usenix.sty"]);
|
|
265
|
+
expect("acmart" in tex.packages).toBe(true);
|
|
266
|
+
expect("hyperref" in tex.packages).toBe(true);
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
it("without papers of its own: the shipped union alone", () => {
|
|
270
|
+
const tex = toolchainTex(project());
|
|
271
|
+
expect("usenix" in tex.packages).toBe(false);
|
|
272
|
+
expect("acmart" in tex.packages).toBe(true);
|
|
273
|
+
});
|
|
274
|
+
});
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
/**
|
|
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.
|
|
4
|
+
*/
|
|
5
|
+
import { describe, expect, it } from "vitest";
|
|
6
|
+
import { memoryFiles } from "./adapters/memory/index.ts";
|
|
7
|
+
import type { AbsolutePath } from "./domain/paths.ts";
|
|
8
|
+
import { findProjectRoot } from "../lib/paper-config.mjs";
|
|
9
|
+
import {
|
|
10
|
+
paperRules,
|
|
11
|
+
parsePaperSettings,
|
|
12
|
+
readPaperSettings,
|
|
13
|
+
} from "./paper-settings.ts";
|
|
14
|
+
|
|
15
|
+
const PAPER = "/work/papers/p";
|
|
16
|
+
const SHIPPED = new Set([
|
|
17
|
+
"pdf/last-page-balance",
|
|
18
|
+
"pdf/profile",
|
|
19
|
+
"paper/section-word",
|
|
20
|
+
]);
|
|
21
|
+
|
|
22
|
+
describe("parsePaperSettings", () => {
|
|
23
|
+
it("reads extends, kind, pdf and rules; every absent field is null", () => {
|
|
24
|
+
expect(
|
|
25
|
+
parsePaperSettings({ extends: "paperlint:aisec", kind: "research" }),
|
|
26
|
+
).toEqual({
|
|
27
|
+
ok: true,
|
|
28
|
+
value: {
|
|
29
|
+
extends: "paperlint:aisec",
|
|
30
|
+
kind: "research",
|
|
31
|
+
pdf: null,
|
|
32
|
+
rules: null,
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
expect(parsePaperSettings({})).toEqual({
|
|
36
|
+
ok: true,
|
|
37
|
+
value: { extends: null, kind: null, pdf: null, rules: null },
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("extends: null is valid and means no venue chosen yet — what `paperlint new` writes", () => {
|
|
42
|
+
expect(parsePaperSettings({ extends: null })).toEqual({
|
|
43
|
+
ok: true,
|
|
44
|
+
value: { extends: null, kind: null, pdf: null, rules: null },
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
describe("parsePaperSettings — optional keys", () => {
|
|
50
|
+
it("accepts $comment, JSON Schema's comment keyword, and ignores it", () => {
|
|
51
|
+
expect(
|
|
52
|
+
parsePaperSettings({ extends: "paperlint:aisec", $comment: "why" }).ok,
|
|
53
|
+
).toBe(true);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it.each([
|
|
57
|
+
[
|
|
58
|
+
"an unknown key (a typo is not silent)",
|
|
59
|
+
{ venu: "aisec" },
|
|
60
|
+
/unknown key "venu" — known keys: papersDir, structure, rules, extends, kind, pdf/,
|
|
61
|
+
],
|
|
62
|
+
[
|
|
63
|
+
"a comment key other than `$comment`",
|
|
64
|
+
{ extends: "paperlint:aisec", _: "note" },
|
|
65
|
+
/unknown key "_"/,
|
|
66
|
+
],
|
|
67
|
+
[
|
|
68
|
+
"an extends that is not a string",
|
|
69
|
+
{ extends: 3 },
|
|
70
|
+
/"extends" must be a non-empty string/,
|
|
71
|
+
],
|
|
72
|
+
[
|
|
73
|
+
"an empty extends",
|
|
74
|
+
{ extends: "" },
|
|
75
|
+
/"extends" must be a non-empty string/,
|
|
76
|
+
],
|
|
77
|
+
[
|
|
78
|
+
"rules that are neither an object nor a list of blocks",
|
|
79
|
+
{ rules: "pdf/profile" },
|
|
80
|
+
/"rules" must be an object/,
|
|
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
|
+
],
|
|
87
|
+
["not an object at all", ["aisec"], /must be a JSON object/],
|
|
88
|
+
])("refuses %s", (_, json, why) => {
|
|
89
|
+
const r = parsePaperSettings(json);
|
|
90
|
+
expect(r.ok).toBe(false);
|
|
91
|
+
if (!r.ok) expect(r.error).toMatch(why);
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
describe("readPaperSettings", () => {
|
|
96
|
+
it("no file: null — the paper declares nothing", () => {
|
|
97
|
+
expect(readPaperSettings(memoryFiles(), PAPER)).toEqual({
|
|
98
|
+
ok: true,
|
|
99
|
+
value: null,
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("reads paperlint.json", () => {
|
|
104
|
+
const files = memoryFiles({
|
|
105
|
+
[`${PAPER}/paperlint.json`]: '{"extends":"paperlint:aisec"}',
|
|
106
|
+
});
|
|
107
|
+
const r = readPaperSettings(files, PAPER);
|
|
108
|
+
expect(r.ok && r.value?.extends).toBe("paperlint:aisec");
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("not JSON: broken, with the parser's reason", () => {
|
|
112
|
+
const files = memoryFiles({
|
|
113
|
+
[`${PAPER}/paperlint.json`]: "{ extends: aisec",
|
|
114
|
+
});
|
|
115
|
+
const r = readPaperSettings(files, PAPER);
|
|
116
|
+
expect(r.ok).toBe(false);
|
|
117
|
+
if (!r.ok) expect(r.error.kind).toBe("broken");
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
|
|
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
|
+
});
|
|
134
|
+
|
|
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
|
+
});
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it("paper only: its own values", () => {
|
|
150
|
+
expect(
|
|
151
|
+
read({ [`${PAPER}/paperlint.json`]: '{"extends":"paperlint:aisec"}' }),
|
|
152
|
+
).toEqual({
|
|
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",
|
|
170
|
+
kind: "research",
|
|
171
|
+
pdf: null,
|
|
172
|
+
// The root's rules are the project's blocks (cli.ts), not this paper's.
|
|
173
|
+
rules: { "pdf/fonts": "off" },
|
|
174
|
+
});
|
|
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
|
+
});
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
describe("paperRules — `rules` in paperlint.json", () => {
|
|
210
|
+
const settings = (rules: Record<string, unknown> | null) => ({
|
|
211
|
+
extends: null,
|
|
212
|
+
kind: null,
|
|
213
|
+
pdf: null,
|
|
214
|
+
rules,
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it("no rules: none", () => {
|
|
218
|
+
expect(paperRules(PAPER, settings(null), SHIPPED)).toEqual({
|
|
219
|
+
ok: true,
|
|
220
|
+
value: null,
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it("known rules, parsed", () => {
|
|
225
|
+
expect(
|
|
226
|
+
paperRules(
|
|
227
|
+
PAPER,
|
|
228
|
+
settings({ "pdf/last-page-balance": "error" }),
|
|
229
|
+
SHIPPED,
|
|
230
|
+
),
|
|
231
|
+
).toEqual({ ok: true, value: { "pdf/last-page-balance": "error" } });
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it.each([
|
|
235
|
+
[
|
|
236
|
+
"an unknown rule id",
|
|
237
|
+
{ "pdf/no-such-rule": "error" },
|
|
238
|
+
/"pdf\/no-such-rule" is not a rule paperlint ships/,
|
|
239
|
+
],
|
|
240
|
+
["a bad severity", { "pdf/profile": "loud" }, /is not a severity/],
|
|
241
|
+
])("refuses %s, naming the file", (_, rules, why) => {
|
|
242
|
+
const r = paperRules(PAPER, settings(rules), SHIPPED);
|
|
243
|
+
expect(r.ok).toBe(false);
|
|
244
|
+
if (!r.ok) {
|
|
245
|
+
expect(r.error).toMatch(why);
|
|
246
|
+
expect(r.error).toMatch(/paperlint\.json/);
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
});
|