paperlint 2.0.0 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/ci.yml +4 -4
- package/CLAUDE.md +3 -3
- package/CONTRIBUTING.md +21 -0
- package/LICENSE +1 -1
- package/README.md +81 -24
- package/action.harness.mjs +10 -10
- package/action.mutations.mjs +3 -3
- package/action.yml +11 -11
- package/bin/{rpp.mjs → paperlint.mjs} +2 -2
- package/dist/adapters/banal/invocation.js +3 -3
- package/dist/adapters/banal/invocation.js.map +1 -1
- package/dist/adapters/banal/run.d.ts +1 -1
- package/dist/adapters/banal/run.js +1 -1
- package/dist/adapters/banal/run.js.map +1 -1
- package/dist/adapters/banal/settings.d.ts +4 -4
- package/dist/adapters/banal/settings.d.ts.map +1 -1
- package/dist/adapters/banal/settings.js +4 -4
- package/dist/adapters/banal/settings.js.map +1 -1
- package/dist/adapters/banal/xml.js +1 -1
- package/dist/adapters/banal/xml.js.map +1 -1
- package/dist/adapters/curl/download.io.d.ts.map +1 -1
- package/dist/adapters/curl/download.io.js +2 -2
- package/dist/adapters/curl/download.io.js.map +1 -1
- package/dist/build-engine.d.ts +1 -1
- package/dist/build-engine.d.ts.map +1 -1
- package/dist/build-engine.js +6 -3
- package/dist/build-engine.js.map +1 -1
- package/dist/build.d.ts +1 -1
- package/dist/build.d.ts.map +1 -1
- package/dist/build.js +5 -4
- package/dist/build.js.map +1 -1
- package/dist/cli.d.ts +29 -30
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +174 -98
- package/dist/cli.js.map +1 -1
- package/dist/doctor.d.ts.map +1 -1
- package/dist/doctor.js +21 -6
- package/dist/doctor.js.map +1 -1
- package/dist/engine.d.ts +2 -2
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +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 -18
- package/dist/hooks-settings.d.ts.map +1 -1
- package/dist/hooks-settings.js +70 -42
- package/dist/hooks-settings.js.map +1 -1
- package/dist/init.d.ts +13 -10
- package/dist/init.d.ts.map +1 -1
- package/dist/init.js +74 -42
- package/dist/init.js.map +1 -1
- package/dist/link-skills.js +2 -2
- 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 +17 -2
- package/dist/new-paper.js.map +1 -1
- package/dist/paper-settings.d.ts +68 -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/presets.d.ts +84 -0
- package/dist/presets.d.ts.map +1 -0
- package/dist/presets.js +186 -0
- package/dist/presets.js.map +1 -0
- package/dist/rules-config.d.ts +8 -1
- package/dist/rules-config.d.ts.map +1 -1
- package/dist/rules-config.js +13 -8
- package/dist/rules-config.js.map +1 -1
- package/dist/structure.d.ts.map +1 -1
- package/dist/structure.js +2 -1
- 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/toolchain.d.ts +2 -2
- package/dist/toolchain.d.ts.map +1 -1
- package/dist/toolchain.js +8 -8
- package/dist/toolchain.js.map +1 -1
- package/dist/types.d.ts +4 -4
- 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 +387 -0
- package/dist/venue-rules.js.map +1 -0
- package/docs/configuration.md +73 -17
- package/docs/e2e.md +3 -4
- package/docs/install.md +11 -3
- package/docs/optional-rules.md +26 -25
- package/docs/rules.md +132 -3
- package/docs/toolchain.md +5 -5
- package/eslint-rules/paper-typography.mjs +1 -1
- package/eslint-rules/paper-typography.mutations.mjs +1 -1
- package/eslint-rules/papers.mjs +6 -2
- package/eslint-rules/pdf-last-page-balance.harness.mjs +5 -3
- package/eslint-rules/pdf-last-page-balance.mjs +23 -2
- package/fixtures/build-e2e/acmart/paperlint.json +1 -0
- package/fixtures/build-e2e/guards/paper.tex +1 -1
- package/fixtures/pdf-facts/README.md +1 -1
- package/fixtures/real-markdown-paper/baseline.json +1 -1
- package/fixtures/real-markdown-paper/baseline.mjs +2 -2
- package/fixtures/toolchain-mirror/install-tl +2 -2
- package/fixtures/toolchain-mirror/stub-pdflatex +1 -1
- package/fixtures/toolchain-mirror/stub-tlmgr +1 -1
- package/hooks/hooks.harness.mjs +1 -1
- package/lib/paper-config.d.mts +7 -0
- package/lib/paper-config.harness.mjs +3 -3
- package/lib/paper-config.mjs +34 -3
- package/lib/skill-trigger-cases.harness.mjs +1 -1
- package/package.json +4 -4
- package/plugin/hooks/hooks.json +3 -3
- package/scripts/check.mjs +1 -1
- package/scripts/harness-api.frozen.json +1 -1
- package/scripts/harness-api.test.ts +2 -2
- package/scripts/layer-legacy.frozen.json +1 -1
- package/scripts/mutation-batteries-frozen.harness.mjs +1 -1
- package/scripts/mutation-batteries.frozen.json +2 -2
- package/scripts/release-config.test.ts +1 -1
- package/skills/find-venue/SKILL.md +5 -1
- package/skills/find-venue/SKILL.md.spec.ts +4 -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/description-language.eval.mjs +1 -1
- package/skills/paper-pipeline/framing-vs-vocabulary.eval.mjs +1 -1
- package/skills/paper-pipeline/grade-paper-writing-ablation.eval.mjs +2 -2
- package/skills/paper-pipeline/pipeline-firing.eval.mjs +1 -1
- package/skills/paper-pipeline/pipeline-language.eval.mjs +1 -1
- package/skills/paper-pipeline/scripts/consumer.d.mts +1 -0
- package/skills/paper-pipeline/scripts/consumer.harness.mjs +10 -4
- package/skills/paper-pipeline/scripts/consumer.mjs +8 -1
- package/skills/paper-pipeline/scripts/consumer.mutations.mjs +1 -1
- package/skills/render-paper/SKILL.md +3 -3
- package/skills/render-paper/SKILL.md.spec.ts +2 -2
- package/skills/render-paper/extract-pdf-facts.harness.mjs +6 -6
- package/skills/render-paper/extract-pdf-facts.mjs +4 -4
- 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 +120 -30
- package/src/adapters/banal/index.test.ts +1 -1
- package/src/adapters/banal/invocation.test.ts +3 -3
- package/src/adapters/banal/invocation.ts +3 -3
- package/src/adapters/banal/locate.test.ts +4 -4
- package/src/adapters/banal/run.ts +2 -2
- package/src/adapters/banal/settings.test.ts +12 -6
- package/src/adapters/banal/settings.ts +9 -5
- package/src/adapters/banal/xml.test.ts +1 -1
- package/src/adapters/banal/xml.ts +1 -1
- package/src/adapters/curl/download.io.ts +4 -2
- package/src/adapters/curl/download.test.ts +3 -1
- package/src/adapters/node/files.test.ts +1 -1
- package/src/adapters/node/process.test.ts +2 -2
- package/src/adapters/node/workspace.test.ts +4 -4
- package/src/build-engine.harness.mjs +2 -2
- package/src/build-engine.ts +11 -3
- package/src/build.harness.mjs +27 -22
- package/src/build.mutations.mjs +3 -2
- package/src/build.ts +6 -5
- package/src/cli.harness.mjs +72 -131
- package/src/cli.mutations.mjs +7 -32
- package/src/cli.ts +204 -121
- package/src/doctor.harness.mjs +9 -25
- package/src/doctor.ts +28 -5
- package/src/engine.harness.mjs +4 -2
- package/src/engine.ts +2 -2
- package/src/facts-file.test.ts +54 -7
- package/src/facts-file.ts +145 -24
- package/src/hooks-settings.harness.mjs +24 -15
- package/src/hooks-settings.mutations.mjs +2 -4
- package/src/hooks-settings.ts +92 -49
- package/src/init.ts +88 -52
- package/src/latex-log.harness.mjs +1 -1
- package/src/link-skills.harness.mjs +3 -1
- package/src/link-skills.mutations.mjs +1 -1
- package/src/link-skills.ts +2 -2
- package/src/new-paper.harness.mjs +10 -7
- package/src/new-paper.test.ts +145 -0
- package/src/new-paper.ts +22 -2
- package/src/paper-settings-commands.test.ts +311 -0
- package/src/paper-settings.test.ts +220 -0
- package/src/paper-settings.ts +206 -0
- package/src/pdf-facts.harness.mjs +1 -1
- package/src/presets.test.ts +236 -0
- package/src/presets.ts +307 -0
- package/src/rules-config.ts +14 -9
- package/src/structure.harness.mjs +4 -4
- package/src/structure.ts +2 -1
- package/src/tex-requirements.harness.mjs +12 -18
- package/src/tex-requirements.ts +179 -29
- package/src/toolchain.harness.mjs +34 -25
- package/src/toolchain.ts +14 -8
- package/src/types.ts +4 -4
- package/src/venue-rules.test.ts +545 -0
- package/src/venue-rules.ts +573 -0
- package/templates/paper/paperlint.json +4 -0
- package/fixtures/build-e2e/acmart/venue.json +0 -1
- package/plugin/.claude-plugin/plugin.json +0 -8
|
@@ -0,0 +1,545 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The venue rules (`pdf/fresh` · `pdf/profile` · `pdf/fonts` · `pdf/geometry` · `pdf/limits` ·
|
|
3
|
+
* `pdf/body-size` · `pdf/measured`) over a paper held in memory: `paper.tex`, `paperlint.json`,
|
|
4
|
+
* `_build/paper.facts.json` and the PDF the facts describe. The venue profiles are the SHIPPED
|
|
5
|
+
* ones, read from the package's venues directory, so a profile edit that breaks a check shows here.
|
|
6
|
+
*
|
|
7
|
+
* Every case has both halves in one table: the finding a planted defect must produce, and — in the
|
|
8
|
+
* first rows — the silence a conforming paper must produce.
|
|
9
|
+
*/
|
|
10
|
+
import { readdirSync, readFileSync } from "node:fs";
|
|
11
|
+
import { join } from "node:path";
|
|
12
|
+
import { describe, expect, it } from "vitest";
|
|
13
|
+
import { memoryFiles } from "./adapters/memory/index.ts";
|
|
14
|
+
import { sha256Hex } from "./domain/sha256.ts";
|
|
15
|
+
import { packageVenuesDir } from "../skills/paper-pipeline/scripts/consumer.mjs";
|
|
16
|
+
import {
|
|
17
|
+
VENUE_RULE_LEVELS,
|
|
18
|
+
venueRules,
|
|
19
|
+
type VenueRuleModule,
|
|
20
|
+
} from "./venue-rules.ts";
|
|
21
|
+
import { buildConfig, OPTIONAL_RULES, SHIPPED_RULES } from "./cli.ts";
|
|
22
|
+
|
|
23
|
+
const VENUES = packageVenuesDir();
|
|
24
|
+
const PAPER = "/work/papers/p";
|
|
25
|
+
const PDF_BYTES = new TextEncoder().encode(
|
|
26
|
+
"%PDF-1.5 a stand-in for the built PDF",
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
/** Every shipped profile and the schema, at their real paths, so the rules read them through Files. */
|
|
30
|
+
const shipped = Object.fromEntries(
|
|
31
|
+
readdirSync(VENUES)
|
|
32
|
+
.filter((f) => f.endsWith(".jsonc") || f.endsWith(".json"))
|
|
33
|
+
.map((f) => [join(VENUES, f), readFileSync(join(VENUES, f))]),
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
type Json = Record<string, unknown>;
|
|
37
|
+
|
|
38
|
+
/** The facts of a short agenticdev paper that meets every number in its profile. */
|
|
39
|
+
function goodFacts(): Json {
|
|
40
|
+
return {
|
|
41
|
+
schema: 2,
|
|
42
|
+
pdf: "paper.pdf",
|
|
43
|
+
pdf_sha256: sha256Hex(PDF_BYTES),
|
|
44
|
+
venue: "agenticdev",
|
|
45
|
+
kind: "short",
|
|
46
|
+
npages: 7,
|
|
47
|
+
fonts_source: "pdfjs-drawn",
|
|
48
|
+
fonts: [
|
|
49
|
+
{
|
|
50
|
+
name: "LinLibertineT",
|
|
51
|
+
type: "Type 1",
|
|
52
|
+
embedded: true,
|
|
53
|
+
program: "Type1",
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
name: "LinBiolinumTB",
|
|
57
|
+
type: "Type 1",
|
|
58
|
+
embedded: true,
|
|
59
|
+
program: "Type1",
|
|
60
|
+
},
|
|
61
|
+
],
|
|
62
|
+
last_page: { kind: "stub", words: 12 },
|
|
63
|
+
last_page_cols_pt: null,
|
|
64
|
+
geometry_source: "banal",
|
|
65
|
+
page_w_in: 8.5,
|
|
66
|
+
page_h_in: 11,
|
|
67
|
+
columns: 2,
|
|
68
|
+
body_pt: 9.3,
|
|
69
|
+
ref_pt: 7.3,
|
|
70
|
+
body_pages: 5,
|
|
71
|
+
ref_pages: 2,
|
|
72
|
+
appendix_pages: 0,
|
|
73
|
+
pages_by_type: { body: 5, bib: 2 },
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const TEX =
|
|
78
|
+
"% a comment\n\\documentclass[sigconf]{acmart}\n\\begin{document}x\\end{document}\n";
|
|
79
|
+
|
|
80
|
+
interface Paper {
|
|
81
|
+
readonly venue?: unknown; // the paperlint.json object, or a raw string, or absent
|
|
82
|
+
readonly facts?: Json | string | null; // null: no facts file
|
|
83
|
+
readonly pdf?: Uint8Array | null; // null: no PDF on disk
|
|
84
|
+
readonly extra?: Readonly<Record<string, string>>;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
interface Finding {
|
|
88
|
+
readonly rule: string;
|
|
89
|
+
readonly messageId: string;
|
|
90
|
+
readonly message: string;
|
|
91
|
+
readonly line: number;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const text = (v: unknown): string =>
|
|
95
|
+
typeof v === "string" ? v : JSON.stringify(v);
|
|
96
|
+
|
|
97
|
+
/** The paper's files, as `Paper` describes them; an absent entry is an absent file. */
|
|
98
|
+
function paperFiles(p: Paper): Record<string, string | Uint8Array> {
|
|
99
|
+
const out: Record<string, string | Uint8Array> = {
|
|
100
|
+
...shipped,
|
|
101
|
+
[`${PAPER}/paper.tex`]: TEX,
|
|
102
|
+
...p.extra,
|
|
103
|
+
};
|
|
104
|
+
if (p.venue !== undefined) out[`${PAPER}/paperlint.json`] = text(p.venue);
|
|
105
|
+
if (p.facts !== null)
|
|
106
|
+
out[`${PAPER}/_build/paper.facts.json`] = text(p.facts ?? goodFacts());
|
|
107
|
+
if (p.pdf !== null) out[`${PAPER}/paper.pdf`] = p.pdf ?? PDF_BYTES;
|
|
108
|
+
return out;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Run every venue rule over one paper, the way ESLint would: `create` on paper.tex, then `root`. */
|
|
112
|
+
function lint(
|
|
113
|
+
p: Paper,
|
|
114
|
+
filename = `${PAPER}/paper.tex`,
|
|
115
|
+
options: Json = {},
|
|
116
|
+
): Finding[] {
|
|
117
|
+
const files = memoryFiles(paperFiles(p));
|
|
118
|
+
const rules = venueRules({ files, venuesDir: VENUES });
|
|
119
|
+
const out: Finding[] = [];
|
|
120
|
+
for (const [name, rule] of Object.entries(rules) as [
|
|
121
|
+
string,
|
|
122
|
+
VenueRuleModule,
|
|
123
|
+
][]) {
|
|
124
|
+
const visitor = rule.create({
|
|
125
|
+
filename,
|
|
126
|
+
sourceCode: { text: TEX },
|
|
127
|
+
options:
|
|
128
|
+
name === "geometry" && Object.keys(options).length ? [options] : [],
|
|
129
|
+
report: (d) =>
|
|
130
|
+
out.push({
|
|
131
|
+
rule: `pdf/${name}`,
|
|
132
|
+
messageId: d.messageId,
|
|
133
|
+
message: Object.entries(d.data ?? {}).reduce(
|
|
134
|
+
(m, [k, v]) => m.replaceAll(`{{${k}}}`, String(v)),
|
|
135
|
+
rule.meta.messages[d.messageId] ?? `(no message ${d.messageId})`,
|
|
136
|
+
),
|
|
137
|
+
line: d.loc.start.line,
|
|
138
|
+
}),
|
|
139
|
+
});
|
|
140
|
+
visitor.root?.();
|
|
141
|
+
}
|
|
142
|
+
return out;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const DECL = { extends: "paperlint:agenticdev", kind: "short" };
|
|
146
|
+
const ids = (fs: readonly Finding[]) =>
|
|
147
|
+
fs.map((f) => `${f.rule}:${f.messageId}`).sort();
|
|
148
|
+
const withFacts = (patch: (f: Json) => void): Json => {
|
|
149
|
+
const f = goodFacts();
|
|
150
|
+
patch(f);
|
|
151
|
+
return f;
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
describe("a paper that meets its venue", () => {
|
|
155
|
+
it("is silent — every rule, on the shipped agenticdev profile", () => {
|
|
156
|
+
expect(lint({ venue: DECL })).toEqual([]);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it("is silent on aisec, a second shipped ACM profile", () => {
|
|
160
|
+
expect(
|
|
161
|
+
lint({ venue: { extends: "paperlint:aisec", kind: "research" } }),
|
|
162
|
+
).toEqual([]);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it("takes the venue from paperlint.json, not from the facts: facts measured with no venue still judge", () => {
|
|
166
|
+
expect(
|
|
167
|
+
lint({
|
|
168
|
+
venue: DECL,
|
|
169
|
+
facts: withFacts((f) => ((f.venue = null), (f.kind = null))),
|
|
170
|
+
}),
|
|
171
|
+
).toEqual([]);
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
describe("a paper that names no venue", () => {
|
|
176
|
+
it("no paperlint.json (a paper from before 2.1.0): every rule is silent, even with no facts and no PDF", () => {
|
|
177
|
+
expect(lint({ venue: undefined, facts: null, pdf: null })).toEqual([]);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it.each([
|
|
181
|
+
["no extends", { kind: "short" }],
|
|
182
|
+
["extends: null — what `paperlint new` writes", { extends: null }],
|
|
183
|
+
])(
|
|
184
|
+
"a paperlint.json with %s: ONE warning from pdf/measured, whatever else is missing",
|
|
185
|
+
(_, venue) => {
|
|
186
|
+
const fs = lint({ venue, facts: null, pdf: null });
|
|
187
|
+
expect(ids(fs)).toEqual(["pdf/measured:noPreset"]);
|
|
188
|
+
expect(fs[0]?.message).toMatch(/set "extends" in .*paperlint\.json/);
|
|
189
|
+
},
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
it("acts on paper.tex only — any other file gets nothing", () => {
|
|
193
|
+
expect(
|
|
194
|
+
lint({ venue: DECL, facts: null }, `${PAPER}/PIPELINE-STATUS.md`),
|
|
195
|
+
).toEqual([]);
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
describe("pdf/profile — the declaration must resolve, or nothing is judged", () => {
|
|
200
|
+
it.each([
|
|
201
|
+
[
|
|
202
|
+
"an unknown venue (a typo would silently disable every check)",
|
|
203
|
+
{ extends: "paperlint:agentic-dev", kind: "short" },
|
|
204
|
+
"preset",
|
|
205
|
+
/agenticdev, aisec, realm/,
|
|
206
|
+
],
|
|
207
|
+
[
|
|
208
|
+
"the base TeX set is not a venue",
|
|
209
|
+
{ extends: "paperlint:tex-base" },
|
|
210
|
+
"preset",
|
|
211
|
+
/agenticdev, aisec, realm/,
|
|
212
|
+
],
|
|
213
|
+
[
|
|
214
|
+
"paperlint.json that is not JSON",
|
|
215
|
+
"{ extends: agenticdev",
|
|
216
|
+
"settingsBroken",
|
|
217
|
+
/paperlint\.json/,
|
|
218
|
+
],
|
|
219
|
+
[
|
|
220
|
+
"paperlint.json with a misspelt key",
|
|
221
|
+
{ venu: "agenticdev" },
|
|
222
|
+
"settingsBroken",
|
|
223
|
+
/unknown key "venu"/,
|
|
224
|
+
],
|
|
225
|
+
])("%s", (_, venue, messageId, text) => {
|
|
226
|
+
const fs = lint({ venue, facts: null, pdf: null });
|
|
227
|
+
expect(ids(fs)).toEqual([`pdf/profile:${messageId}`]);
|
|
228
|
+
expect(fs[0]?.message).toMatch(text);
|
|
229
|
+
});
|
|
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
|
+
it("a profile that does not parse is named with the file", () => {
|
|
242
|
+
const fs = lint({
|
|
243
|
+
venue: { extends: "paperlint:mine", kind: "short" },
|
|
244
|
+
extra: {
|
|
245
|
+
[join(VENUES, "mine.jsonc")]:
|
|
246
|
+
'{ "tex": { "packages": {} }, "columns": "two" }',
|
|
247
|
+
},
|
|
248
|
+
});
|
|
249
|
+
expect(ids(fs)).toEqual(["pdf/profile:preset"]);
|
|
250
|
+
expect(fs[0]?.message).toMatch(/mine\.jsonc/);
|
|
251
|
+
});
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
describe("pdf/profile — the kind", () => {
|
|
255
|
+
it.each([
|
|
256
|
+
["no kind", { extends: "paperlint:agenticdev" }, "kindMissing"],
|
|
257
|
+
[
|
|
258
|
+
"a kind the venue does not have",
|
|
259
|
+
{ extends: "paperlint:agenticdev", kind: "long" },
|
|
260
|
+
"kindUnknown",
|
|
261
|
+
],
|
|
262
|
+
])(
|
|
263
|
+
"%s: the page limit is not checked — and the rest still is",
|
|
264
|
+
(_, venue, messageId) => {
|
|
265
|
+
const fs = lint({ venue, facts: withFacts((f) => (f.body_pages = 99)) });
|
|
266
|
+
expect(ids(fs)).toEqual([`pdf/profile:${messageId}`]);
|
|
267
|
+
expect(fs[0]?.message).toMatch(/short, full, demo/);
|
|
268
|
+
// the fonts rule still runs with an unresolved kind
|
|
269
|
+
const fonts = lint({
|
|
270
|
+
venue,
|
|
271
|
+
facts: withFacts((f) => ((f.fonts as Json[])[0]!.embedded = false)),
|
|
272
|
+
});
|
|
273
|
+
expect(ids(fonts)).toEqual([
|
|
274
|
+
`pdf/fonts:notEmbedded`,
|
|
275
|
+
`pdf/profile:${messageId}`,
|
|
276
|
+
]);
|
|
277
|
+
},
|
|
278
|
+
);
|
|
279
|
+
|
|
280
|
+
it("a kind with no limits in the profile (realm's `long`) is not a finding", () => {
|
|
281
|
+
const realm = withFacts((f) => {
|
|
282
|
+
f.page_w_in = 8.27;
|
|
283
|
+
f.page_h_in = 11.69;
|
|
284
|
+
f.body_pt = 11;
|
|
285
|
+
f.ref_pt = 10;
|
|
286
|
+
f.body_pages = 30;
|
|
287
|
+
f.fonts = [
|
|
288
|
+
{
|
|
289
|
+
name: "NimbusRomNo9L-Regu",
|
|
290
|
+
type: "Type 1",
|
|
291
|
+
embedded: true,
|
|
292
|
+
program: "Type1",
|
|
293
|
+
},
|
|
294
|
+
];
|
|
295
|
+
});
|
|
296
|
+
expect(
|
|
297
|
+
lint({
|
|
298
|
+
venue: { extends: "paperlint:realm", kind: "long" },
|
|
299
|
+
facts: realm,
|
|
300
|
+
}),
|
|
301
|
+
).toEqual([]);
|
|
302
|
+
});
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
describe("pdf/measured — a paper that names a venue and was not measured says so (warn)", () => {
|
|
306
|
+
it("no facts file: one finding, from pdf/measured only", () => {
|
|
307
|
+
const fs = lint({ venue: DECL, facts: null, pdf: null });
|
|
308
|
+
expect(ids(fs)).toEqual(["pdf/measured:unbuilt"]);
|
|
309
|
+
expect(fs[0]?.message).toMatch(/paperlint build/);
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
it("facts without geometry (no banal): pdf/measured names it, the three geometry rules are silent, fonts still judge", () => {
|
|
313
|
+
const g = withFacts((f) => {
|
|
314
|
+
f.geometry_source = null;
|
|
315
|
+
for (const k of [
|
|
316
|
+
"page_w_in",
|
|
317
|
+
"page_h_in",
|
|
318
|
+
"columns",
|
|
319
|
+
"body_pt",
|
|
320
|
+
"ref_pt",
|
|
321
|
+
"body_pages",
|
|
322
|
+
"ref_pages",
|
|
323
|
+
"appendix_pages",
|
|
324
|
+
"pages_by_type",
|
|
325
|
+
])
|
|
326
|
+
f[k] = null;
|
|
327
|
+
(f.fonts as Json[])[0]!.program = "Type3";
|
|
328
|
+
});
|
|
329
|
+
expect(ids(lint({ venue: DECL, facts: g }))).toEqual([
|
|
330
|
+
"pdf/fonts:type3",
|
|
331
|
+
"pdf/measured:noGeometry",
|
|
332
|
+
]);
|
|
333
|
+
});
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
describe("pdf/fresh — facts about another build are not judged", () => {
|
|
337
|
+
it.each([
|
|
338
|
+
[
|
|
339
|
+
"a changed PDF",
|
|
340
|
+
{ pdf: new TextEncoder().encode("another build") },
|
|
341
|
+
"stale",
|
|
342
|
+
],
|
|
343
|
+
["a PDF that is gone", { pdf: null }, "pdfMissing"],
|
|
344
|
+
["a foreign schema", { facts: withFacts((f) => (f.schema = 1)) }, "schema"],
|
|
345
|
+
["facts that are not JSON", { facts: "{" }, "factsBroken"],
|
|
346
|
+
[
|
|
347
|
+
"facts missing pdf_sha256",
|
|
348
|
+
{ facts: withFacts((f) => delete f.pdf_sha256) },
|
|
349
|
+
"factsBroken",
|
|
350
|
+
],
|
|
351
|
+
[
|
|
352
|
+
"a font entry of the wrong shape",
|
|
353
|
+
{ facts: withFacts((f) => (f.fonts = [{ name: 3 }])) },
|
|
354
|
+
"factsBroken",
|
|
355
|
+
],
|
|
356
|
+
] as [string, Paper, string][])(
|
|
357
|
+
"%s: one finding, from pdf/fresh only",
|
|
358
|
+
(_, p, messageId) => {
|
|
359
|
+
// Everything else planted wrong too — none of it may be reported over stale facts.
|
|
360
|
+
const facts =
|
|
361
|
+
p.facts ?? withFacts((f) => ((f.body_pages = 99), (f.columns = 1)));
|
|
362
|
+
expect(ids(lint({ venue: DECL, ...p, facts }))).toEqual([
|
|
363
|
+
`pdf/fresh:${messageId}`,
|
|
364
|
+
]);
|
|
365
|
+
},
|
|
366
|
+
);
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
describe("pdf/fonts", () => {
|
|
370
|
+
it.each([
|
|
371
|
+
[
|
|
372
|
+
"a Type 3 font",
|
|
373
|
+
(f: Json) => ((f.fonts as Json[])[0]!.program = "Type3"),
|
|
374
|
+
["pdf/fonts:type3"],
|
|
375
|
+
],
|
|
376
|
+
[
|
|
377
|
+
"a font not embedded",
|
|
378
|
+
(f: Json) => ((f.fonts as Json[])[1]!.embedded = false),
|
|
379
|
+
["pdf/fonts:notEmbedded"],
|
|
380
|
+
],
|
|
381
|
+
[
|
|
382
|
+
"the text family missing — acmart fell back to Computer Modern",
|
|
383
|
+
(f: Json) => ((f.fonts as Json[])[0]!.name = "CMR10"),
|
|
384
|
+
["pdf/fonts:noFamily"],
|
|
385
|
+
],
|
|
386
|
+
[
|
|
387
|
+
"both families missing: one finding per family",
|
|
388
|
+
(f: Json) =>
|
|
389
|
+
(f.fonts = [
|
|
390
|
+
{ name: "CMR10", type: "Type 1", embedded: true, program: "Type1" },
|
|
391
|
+
]),
|
|
392
|
+
["pdf/fonts:noFamily", "pdf/fonts:noFamily"],
|
|
393
|
+
],
|
|
394
|
+
] as [string, (f: Json) => void, string[]][])("%s", (_, patch, want) => {
|
|
395
|
+
expect(ids(lint({ venue: DECL, facts: withFacts(patch) }))).toEqual(want);
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
it("names the family and the fonts the PDF does have", () => {
|
|
399
|
+
const [f] = lint({
|
|
400
|
+
venue: DECL,
|
|
401
|
+
facts: withFacts((x) => ((x.fonts as Json[])[0]!.name = "CMR10")),
|
|
402
|
+
});
|
|
403
|
+
expect(f?.message).toMatch(/LinLibertine/);
|
|
404
|
+
expect(f?.message).toMatch(/CMR10/);
|
|
405
|
+
});
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
describe("pdf/geometry", () => {
|
|
409
|
+
it.each([
|
|
410
|
+
[
|
|
411
|
+
"A4 instead of letter",
|
|
412
|
+
(f: Json) => ((f.page_w_in = 8.27), (f.page_h_in = 11.69)),
|
|
413
|
+
["pdf/geometry:dim", "pdf/geometry:dim"],
|
|
414
|
+
],
|
|
415
|
+
[
|
|
416
|
+
"one column instead of two",
|
|
417
|
+
(f: Json) => (f.columns = 1),
|
|
418
|
+
["pdf/geometry:columns"],
|
|
419
|
+
],
|
|
420
|
+
[
|
|
421
|
+
"a page size banal could not read",
|
|
422
|
+
(f: Json) => (f.page_w_in = null),
|
|
423
|
+
["pdf/geometry:dimMissing"],
|
|
424
|
+
],
|
|
425
|
+
[
|
|
426
|
+
"0.03 in off is inside the default tolerance",
|
|
427
|
+
(f: Json) => (f.page_w_in = 8.53),
|
|
428
|
+
[],
|
|
429
|
+
],
|
|
430
|
+
] as [string, (f: Json) => void, string[]][])("%s", (_, patch, want) => {
|
|
431
|
+
expect(ids(lint({ venue: DECL, facts: withFacts(patch) }))).toEqual(want);
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
it("dimTol widens the tolerance", () => {
|
|
435
|
+
const facts = withFacts((f) => (f.page_w_in = 8.6));
|
|
436
|
+
expect(ids(lint({ venue: DECL, facts }))).toEqual(["pdf/geometry:dim"]);
|
|
437
|
+
expect(
|
|
438
|
+
lint({ venue: DECL, facts }, `${PAPER}/paper.tex`, { dimTol: 0.2 }),
|
|
439
|
+
).toEqual([]);
|
|
440
|
+
});
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
describe("pdf/limits", () => {
|
|
444
|
+
it.each([
|
|
445
|
+
[
|
|
446
|
+
"one body page over the short-paper limit",
|
|
447
|
+
(f: Json) => (f.body_pages = 6),
|
|
448
|
+
["pdf/limits:pages"],
|
|
449
|
+
/body pages: 6, over the limit 5 for agenticdev\/short/,
|
|
450
|
+
],
|
|
451
|
+
[
|
|
452
|
+
"one reference page over",
|
|
453
|
+
(f: Json) => (f.ref_pages = 3),
|
|
454
|
+
["pdf/limits:pages"],
|
|
455
|
+
/reference pages: 3, over the limit 2/,
|
|
456
|
+
],
|
|
457
|
+
["exactly at the limit", (f: Json) => (f.body_pages = 5), [], null],
|
|
458
|
+
[
|
|
459
|
+
"a reference font below the range",
|
|
460
|
+
(f: Json) => (f.ref_pt = 6),
|
|
461
|
+
["pdf/limits:refPt"],
|
|
462
|
+
/6 pt/,
|
|
463
|
+
],
|
|
464
|
+
[
|
|
465
|
+
"6.8 pt is inside the range once the measuring drift (body_pt_tol) is allowed",
|
|
466
|
+
(f: Json) => (f.ref_pt = 6.8),
|
|
467
|
+
[],
|
|
468
|
+
null,
|
|
469
|
+
],
|
|
470
|
+
[
|
|
471
|
+
"no reference font measured (no bibliography) is not a finding",
|
|
472
|
+
(f: Json) => (f.ref_pt = null),
|
|
473
|
+
[],
|
|
474
|
+
null,
|
|
475
|
+
],
|
|
476
|
+
] as [string, (f: Json) => void, string[], RegExp | null][])(
|
|
477
|
+
"%s",
|
|
478
|
+
(_, patch, want, text) => {
|
|
479
|
+
const fs = lint({ venue: DECL, facts: withFacts(patch) });
|
|
480
|
+
expect(ids(fs)).toEqual(want);
|
|
481
|
+
if (text) expect(fs[0]?.message).toMatch(text);
|
|
482
|
+
},
|
|
483
|
+
);
|
|
484
|
+
|
|
485
|
+
it("a longer kind of the same venue passes the same page count", () => {
|
|
486
|
+
const facts = withFacts((f) => (f.body_pages = 9));
|
|
487
|
+
expect(ids(lint({ venue: DECL, facts }))).toEqual(["pdf/limits:pages"]);
|
|
488
|
+
expect(
|
|
489
|
+
lint({ venue: { extends: "paperlint:agenticdev", kind: "full" }, facts }),
|
|
490
|
+
).toEqual([]);
|
|
491
|
+
});
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
describe("pdf/body-size", () => {
|
|
495
|
+
it.each([
|
|
496
|
+
[
|
|
497
|
+
"10.2 pt against 9 ± 0.5",
|
|
498
|
+
(f: Json) => (f.body_pt = 10.2),
|
|
499
|
+
["pdf/body-size:body"],
|
|
500
|
+
],
|
|
501
|
+
["9.4 pt is inside the tolerance", (f: Json) => (f.body_pt = 9.4), []],
|
|
502
|
+
[
|
|
503
|
+
"no body size measured",
|
|
504
|
+
(f: Json) => (f.body_pt = null),
|
|
505
|
+
["pdf/body-size:bodyMissing"],
|
|
506
|
+
],
|
|
507
|
+
] as [string, (f: Json) => void, string[]][])("%s", (_, patch, want) => {
|
|
508
|
+
expect(ids(lint({ venue: DECL, facts: withFacts(patch) }))).toEqual(want);
|
|
509
|
+
});
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
describe("where a finding points", () => {
|
|
513
|
+
it("at the \\documentclass line, where the class and its options live", () => {
|
|
514
|
+
const [f] = lint({ venue: DECL, facts: withFacts((x) => (x.columns = 1)) });
|
|
515
|
+
expect(f?.line).toBe(2);
|
|
516
|
+
});
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
describe("paperlint's own config turns the venue rules on for every paper.tex", () => {
|
|
520
|
+
const texBlock = buildConfig({}, { sentinel: "tex language" }).find((b) =>
|
|
521
|
+
((b as { files?: string[] }).files ?? []).includes("**/paper.tex"),
|
|
522
|
+
) as { rules: Record<string, unknown> };
|
|
523
|
+
|
|
524
|
+
it.each(Object.entries(VENUE_RULE_LEVELS))("%s is on at %s", (id, level) => {
|
|
525
|
+
expect(texBlock.rules[id]).toBe(level);
|
|
526
|
+
expect(SHIPPED_RULES.has(id)).toBe(true);
|
|
527
|
+
expect(OPTIONAL_RULES.has(id)).toBe(false);
|
|
528
|
+
});
|
|
529
|
+
|
|
530
|
+
it("the six rules of the issue are errors except body-size, and measured is a warning", () => {
|
|
531
|
+
expect(VENUE_RULE_LEVELS).toEqual({
|
|
532
|
+
"pdf/fresh": "error",
|
|
533
|
+
"pdf/profile": "error",
|
|
534
|
+
"pdf/fonts": "error",
|
|
535
|
+
"pdf/geometry": "error",
|
|
536
|
+
"pdf/limits": "error",
|
|
537
|
+
"pdf/body-size": "warn",
|
|
538
|
+
"pdf/measured": "warn",
|
|
539
|
+
});
|
|
540
|
+
});
|
|
541
|
+
|
|
542
|
+
it("pdf/last-page-balance stays optional", () => {
|
|
543
|
+
expect(OPTIONAL_RULES.has("pdf/last-page-balance")).toBe(true);
|
|
544
|
+
});
|
|
545
|
+
});
|