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,209 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The references a build checked, end to end without a network: the `references` build step
|
|
3
|
+
* records what a (fake) checker answers, and the offline rules judge that record.
|
|
4
|
+
*
|
|
5
|
+
* a pass is recorded, with the hash of the bibliography checked
|
|
6
|
+
* one mismatched entry → paper/author-list on THAT entry's line
|
|
7
|
+
* the bibliography edited after the build → paper/refs-fresh, and the per-entry rules go quiet
|
|
8
|
+
* offline → the step still succeeds (the build is not failed) and paper/refs-checked warns
|
|
9
|
+
*/
|
|
10
|
+
import { describe, expect, it } from "vitest";
|
|
11
|
+
import { ESLint } from "eslint";
|
|
12
|
+
import { join } from "node:path";
|
|
13
|
+
import { memoryFiles } from "./adapters/memory/index.ts";
|
|
14
|
+
import type { AbsolutePath } from "./domain/paths.ts";
|
|
15
|
+
import type {
|
|
16
|
+
CheckReferences,
|
|
17
|
+
EntryVerdict,
|
|
18
|
+
} from "./ports/check-references.ts";
|
|
19
|
+
import { referencesStep } from "./build.ts";
|
|
20
|
+
import {
|
|
21
|
+
bibHash,
|
|
22
|
+
bibliographyOf,
|
|
23
|
+
readReferences,
|
|
24
|
+
referencesPath,
|
|
25
|
+
} from "./references.ts";
|
|
26
|
+
import { referenceRules, REFERENCE_RULE_LEVELS } from "./reference-rules.ts";
|
|
27
|
+
// @ts-expect-error — an ESLint language in .mjs, it has no types
|
|
28
|
+
import { texLanguage } from "../eslint-rules/latex-language.mjs";
|
|
29
|
+
|
|
30
|
+
const PAPER = "/work/papers/p";
|
|
31
|
+
const TEX = (entries: string) =>
|
|
32
|
+
[
|
|
33
|
+
"\\documentclass{acmart}",
|
|
34
|
+
"\\begin{filecontents*}{refs.bib}",
|
|
35
|
+
entries,
|
|
36
|
+
"\\end{filecontents*}",
|
|
37
|
+
"\\begin{document}",
|
|
38
|
+
"x",
|
|
39
|
+
"\\end{document}",
|
|
40
|
+
"",
|
|
41
|
+
].join("\n");
|
|
42
|
+
const ENTRIES =
|
|
43
|
+
"@inproceedings{schick2023,\n title = {Toolformer},\n author = {Schick, Timo},\n booktitle = {NeurIPS}\n}\n@misc{other,\n title = {Other},\n url = {https://x.org}\n}";
|
|
44
|
+
|
|
45
|
+
const verdict = (
|
|
46
|
+
key: string,
|
|
47
|
+
authors: EntryVerdict["authors"] = "match",
|
|
48
|
+
exists: EntryVerdict["exists"] = "true",
|
|
49
|
+
): EntryVerdict => ({
|
|
50
|
+
key,
|
|
51
|
+
exists,
|
|
52
|
+
authors,
|
|
53
|
+
...(authors === "mismatch" ? { why: "missing hambro" } : {}),
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
const checker =
|
|
57
|
+
(entries: readonly EntryVerdict[]): CheckReferences =>
|
|
58
|
+
async () => ({ kind: "checked", entries });
|
|
59
|
+
const offline: CheckReferences = async () => ({
|
|
60
|
+
kind: "not-checked",
|
|
61
|
+
why: "the citation services cannot be reached (fetch failed)",
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
/** Run the build step on a paper held in memory; returns the files and the step's outcome. */
|
|
65
|
+
async function build(tex: string, check: CheckReferences) {
|
|
66
|
+
const files = memoryFiles({ [`${PAPER}/paper.tex`]: tex });
|
|
67
|
+
const out = await referencesStep.run({
|
|
68
|
+
paperDir: PAPER,
|
|
69
|
+
env: {},
|
|
70
|
+
run: (() => ({})) as never,
|
|
71
|
+
readPdf: (() => null) as never,
|
|
72
|
+
measure: (() => null) as never,
|
|
73
|
+
files,
|
|
74
|
+
checkReferences: check,
|
|
75
|
+
});
|
|
76
|
+
return { files, out };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* The rules as an ESLint plugin. Their context type is the narrow one they read (a custom
|
|
81
|
+
* language's SourceCode), which ESLint's JS-centred plugin type does not describe.
|
|
82
|
+
*/
|
|
83
|
+
const asPlugin = (rules: object): ESLint.Plugin => ({ rules }) as ESLint.Plugin;
|
|
84
|
+
|
|
85
|
+
/** Lint `tex` as the paper's paper.tex against what `files` recorded. */
|
|
86
|
+
async function lint(files: ReturnType<typeof memoryFiles>, tex: string) {
|
|
87
|
+
files.writeAtomic(
|
|
88
|
+
`${PAPER}/paper.tex` as AbsolutePath,
|
|
89
|
+
new TextEncoder().encode(tex),
|
|
90
|
+
);
|
|
91
|
+
const eslint = new ESLint({
|
|
92
|
+
cwd: "/work",
|
|
93
|
+
overrideConfigFile: true,
|
|
94
|
+
overrideConfig: [
|
|
95
|
+
{
|
|
96
|
+
files: ["**/paper.tex"],
|
|
97
|
+
plugins: {
|
|
98
|
+
tex: { languages: { latex: texLanguage } },
|
|
99
|
+
paper: asPlugin(referenceRules({ files })),
|
|
100
|
+
},
|
|
101
|
+
language: "tex/latex",
|
|
102
|
+
rules: REFERENCE_RULE_LEVELS,
|
|
103
|
+
},
|
|
104
|
+
],
|
|
105
|
+
});
|
|
106
|
+
const [res] = await eslint.lintText(tex, {
|
|
107
|
+
filePath: join(PAPER, "paper.tex"),
|
|
108
|
+
});
|
|
109
|
+
return res!.messages;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
describe("the references build step", () => {
|
|
113
|
+
it("records a pass, with the hash of the bibliography it checked", async () => {
|
|
114
|
+
const tex = TEX(ENTRIES);
|
|
115
|
+
const { files, out } = await build(
|
|
116
|
+
tex,
|
|
117
|
+
checker([verdict("schick2023"), verdict("other")]),
|
|
118
|
+
);
|
|
119
|
+
expect(out.ok).toBe(true);
|
|
120
|
+
const doc = readReferences(files, PAPER);
|
|
121
|
+
expect(doc?.status).toBe("checked");
|
|
122
|
+
expect(doc?.bib.sha256).toBe(bibHash(bibliographyOf(files, PAPER)!));
|
|
123
|
+
expect(await lint(files, tex)).toEqual([]);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it("🔴 offline: the step succeeds — the build is not failed — and lint warns once", async () => {
|
|
127
|
+
const tex = TEX(ENTRIES);
|
|
128
|
+
const { files, out } = await build(tex, offline);
|
|
129
|
+
expect(out.ok).toBe(true);
|
|
130
|
+
expect(readReferences(files, PAPER)?.status).toBe("not-checked");
|
|
131
|
+
const msgs = await lint(files, tex);
|
|
132
|
+
expect(msgs.map((m) => [m.ruleId, m.severity])).toEqual([
|
|
133
|
+
["paper/refs-checked", 1],
|
|
134
|
+
]);
|
|
135
|
+
expect(msgs[0]!.message).toMatch(/cannot be reached/);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it("a checker that throws is recorded as not checked, not as a failed build", async () => {
|
|
139
|
+
const { files, out } = await build(TEX(ENTRIES), async () => {
|
|
140
|
+
throw new Error("DBLP exploded");
|
|
141
|
+
});
|
|
142
|
+
expect(out.ok).toBe(true);
|
|
143
|
+
expect(readReferences(files, PAPER)?.why).toMatch(/DBLP exploded/);
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
describe("the reference rules", () => {
|
|
148
|
+
it("one mismatched entry → paper/author-list on that entry's own line", async () => {
|
|
149
|
+
const tex = TEX(ENTRIES);
|
|
150
|
+
const { files } = await build(
|
|
151
|
+
tex,
|
|
152
|
+
checker([verdict("schick2023", "mismatch"), verdict("other")]),
|
|
153
|
+
);
|
|
154
|
+
const msgs = await lint(files, tex);
|
|
155
|
+
expect(msgs.map((m) => m.ruleId)).toEqual(["paper/author-list"]);
|
|
156
|
+
expect(msgs[0]!.line).toBe(3);
|
|
157
|
+
expect(msgs[0]!.message).toMatch(/`schick2023`.*missing hambro/);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it("an identifier that provably fails → paper/cite-exists on that entry", async () => {
|
|
161
|
+
const tex = TEX(ENTRIES);
|
|
162
|
+
const { files } = await build(
|
|
163
|
+
tex,
|
|
164
|
+
checker([verdict("schick2023"), verdict("other", "skipped", "false")]),
|
|
165
|
+
);
|
|
166
|
+
const msgs = await lint(files, tex);
|
|
167
|
+
expect(msgs.map((m) => [m.ruleId, m.line])).toEqual([
|
|
168
|
+
["paper/cite-exists", 8],
|
|
169
|
+
]);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it("🔴 the bibliography edited after the build → refs-fresh, and the stale verdicts are not judged", async () => {
|
|
173
|
+
const { files } = await build(
|
|
174
|
+
TEX(ENTRIES),
|
|
175
|
+
checker([verdict("schick2023", "mismatch"), verdict("other")]),
|
|
176
|
+
);
|
|
177
|
+
const edited = TEX(
|
|
178
|
+
ENTRIES.replace("Schick, Timo", "Schick, Timo and Hambro, Eric"),
|
|
179
|
+
);
|
|
180
|
+
const msgs = await lint(files, edited);
|
|
181
|
+
expect(msgs.map((m) => [m.ruleId, m.severity])).toEqual([
|
|
182
|
+
["paper/refs-fresh", 2],
|
|
183
|
+
]);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it("the same bibliography, unchanged → silent", async () => {
|
|
187
|
+
const tex = TEX(ENTRIES);
|
|
188
|
+
const { files } = await build(tex, checker([verdict("schick2023")]));
|
|
189
|
+
expect(await lint(files, tex)).toEqual([]);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it("never built → one warning naming the command", async () => {
|
|
193
|
+
const files = memoryFiles({});
|
|
194
|
+
const msgs = await lint(files, TEX(ENTRIES));
|
|
195
|
+
expect(msgs.map((m) => m.ruleId)).toEqual(["paper/refs-checked"]);
|
|
196
|
+
expect(msgs[0]!.message).toMatch(/npx paperlint build/);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it("a paper with no bibliography gets nothing", async () => {
|
|
200
|
+
const files = memoryFiles({});
|
|
201
|
+
expect(
|
|
202
|
+
await lint(
|
|
203
|
+
files,
|
|
204
|
+
"\\documentclass{article}\n\\begin{document}x\\end{document}\n",
|
|
205
|
+
),
|
|
206
|
+
).toEqual([]);
|
|
207
|
+
expect(referencesPath(PAPER)).toBe(`${PAPER}/_build/references.json`);
|
|
208
|
+
});
|
|
209
|
+
});
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE REFERENCES A BUILD CHECKED — `<paper>/_build/references.json`, written by `paperlint build`
|
|
3
|
+
* and read, offline, by the lint rules in `reference-rules.ts`.
|
|
4
|
+
*
|
|
5
|
+
* The package ships two online checks: `verify-cites` (the cited work exists and its title
|
|
6
|
+
* matches) and `bib-authors` (the authors are those of the version cited, not the preprint's).
|
|
7
|
+
* Until 3.0.0 lint could only ask whether someone had run them — by finding the word
|
|
8
|
+
* "bib-authors" in a scorecard table, or a command the project configured. Now the build runs
|
|
9
|
+
* them and records the result as data, beside the facts it already writes about the PDF:
|
|
10
|
+
*
|
|
11
|
+
* { "schema": 1,
|
|
12
|
+
* "bib": { "source": "paper.tex", "sha256": "…" }, the bibliography that was checked
|
|
13
|
+
* "status": "checked" | "not-checked", "why": "…", not-checked: nothing could be asked
|
|
14
|
+
* "entries": [ { "key", "exists", "authors", "why" } ] }
|
|
15
|
+
*
|
|
16
|
+
* A sibling file, not a new field of `paper.facts.json`: those facts are about ONE PDF and are
|
|
17
|
+
* stale when it changes (`pdf_sha256`); these are about the bibliography and are stale when IT
|
|
18
|
+
* changes (`bib.sha256`). One staleness key per file, the way `pdf/fresh` already works.
|
|
19
|
+
*
|
|
20
|
+
* 🔴 THE STEP NEVER FAILS THE BUILD. A build without network still builds the PDF; the reference
|
|
21
|
+
* step records `not-checked` and the lint rule says so, as a warning. Recording "not checked" as
|
|
22
|
+
* a pass would be the counter that counts what it never looked at.
|
|
23
|
+
*/
|
|
24
|
+
import { join } from "node:path";
|
|
25
|
+
import type { AbsolutePath } from "./domain/paths.ts";
|
|
26
|
+
import { sha256Hex } from "./domain/sha256.ts";
|
|
27
|
+
import type { Files } from "./ports/files.ts";
|
|
28
|
+
import type {
|
|
29
|
+
CheckReferences,
|
|
30
|
+
EntryVerdict,
|
|
31
|
+
ReferencesCheck,
|
|
32
|
+
} from "./ports/check-references.ts";
|
|
33
|
+
// @ts-expect-error — an ESLint rule module in .mjs, it has no types
|
|
34
|
+
import { bibRange } from "../eslint-rules/paper-typography.mjs";
|
|
35
|
+
|
|
36
|
+
export const REFERENCES_SCHEMA = 1;
|
|
37
|
+
export const REFERENCES_FILE = "references.json";
|
|
38
|
+
|
|
39
|
+
/** Where a paper's reference verdicts live — beside `paper.facts.json`. */
|
|
40
|
+
export const referencesPath = (paperDir: string): string =>
|
|
41
|
+
join(paperDir, "_build", REFERENCES_FILE);
|
|
42
|
+
|
|
43
|
+
const at = (p: string): AbsolutePath => p as AbsolutePath;
|
|
44
|
+
const text = (files: Files, p: string): string | null => {
|
|
45
|
+
const b = files.readBytes(at(p));
|
|
46
|
+
return b === null ? null : new TextDecoder().decode(b);
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/** The bibliography a paper carries: inside `paper.tex` (`filecontents`), else `refs.bib`. */
|
|
50
|
+
export interface Bibliography {
|
|
51
|
+
readonly source: "paper.tex" | "refs.bib";
|
|
52
|
+
readonly text: string;
|
|
53
|
+
/** Where `text` starts in the source file — so a finding can point at an entry. */
|
|
54
|
+
readonly offset: number;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function bibliographyOf(
|
|
58
|
+
files: Files,
|
|
59
|
+
paperDir: string,
|
|
60
|
+
): Bibliography | null {
|
|
61
|
+
const tex = text(files, join(paperDir, "paper.tex"));
|
|
62
|
+
const inline = tex === null ? null : bibRange(tex);
|
|
63
|
+
if (inline)
|
|
64
|
+
return { source: "paper.tex", text: inline.body, offset: inline.bodyStart };
|
|
65
|
+
const bib = text(files, join(paperDir, "refs.bib"));
|
|
66
|
+
return bib === null ? null : { source: "refs.bib", text: bib, offset: 0 };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export const bibHash = (b: Bibliography): string =>
|
|
70
|
+
sha256Hex(new TextEncoder().encode(b.text));
|
|
71
|
+
|
|
72
|
+
/** What `_build/references.json` holds. */
|
|
73
|
+
export interface ReferencesDocument {
|
|
74
|
+
readonly schema: number;
|
|
75
|
+
readonly bib: { readonly source: string; readonly sha256: string };
|
|
76
|
+
readonly status: "checked" | "not-checked";
|
|
77
|
+
readonly why?: string;
|
|
78
|
+
readonly entries: readonly EntryVerdict[];
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export const documentOf = (
|
|
82
|
+
bib: Bibliography,
|
|
83
|
+
check: ReferencesCheck,
|
|
84
|
+
): ReferencesDocument => ({
|
|
85
|
+
schema: REFERENCES_SCHEMA,
|
|
86
|
+
bib: { source: bib.source, sha256: bibHash(bib) },
|
|
87
|
+
...(check.kind === "checked"
|
|
88
|
+
? { status: "checked" as const, entries: check.entries }
|
|
89
|
+
: { status: "not-checked" as const, why: check.why, entries: [] }),
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
/** The recorded verdicts, or null when there are none or they do not parse. */
|
|
93
|
+
export function readReferences(
|
|
94
|
+
files: Files,
|
|
95
|
+
paperDir: string,
|
|
96
|
+
): ReferencesDocument | null {
|
|
97
|
+
const raw = text(files, referencesPath(paperDir));
|
|
98
|
+
if (raw === null) return null;
|
|
99
|
+
try {
|
|
100
|
+
const d = JSON.parse(raw) as ReferencesDocument;
|
|
101
|
+
return d?.schema === REFERENCES_SCHEMA && Array.isArray(d.entries)
|
|
102
|
+
? d
|
|
103
|
+
: null;
|
|
104
|
+
} catch {
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* The build step's work: check the bibliography and record the verdicts. Returns the one-line
|
|
111
|
+
* note the build prints. A checker that throws is recorded as `not-checked` — the step reports,
|
|
112
|
+
* it never refuses.
|
|
113
|
+
*/
|
|
114
|
+
export async function recordReferences(
|
|
115
|
+
files: Files,
|
|
116
|
+
paperDir: string,
|
|
117
|
+
check: CheckReferences,
|
|
118
|
+
): Promise<string> {
|
|
119
|
+
const bib = bibliographyOf(files, paperDir);
|
|
120
|
+
if (bib === null) return "no bibliography — nothing to check";
|
|
121
|
+
let result: ReferencesCheck;
|
|
122
|
+
try {
|
|
123
|
+
result = await check(bib.text);
|
|
124
|
+
} catch (e) {
|
|
125
|
+
result = { kind: "not-checked", why: (e as Error).message };
|
|
126
|
+
}
|
|
127
|
+
const doc = documentOf(bib, result);
|
|
128
|
+
files.writeAtomic(
|
|
129
|
+
at(referencesPath(paperDir)),
|
|
130
|
+
new TextEncoder().encode(`${JSON.stringify(doc, null, 2)}\n`),
|
|
131
|
+
);
|
|
132
|
+
if (doc.status === "not-checked")
|
|
133
|
+
return `references NOT checked — ${doc.why ?? "no reason given"}; lint will say so`;
|
|
134
|
+
const bad = doc.entries.filter(
|
|
135
|
+
(e) => e.exists === "false" || e.authors === "mismatch",
|
|
136
|
+
).length;
|
|
137
|
+
return `references: ${String(doc.entries.length)} checked, ${String(bad)} failing → _build/${REFERENCES_FILE}`;
|
|
138
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `rules-config.ts` — parsing the `rules` key into ESLint config blocks, and deriving which rules
|
|
3
|
-
* paperlint ships. The end-to-end path (a
|
|
3
|
+
* paperlint ships. The end-to-end path (a project's paperlint.json → `paperlint lint`) is in `cli.harness.mjs`;
|
|
4
4
|
* this pins the parser's output shape and each refusal.
|
|
5
5
|
*/
|
|
6
6
|
import assert from "node:assert/strict";
|
|
@@ -46,23 +46,39 @@ check(
|
|
|
46
46
|
JSON.stringify(r),
|
|
47
47
|
);
|
|
48
48
|
}
|
|
49
|
+
{
|
|
50
|
+
const r = parse({ "pdf/last-page-balance": "error" });
|
|
51
|
+
check(
|
|
52
|
+
"the object form is ONE block with no `files` — narrowed to each rule's own files later",
|
|
53
|
+
r.ok &&
|
|
54
|
+
JSON.stringify(r.value) ===
|
|
55
|
+
JSON.stringify([
|
|
56
|
+
{
|
|
57
|
+
basePath: "/consumer",
|
|
58
|
+
rules: { "pdf/last-page-balance": "error" },
|
|
59
|
+
},
|
|
60
|
+
]),
|
|
61
|
+
JSON.stringify(r),
|
|
62
|
+
);
|
|
63
|
+
}
|
|
49
64
|
check(
|
|
50
65
|
"both severity spellings are accepted",
|
|
51
66
|
parse([{ rules: { "paper/typography": 0, "pdf/last-page-balance": "warn" } }])
|
|
52
67
|
.ok,
|
|
53
68
|
);
|
|
54
69
|
const refusals = [
|
|
55
|
-
[
|
|
56
|
-
[
|
|
57
|
-
[[
|
|
58
|
-
[[{
|
|
70
|
+
["pdf/last-page-balance", 'S → "rules" must be { "<rule>": "<severity>" }'],
|
|
71
|
+
[{ a: 1 }, '"a" is not a rule paperlint ships'],
|
|
72
|
+
[[42], "S → rules[0] must be an object"],
|
|
73
|
+
[[{ rules: [] }], "S → rules[0].rules must be an object"],
|
|
74
|
+
[[{ files: [], rules: {} }], "S → rules[0].files must be a non-empty list"],
|
|
59
75
|
[
|
|
60
76
|
[{ files: ["x"], ignores: [1], rules: {} }],
|
|
61
|
-
"S
|
|
77
|
+
"S → rules[0].ignores must be a non-empty list",
|
|
62
78
|
],
|
|
63
79
|
[
|
|
64
80
|
[{ languageOptions: {}, rules: {} }],
|
|
65
|
-
'S
|
|
81
|
+
'S → rules[0]: unknown key "languageOptions"',
|
|
66
82
|
],
|
|
67
83
|
[
|
|
68
84
|
[{ rules: { "markdown/no-html": "error" } }],
|
|
@@ -70,7 +86,7 @@ const refusals = [
|
|
|
70
86
|
],
|
|
71
87
|
[
|
|
72
88
|
[{ rules: { "paper/typography": ["loud"] } }],
|
|
73
|
-
'S
|
|
89
|
+
'S → rules[0].rules["paper/typography"]: ["loud"] is not a severity',
|
|
74
90
|
],
|
|
75
91
|
];
|
|
76
92
|
for (const [raw, says] of refusals) {
|
package/src/rules-config.ts
CHANGED
|
@@ -44,6 +44,7 @@ export type Parsed<T> =
|
|
|
44
44
|
| { readonly ok: false; readonly error: string };
|
|
45
45
|
|
|
46
46
|
const SEVERITIES: readonly unknown[] = ["off", "warn", "error", 0, 1, 2];
|
|
47
|
+
|
|
47
48
|
const BLOCK_KEYS = new Set(["files", "ignores", "rules"]);
|
|
48
49
|
|
|
49
50
|
const bad = <T>(error: string): Parsed<T> => ({ ok: false, error });
|
|
@@ -67,7 +68,7 @@ function parseEntry(v: unknown): RuleEntry | null {
|
|
|
67
68
|
|
|
68
69
|
/**
|
|
69
70
|
* A `rules` object — rule id → severity or `[severity, ...options]` — with only the rules paperlint
|
|
70
|
-
* ships. Read from
|
|
71
|
+
* ships. Read from every `paperlint.json`, the root's and each paper's.
|
|
71
72
|
*
|
|
72
73
|
* @param where the key path to name in messages, e.g. `package.json → "paperlint".rules[0].rules`
|
|
73
74
|
*/
|
|
@@ -108,17 +109,20 @@ const globsOf = (o: Record<string, unknown>) => ({
|
|
|
108
109
|
...(o["ignores"] ? { ignores: o["ignores"] as string[] } : {}),
|
|
109
110
|
});
|
|
110
111
|
|
|
112
|
+
const isRecord = (v: unknown): v is Record<string, unknown> =>
|
|
113
|
+
typeof v === "object" && v !== null && !Array.isArray(v);
|
|
114
|
+
|
|
111
115
|
/** One element of the `rules` list. */
|
|
112
116
|
function parseBlock(
|
|
113
117
|
v: unknown,
|
|
114
118
|
at: string,
|
|
115
119
|
ctx: { shipped: ReadonlySet<string>; baseDir: string },
|
|
116
120
|
): Parsed<RuleBlock> {
|
|
117
|
-
if (
|
|
121
|
+
if (!isRecord(v))
|
|
118
122
|
return bad(
|
|
119
123
|
`${at} must be an object like { "files": [...], "rules": {...} }`,
|
|
120
124
|
);
|
|
121
|
-
const o = v
|
|
125
|
+
const o = v;
|
|
122
126
|
const extra = Object.keys(o).filter((k) => !BLOCK_KEYS.has(k));
|
|
123
127
|
if (extra.length)
|
|
124
128
|
return bad(
|
|
@@ -135,10 +139,12 @@ function parseBlock(
|
|
|
135
139
|
}
|
|
136
140
|
|
|
137
141
|
/**
|
|
138
|
-
* The `rules` key → config blocks, or one error naming the key path.
|
|
142
|
+
* The `rules` key → config blocks, or one error naming the key path. Two shapes, at every level:
|
|
143
|
+
* `{ id: severity }` is one block over every paper file under `baseDir`; a list is ESLint blocks
|
|
144
|
+
* (`files`, `ignores`, `rules`), their globs relative to `baseDir`.
|
|
139
145
|
*
|
|
140
146
|
* @param raw the value under `rules` (undefined when absent)
|
|
141
|
-
* @param where how the settings are named in messages, e.g. `
|
|
147
|
+
* @param where how the settings are named in messages, e.g. `paperlint.json`
|
|
142
148
|
* @param shipped the rule ids paperlint ships (`shippedRuleIds`)
|
|
143
149
|
* @param baseDir the directory of the file holding the settings
|
|
144
150
|
*/
|
|
@@ -149,13 +155,19 @@ export function parseRuleBlocks(
|
|
|
149
155
|
baseDir: string,
|
|
150
156
|
): Parsed<readonly RuleBlock[]> {
|
|
151
157
|
if (raw === undefined) return { ok: true, value: [] };
|
|
158
|
+
if (isRecord(raw)) {
|
|
159
|
+
const rules = parseRuleEntries(raw, `${where} → "rules"`, shipped);
|
|
160
|
+
return rules.ok
|
|
161
|
+
? { ok: true, value: [{ basePath: baseDir, rules: rules.value }] }
|
|
162
|
+
: rules;
|
|
163
|
+
}
|
|
152
164
|
if (!Array.isArray(raw))
|
|
153
165
|
return bad(
|
|
154
|
-
`${where}
|
|
166
|
+
`${where} → "rules" must be { "<rule>": "<severity>" }, or a list of blocks like ESLint's flat config: [{ "files": [...], "rules": {...} }]`,
|
|
155
167
|
);
|
|
156
168
|
const out: RuleBlock[] = [];
|
|
157
169
|
for (const [i, v] of raw.entries()) {
|
|
158
|
-
const b = parseBlock(v, `${where}
|
|
170
|
+
const b = parseBlock(v, `${where} → rules[${i}]`, { shipped, baseDir });
|
|
159
171
|
if (!b.ok) return b;
|
|
160
172
|
out.push(b.value);
|
|
161
173
|
}
|
|
@@ -72,7 +72,9 @@ try {
|
|
|
72
72
|
check(
|
|
73
73
|
"and the message names the CONSEQUENCE, not a restatement of the condition",
|
|
74
74
|
/is checked/.test(at("no-scorecard")[0].message) &&
|
|
75
|
-
/stages, sources or
|
|
75
|
+
/stages, sources or research question/.test(
|
|
76
|
+
at("no-scorecard")[0].message,
|
|
77
|
+
),
|
|
76
78
|
);
|
|
77
79
|
check(
|
|
78
80
|
"and the consequence names THIS directory by name",
|
package/src/structure.ts
CHANGED
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
import { readdirSync, existsSync } from "node:fs";
|
|
26
26
|
import { join, relative, basename, isAbsolute, sep } from "node:path";
|
|
27
27
|
import type { StructureConfig, StructureFinding } from "./types.ts";
|
|
28
|
-
import {
|
|
28
|
+
import { CONFIG_FILE } from "../lib/paper-config.mjs";
|
|
29
29
|
|
|
30
30
|
/** The requirements after the consumer's config is laid over the defaults. */
|
|
31
31
|
type Rules = Required<StructureConfig>;
|
|
@@ -38,7 +38,7 @@ type Rules = Required<StructureConfig>;
|
|
|
38
38
|
* byte-compared by the `paper/stages` rule anyway.
|
|
39
39
|
*/
|
|
40
40
|
export const STRUCTURE_DEFAULTS = {
|
|
41
|
-
markers: ["PIPELINE-STATUS.md", "paper.tex", "paper.md",
|
|
41
|
+
markers: ["PIPELINE-STATUS.md", "paper.tex", "paper.md", CONFIG_FILE],
|
|
42
42
|
require: ["PIPELINE-STATUS.md"],
|
|
43
43
|
requireOneOf: [["paper.tex", "paper.md"]],
|
|
44
44
|
ignore: [],
|
|
@@ -122,7 +122,7 @@ export function checkStructure(
|
|
|
122
122
|
*/
|
|
123
123
|
function whyMissingMatters(file: string, dirName: string): string {
|
|
124
124
|
if (file === "PIPELINE-STATUS.md")
|
|
125
|
-
return `\`paper/stages\`, \`paper/source\` and \`paper/
|
|
125
|
+
return `\`paper/stages\`, \`paper/source\` and \`paper/research-question\` read this file, so nothing \`${dirName}\` declares about its stages, sources or research question is checked`;
|
|
126
126
|
return `declared as required by your \`structure\` configuration`;
|
|
127
127
|
}
|
|
128
128
|
|
package/src/tex-requirements.ts
CHANGED
|
@@ -26,7 +26,7 @@ import { join } from "node:path";
|
|
|
26
26
|
// eslint-disable-next-line boundaries/dependencies -- legacy layer, moves behind a port in #76
|
|
27
27
|
import Ajv from "ajv";
|
|
28
28
|
import { packageVenuesDir } from "../skills/paper-pipeline/scripts/consumer.mjs";
|
|
29
|
-
import {
|
|
29
|
+
import { CONFIG_FILE } from "../lib/paper-config.mjs";
|
|
30
30
|
|
|
31
31
|
/** CTAN package name → the names that prove it is installed. */
|
|
32
32
|
export type PackageProofs = Readonly<Record<string, readonly string[]>>;
|
|
@@ -298,7 +298,7 @@ export function requirementsFor(
|
|
|
298
298
|
const base = readProfile(dir, BASE_PROFILE);
|
|
299
299
|
return preset === null
|
|
300
300
|
? {
|
|
301
|
-
source: `the base set (no venue preset in ${
|
|
301
|
+
source: `the base set (no venue preset in ${CONFIG_FILE})`,
|
|
302
302
|
tex: base,
|
|
303
303
|
}
|
|
304
304
|
: {
|
package/src/types.ts
CHANGED
|
@@ -5,29 +5,27 @@ import type { RuleBlock } from "./rules-config.ts";
|
|
|
5
5
|
* in someone's head: a typo in a config key read as "field not set", not as an error.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
/**
|
|
8
|
+
/**
|
|
9
|
+
* The project's settings — the root `paperlint.json`, optional. Every key is optional; the key list
|
|
10
|
+
* (and which keys a paper's own `paperlint.json` may also hold) is `SETTINGS_KEYS` in
|
|
11
|
+
* lib/paper-config.mjs.
|
|
12
|
+
*/
|
|
9
13
|
export interface PaperlintConfig {
|
|
10
|
-
/**
|
|
14
|
+
/** The paper director(ies), relative to the project root. Default `papers`. The field name is
|
|
11
15
|
* `PAPERS_DIR_FIELD` in lib/paper-config.mjs; code reads it through `papersDirOf()` in cli.ts. */
|
|
12
16
|
papersDir?: string | string[];
|
|
13
|
-
/** The command that prints the author list from .bib — each corpus has its own. */
|
|
14
|
-
authorListCommand?: string;
|
|
15
|
-
/** Per-paper typography debt: how many findings already exist; the number may only go down. */
|
|
16
|
-
typographyDebt?: Record<string, Record<string, number>>;
|
|
17
|
-
/** Frontmatter fields required for review notes, and the permitted values of each. */
|
|
18
|
-
docFields?: Record<string, { values: string[] }>;
|
|
19
|
-
/** Ignore review findings older than this date. */
|
|
20
|
-
reviewSince?: string;
|
|
21
|
-
/** How many findings a cold read must produce to count as a cold read. */
|
|
22
|
-
minFindings?: number;
|
|
23
|
-
/** The word with which review notes introduce the cause. */
|
|
24
|
-
causeMarker?: string;
|
|
25
17
|
/** Which files a paper directory must carry; `false` turns the check off entirely. */
|
|
26
18
|
structure?: StructureConfig | false;
|
|
27
|
-
/** REMOVED — `paperlint build` compiles the paper itself. Still typed so a leftover key can be named. */
|
|
28
|
-
buildScripts?: unknown;
|
|
29
19
|
/** ESLint config blocks appended after paperlint's own — PARSED by `parseSettings` in cli.ts. */
|
|
30
20
|
rules?: readonly RuleBlock[];
|
|
21
|
+
/** The default venue preset of every paper that names none (`src/paper-settings.ts`). */
|
|
22
|
+
extends?: string | null;
|
|
23
|
+
/** The default kind of paper. */
|
|
24
|
+
kind?: string;
|
|
25
|
+
/** The default path of the built PDF, relative to each paper. */
|
|
26
|
+
pdf?: string;
|
|
27
|
+
/** A note for humans; ignored. */
|
|
28
|
+
$comment?: unknown;
|
|
31
29
|
}
|
|
32
30
|
|
|
33
31
|
export interface StructureConfig {
|
|
@@ -47,6 +45,8 @@ export interface Args {
|
|
|
47
45
|
paths: string[];
|
|
48
46
|
config: string | null;
|
|
49
47
|
json: boolean;
|
|
48
|
+
/** `--fix`: `lint` writes every fix a rule offers, then reports what is left. */
|
|
49
|
+
fix: boolean;
|
|
50
50
|
all: boolean;
|
|
51
51
|
dryRun: boolean;
|
|
52
52
|
/** `--check`: `toolchain` reports what is missing and changes nothing. */
|
|
@@ -102,5 +102,17 @@ export interface BuildResult {
|
|
|
102
102
|
|
|
103
103
|
/** The result of reading the config: either data, or the exit code the caller exits with. */
|
|
104
104
|
export type ConfigRead =
|
|
105
|
-
| {
|
|
106
|
-
|
|
105
|
+
| {
|
|
106
|
+
opts: PaperlintConfig;
|
|
107
|
+
/** The root `paperlint.json`, or null when the project has none. */
|
|
108
|
+
configPath: string | null;
|
|
109
|
+
/** The project root: `papersDir` and the `rules` globs are relative to it. */
|
|
110
|
+
root: string;
|
|
111
|
+
code?: undefined;
|
|
112
|
+
}
|
|
113
|
+
| {
|
|
114
|
+
code: number;
|
|
115
|
+
opts?: undefined;
|
|
116
|
+
configPath?: undefined;
|
|
117
|
+
root?: undefined;
|
|
118
|
+
};
|
package/src/venue-rules.test.ts
CHANGED
|
@@ -173,7 +173,7 @@ describe("a paper that meets its venue", () => {
|
|
|
173
173
|
});
|
|
174
174
|
|
|
175
175
|
describe("a paper that names no venue", () => {
|
|
176
|
-
it("no paperlint.json
|
|
176
|
+
it("no paperlint.json: every rule is silent, even with no facts and no PDF", () => {
|
|
177
177
|
expect(lint({ venue: undefined, facts: null, pdf: null })).toEqual([]);
|
|
178
178
|
});
|
|
179
179
|
|
|
@@ -228,16 +228,6 @@ describe("pdf/profile — the declaration must resolve, or nothing is judged", (
|
|
|
228
228
|
expect(fs[0]?.message).toMatch(text);
|
|
229
229
|
});
|
|
230
230
|
|
|
231
|
-
it("🔴 a venue.json left from before 2.1.0 is not read — pdf/profile says to run `paperlint init`", () => {
|
|
232
|
-
const fs = lint({
|
|
233
|
-
facts: null,
|
|
234
|
-
pdf: null,
|
|
235
|
-
extra: { [`${PAPER}/venue.json`]: JSON.stringify(DECL) },
|
|
236
|
-
});
|
|
237
|
-
expect(ids(fs)).toEqual(["pdf/profile:legacySettings"]);
|
|
238
|
-
expect(fs[0]?.message).toMatch(/npx paperlint init/);
|
|
239
|
-
});
|
|
240
|
-
|
|
241
231
|
it("a profile that does not parse is named with the file", () => {
|
|
242
232
|
const fs = lint({
|
|
243
233
|
venue: { extends: "paperlint:mine", kind: "short" },
|
|
@@ -302,6 +292,25 @@ describe("pdf/profile — the kind", () => {
|
|
|
302
292
|
});
|
|
303
293
|
});
|
|
304
294
|
|
|
295
|
+
describe("pdf/profile — a preset with no kinds", () => {
|
|
296
|
+
it("🔴 a preset with NO kinds (acm-sigconf) and no `kind`: nothing to pick, no finding", () => {
|
|
297
|
+
const fs = lint({
|
|
298
|
+
venue: { extends: "paperlint:acm-sigconf" },
|
|
299
|
+
facts: withFacts((f) => (f.body_pages = 99)),
|
|
300
|
+
});
|
|
301
|
+
expect(ids(fs).filter((id) => id.startsWith("pdf/profile"))).toEqual([]);
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
it("…but a `kind` named against a kindless preset is still an error that says it has none", () => {
|
|
305
|
+
const fs = lint({
|
|
306
|
+
venue: { extends: "paperlint:acm-sigconf", kind: "short" },
|
|
307
|
+
});
|
|
308
|
+
const profile = fs.filter((f) => f.rule === "pdf/profile");
|
|
309
|
+
expect(ids(profile)).toEqual(["pdf/profile:kindUnknown"]);
|
|
310
|
+
expect(profile[0]?.message).toMatch(/its kinds: \(none\)/);
|
|
311
|
+
});
|
|
312
|
+
});
|
|
313
|
+
|
|
305
314
|
describe("pdf/measured — a paper that names a venue and was not measured says so (warn)", () => {
|
|
306
315
|
it("no facts file: one finding, from pdf/measured only", () => {
|
|
307
316
|
const fs = lint({ venue: DECL, facts: null, pdf: null });
|