paperlint 5.0.0 → 6.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/CLAUDE.md +13 -0
- package/CONTRIBUTING.md +3 -0
- package/README.md +8 -8
- package/bin/paperlint.mjs +5 -1
- package/dist/adapters/latex/index.d.ts +2 -1
- package/dist/adapters/latex/index.d.ts.map +1 -1
- package/dist/adapters/latex/index.js +6 -2
- package/dist/adapters/latex/index.js.map +1 -1
- package/dist/adapters/latex/layout.d.ts +27 -0
- package/dist/adapters/latex/layout.d.ts.map +1 -0
- package/dist/adapters/latex/layout.js +62 -0
- package/dist/adapters/latex/layout.js.map +1 -0
- package/dist/adapters/latex/nodes.d.ts.map +1 -1
- package/dist/adapters/latex/nodes.js +11 -1
- package/dist/adapters/latex/nodes.js.map +1 -1
- package/dist/adapters/latex/parse.d.ts.map +1 -1
- package/dist/adapters/latex/parse.js +5 -1
- package/dist/adapters/latex/parse.js.map +1 -1
- package/dist/adapters/latex/prose.d.ts +6 -1
- package/dist/adapters/latex/prose.d.ts.map +1 -1
- package/dist/adapters/latex/prose.js +142 -31
- package/dist/adapters/latex/prose.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +35 -18
- package/dist/cli.js.map +1 -1
- package/dist/domain/anonymity.d.ts +68 -0
- package/dist/domain/anonymity.d.ts.map +1 -0
- package/dist/domain/anonymity.js +94 -0
- package/dist/domain/anonymity.js.map +1 -0
- package/dist/domain/body-pages.d.ts +69 -0
- package/dist/domain/body-pages.d.ts.map +1 -0
- package/dist/domain/body-pages.js +141 -0
- package/dist/domain/body-pages.js.map +1 -0
- package/dist/domain/register.d.ts +96 -0
- package/dist/domain/register.d.ts.map +1 -0
- package/dist/domain/register.js +136 -0
- package/dist/domain/register.js.map +1 -0
- package/dist/domain/tex-document.d.ts +28 -0
- package/dist/domain/tex-document.d.ts.map +1 -1
- package/dist/domain/tex-document.js +8 -3
- package/dist/domain/tex-document.js.map +1 -1
- package/dist/eslint-rules/pdf-last-page-balance.d.ts +1 -1
- package/dist/eslint-rules/pdf-last-page-balance.js +2 -2
- package/dist/facts-file.d.ts +28 -2
- package/dist/facts-file.d.ts.map +1 -1
- package/dist/facts-file.js +54 -9
- package/dist/facts-file.js.map +1 -1
- package/dist/lib/paper-config.d.ts.map +1 -1
- package/dist/lib/paper-config.js +2 -1
- package/dist/lib/paper-config.js.map +1 -1
- package/dist/paper-settings.d.ts +11 -0
- package/dist/paper-settings.d.ts.map +1 -1
- package/dist/paper-settings.js +38 -4
- package/dist/paper-settings.js.map +1 -1
- package/dist/pdf-content.d.ts +40 -0
- package/dist/pdf-content.d.ts.map +1 -0
- package/dist/pdf-content.js +97 -0
- package/dist/pdf-content.js.map +1 -0
- package/dist/pdf-facts.d.ts +30 -0
- package/dist/pdf-facts.d.ts.map +1 -1
- package/dist/pdf-facts.js +37 -1
- package/dist/pdf-facts.js.map +1 -1
- package/dist/ports/latex.d.ts +8 -1
- package/dist/ports/latex.d.ts.map +1 -1
- package/dist/presets.d.ts +5 -0
- package/dist/presets.d.ts.map +1 -1
- package/dist/presets.js +8 -1
- package/dist/presets.js.map +1 -1
- package/dist/register-bands.d.ts +37 -0
- package/dist/register-bands.d.ts.map +1 -0
- package/dist/register-bands.js +196 -0
- package/dist/register-bands.js.map +1 -0
- package/dist/register.d.ts +4 -2
- package/dist/register.d.ts.map +1 -1
- package/dist/register.js +6 -45
- package/dist/register.js.map +1 -1
- package/dist/tex-requirements.d.ts +10 -0
- package/dist/tex-requirements.d.ts.map +1 -1
- package/dist/tex-requirements.js +29 -6
- package/dist/tex-requirements.js.map +1 -1
- package/dist/tex-venue-rules.d.ts +8 -2
- package/dist/tex-venue-rules.d.ts.map +1 -1
- package/dist/tex-venue-rules.js +40 -0
- package/dist/tex-venue-rules.js.map +1 -1
- package/dist/types.d.ts +2 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/venue-rules.d.ts +38 -8
- package/dist/venue-rules.d.ts.map +1 -1
- package/dist/venue-rules.js +179 -41
- package/dist/venue-rules.js.map +1 -1
- package/docs/configuration.md +18 -16
- package/docs/e2e.md +32 -11
- package/docs/rules/anonymity/identity.md +111 -0
- package/docs/rules/format/layout-override.md +61 -0
- package/docs/rules/format/page-limit.md +108 -0
- package/docs/rules/pdf/body-size.md +87 -0
- package/docs/rules/tex/claim-emphasis.md +71 -0
- package/docs/rules/tex/contrast-frames.md +100 -0
- package/docs/rules/tex/relation-markers.md +72 -0
- package/docs/rules.md +59 -50
- package/eslint-rules/pdf-last-page-balance.harness.mjs +6 -1
- package/eslint-rules/pdf-last-page-balance.ts +2 -2
- package/fixtures/build-e2e/aidc/expect.json +1 -1
- package/fixtures/build-e2e/aidc/paperlint.json +1 -1
- package/fixtures/build-e2e/anon-ack/PIPELINE-STATUS.md +3 -0
- package/fixtures/build-e2e/anon-ack/expect.json +19 -0
- package/fixtures/build-e2e/anon-ack/paper.tex +31 -0
- package/fixtures/build-e2e/anon-ack/paperlint.json +1 -0
- package/fixtures/build-e2e/anon-author/PIPELINE-STATUS.md +3 -0
- package/fixtures/build-e2e/anon-author/expect.json +19 -0
- package/fixtures/build-e2e/anon-author/paper.tex +29 -0
- package/fixtures/build-e2e/anon-author/paperlint.json +1 -0
- package/fixtures/build-e2e/anon-bib/PIPELINE-STATUS.md +3 -0
- package/fixtures/build-e2e/anon-bib/expect.json +20 -0
- package/fixtures/build-e2e/anon-bib/paper.tex +35 -0
- package/fixtures/build-e2e/anon-bib/paperlint.json +1 -0
- package/fixtures/build-e2e/anon-href/PIPELINE-STATUS.md +3 -0
- package/fixtures/build-e2e/anon-href/expect.json +19 -0
- package/fixtures/build-e2e/anon-href/paper.tex +30 -0
- package/fixtures/build-e2e/anon-href/paperlint.json +1 -0
- package/fixtures/build-e2e/anon-macro/PIPELINE-STATUS.md +3 -0
- package/fixtures/build-e2e/anon-macro/expect.json +19 -0
- package/fixtures/build-e2e/anon-macro/paper.tex +30 -0
- package/fixtures/build-e2e/anon-macro/paperlint.json +1 -0
- package/fixtures/build-e2e/anon-pdfauthor/PIPELINE-STATUS.md +3 -0
- package/fixtures/build-e2e/anon-pdfauthor/expect.json +19 -0
- package/fixtures/build-e2e/anon-pdfauthor/paper.tex +31 -0
- package/fixtures/build-e2e/anon-pdfauthor/paperlint.json +1 -0
- package/fixtures/build-e2e/unbalanced/expect.json +2 -2
- package/lib/paper-config.ts +3 -1
- package/package.json +1 -1
- package/presets/acm-sigconf.jsonc +3 -1
- package/presets/aidc.jsonc +66 -17
- package/presets/ieee-conference.jsonc +8 -0
- package/presets/venue-profile.schema.json +57 -1
- package/skills/grade-paper-writing/SKILL.md +59 -27
- package/skills/grade-paper-writing/SKILL.md.spec.ts +58 -26
- package/skills/render-paper/extract-pdf-facts.harness.ts +3 -3
- package/src/adapters/latex/index.test.ts +91 -0
- package/src/adapters/latex/index.ts +6 -2
- package/src/adapters/latex/layout.ts +106 -0
- package/src/adapters/latex/nodes.ts +11 -3
- package/src/adapters/latex/parse.ts +5 -1
- package/src/adapters/latex/prose.ts +188 -58
- package/src/build.harness.mjs +2 -2
- package/src/build.test.ts +5 -0
- package/src/cli.test.ts +13 -0
- package/src/cli.ts +50 -17
- package/src/domain/anonymity.test.ts +163 -0
- package/src/domain/anonymity.ts +129 -0
- package/src/domain/body-pages.test.ts +300 -0
- package/src/domain/body-pages.ts +223 -0
- package/src/domain/register.test.ts +153 -0
- package/src/domain/register.ts +261 -0
- package/src/domain/tex-document.ts +47 -3
- package/src/facts-file.test.ts +100 -4
- package/src/facts-file.ts +91 -9
- package/src/new-paper.test.ts +7 -1
- package/src/paper-settings.test.ts +63 -6
- package/src/paper-settings.ts +53 -5
- package/src/pdf-content.test.ts +71 -0
- package/src/pdf-content.ts +125 -0
- package/src/pdf-facts.test.ts +110 -2
- package/src/pdf-facts.ts +106 -8
- package/src/ports/latex.ts +9 -0
- package/src/presets.ts +15 -9
- package/src/register-bands.test.ts +327 -0
- package/src/register-bands.ts +291 -0
- package/src/register.ts +7 -37
- package/src/rule-cases.test.ts +96 -20
- package/src/rule-docs.test.ts +0 -2
- package/src/tex-requirements.ts +56 -8
- package/src/tex-venue-rules.test.ts +110 -5
- package/src/tex-venue-rules.ts +64 -3
- package/src/types.ts +2 -0
- package/src/venue-rules.test.ts +351 -39
- package/src/venue-rules.ts +247 -47
package/src/venue-rules.ts
CHANGED
|
@@ -5,10 +5,14 @@
|
|
|
5
5
|
* pdf/profile error the preset the paper extends resolves, and the kind it names exists
|
|
6
6
|
* pdf/fonts error every font is embedded, none is Type 3, the venue's families are present
|
|
7
7
|
* pdf/geometry error page size and column count match the preset
|
|
8
|
-
* pdf/
|
|
9
|
-
* pdf/body-size warn body font size within the preset's tolerance
|
|
8
|
+
* pdf/body-size error body and reference font sizes within the preset's ranges
|
|
10
9
|
* pdf/measured warn the venue checks ran at all (a preset is named, facts exist, geometry measured)
|
|
11
10
|
*
|
|
11
|
+
* and, in plugins named for the requirement rather than the input (paperlint#151):
|
|
12
|
+
*
|
|
13
|
+
* format/page-limit error body and reference pages within the limits of the paper's kind
|
|
14
|
+
* anonymity/identity error a blind venue: the paper declares `identity`, and the PDF says none of it
|
|
15
|
+
*
|
|
12
16
|
* ── WHAT THEY READ ───────────────────────────────────────────────────────────────
|
|
13
17
|
* Like `pdf/last-page-balance`, they run on a paper's `paper.tex` and judge the files beside it:
|
|
14
18
|
* `paperlint.json` (which preset, which kind), `_build/paper.facts.json` (what `paperlint build`
|
|
@@ -30,26 +34,32 @@
|
|
|
30
34
|
* not built / no facts pdf/measured (warn) — lint often runs before or without a build (the
|
|
31
35
|
* CI action only lints); a warning is printed and does not fail
|
|
32
36
|
* facts about another PDF pdf/fresh (error) — judging them would judge an earlier build
|
|
33
|
-
* no geometry (no banal) pdf/measured (warn); geometry
|
|
34
|
-
*
|
|
37
|
+
* no geometry (no banal) pdf/measured (warn); geometry and body-size silent, page-limit judges
|
|
38
|
+
* only a body counted before the references; fonts still judge
|
|
39
|
+
* kind does not resolve pdf/profile (error); page-limit silent; everything else still judges
|
|
35
40
|
*/
|
|
36
41
|
import { dirname, isAbsolute, join, basename, relative } from "node:path";
|
|
37
42
|
import {
|
|
38
43
|
FACTS_DIR,
|
|
39
44
|
FACTS_FILE,
|
|
45
|
+
FACTS_SCHEMA,
|
|
40
46
|
factsPath,
|
|
41
47
|
parseFactsText,
|
|
42
48
|
type FontEntry,
|
|
43
49
|
type ReadFacts,
|
|
44
50
|
} from "./facts-file.ts";
|
|
45
51
|
import type { KindLimits, VenueFormat } from "./tex-requirements.ts";
|
|
46
|
-
import { paperPreset, presetProblemText } from "./presets.ts";
|
|
52
|
+
import { paperPreset, presetProblemText, type Preset } from "./presets.ts";
|
|
53
|
+
import type { PaperSettings } from "./paper-settings.ts";
|
|
47
54
|
import type { FlatGeometry } from "./domain/geometry.ts";
|
|
48
55
|
import { callerPath } from "./caller-path.ts";
|
|
49
56
|
import { sha256Hex } from "./domain/sha256.ts";
|
|
50
57
|
import { fieldOf } from "./domain/record.ts";
|
|
51
58
|
import type { Files } from "./ports/files.ts";
|
|
52
59
|
import { CONFIG_FILE } from "#lib/paper-config";
|
|
60
|
+
import { findLeaks, type Leak, type Searchable } from "./domain/anonymity.ts";
|
|
61
|
+
import { bodyEnd, type BodyEnd } from "./domain/body-pages.ts";
|
|
62
|
+
import type { TextFacts } from "./facts-file.ts";
|
|
53
63
|
|
|
54
64
|
// ── the verdict's vocabulary ─────────────────────────────────────────────────────────
|
|
55
65
|
|
|
@@ -71,6 +81,10 @@ export interface Resolved {
|
|
|
71
81
|
readonly kind: { readonly name: string; readonly limits: KindLimits } | null;
|
|
72
82
|
/** Why the kind did not resolve — `pdf/profile` reports it — or null. */
|
|
73
83
|
readonly kindProblem: Finding | null;
|
|
84
|
+
/** Whether the venue reviews double-blind (the preset's `blind`). */
|
|
85
|
+
readonly blind: boolean;
|
|
86
|
+
/** What identifies the authors (`identity` in paperlint.json), or null when undeclared. */
|
|
87
|
+
readonly identity: readonly string[] | null;
|
|
74
88
|
}
|
|
75
89
|
|
|
76
90
|
/** Everything the rules need to know about one paper, decided once. */
|
|
@@ -125,6 +139,15 @@ function kindOf(
|
|
|
125
139
|
};
|
|
126
140
|
}
|
|
127
141
|
|
|
142
|
+
/** How the venue reviews, and who the paper says wrote it: what `anonymity/*` judges by. */
|
|
143
|
+
const reviewOf = (
|
|
144
|
+
preset: Pick<Preset, "blind">,
|
|
145
|
+
settings: Pick<PaperSettings, "identity">,
|
|
146
|
+
): Pick<Resolved, "blind" | "identity"> => ({
|
|
147
|
+
blind: preset.blind,
|
|
148
|
+
identity: settings.identity,
|
|
149
|
+
});
|
|
150
|
+
|
|
128
151
|
/** The facts about the PDF on disk, or the `pdf/fresh` finding that says why they are not. */
|
|
129
152
|
function freshFacts(
|
|
130
153
|
files: Files,
|
|
@@ -134,7 +157,7 @@ function freshFacts(
|
|
|
134
157
|
const parsed = parseFactsText(body);
|
|
135
158
|
if (!parsed.ok)
|
|
136
159
|
return parsed.error.kind === "schema"
|
|
137
|
-
? finding("schema", { got: parsed.error.got })
|
|
160
|
+
? finding("schema", { got: parsed.error.got, want: FACTS_SCHEMA })
|
|
138
161
|
: finding("factsBroken", { why: parsed.error.why });
|
|
139
162
|
const f = parsed.value;
|
|
140
163
|
const pdf = files.readBytes(
|
|
@@ -168,6 +191,7 @@ export function assessPaper(paperDir: string, deps: VenueRuleDeps): Assessment {
|
|
|
168
191
|
venue: label,
|
|
169
192
|
format: p.preset.format,
|
|
170
193
|
...kindOf(label, p.preset.format, p.settings.kind),
|
|
194
|
+
...reviewOf(p.preset, p.settings),
|
|
171
195
|
};
|
|
172
196
|
const factsText = text(deps.files, factsPath(paperDir));
|
|
173
197
|
if (factsText === null) return { kind: "unbuilt", venue };
|
|
@@ -238,22 +262,18 @@ export function judgeGeometry(
|
|
|
238
262
|
}
|
|
239
263
|
|
|
240
264
|
/**
|
|
241
|
-
* Body and reference pages against the kind's limits
|
|
242
|
-
*
|
|
243
|
-
* it is widened by `body_pt_tol` — the drift the preset itself names — or a correct paper fails.
|
|
265
|
+
* Body and reference pages, as banal classifies them, against the kind's limits. A venue that counts
|
|
266
|
+
* the body before the references is judged on its body by `judgeBodyEnd` instead.
|
|
244
267
|
*/
|
|
245
|
-
export function
|
|
246
|
-
return [...judgePages(g, resolved), ...judgeRefPt(g, resolved)];
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
function judgePages(g: FlatGeometry, resolved: Resolved): Finding[] {
|
|
268
|
+
export function judgePages(g: FlatGeometry, resolved: Resolved): Finding[] {
|
|
250
269
|
const kind = resolved.kind;
|
|
251
270
|
if (kind === null) return [];
|
|
252
271
|
const pages: [number | null, number, string][] = [
|
|
253
272
|
[kind.limits.bodyPagesMax, g.body_pages, "body pages"],
|
|
254
273
|
[kind.limits.refPagesMax, g.ref_pages, "reference pages"],
|
|
255
274
|
];
|
|
256
|
-
|
|
275
|
+
const from = resolved.format.bodyEndsAt === "references" ? 1 : 0;
|
|
276
|
+
return pages.slice(from).flatMap(([max, got, what]) =>
|
|
257
277
|
max !== null && got > max
|
|
258
278
|
? [
|
|
259
279
|
finding("pages", {
|
|
@@ -268,29 +288,122 @@ function judgePages(g: FlatGeometry, resolved: Resolved): Finding[] {
|
|
|
268
288
|
);
|
|
269
289
|
}
|
|
270
290
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
291
|
+
/**
|
|
292
|
+
* The body counted up to where it ends (`body_ends_at: references`) — the references or the
|
|
293
|
+
* appendix, whichever comes first — against the kind's limit: the page it ends on counts when body
|
|
294
|
+
* text stands above the heading there (`bodyEnd`). A limit to check and no end found, or a
|
|
295
|
+
* structural signal the text does not bear out, is said, not passed: the count has nothing to
|
|
296
|
+
* stand on.
|
|
297
|
+
*/
|
|
298
|
+
export function judgeBodyEnd(
|
|
299
|
+
text: Pick<TextFacts, "pages" | "bibAnchorPage" | "appendixAnchorPage">,
|
|
300
|
+
resolved: Resolved,
|
|
301
|
+
): readonly Finding[] {
|
|
302
|
+
const kind = resolved.kind;
|
|
303
|
+
const max = kind?.limits.bodyPagesMax ?? null;
|
|
304
|
+
if (
|
|
305
|
+
resolved.format.bodyEndsAt !== "references" ||
|
|
306
|
+
kind === null ||
|
|
307
|
+
max === null
|
|
308
|
+
)
|
|
275
309
|
return [];
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
310
|
+
const end = endOfBody(text);
|
|
311
|
+
const venue = resolved.venue;
|
|
312
|
+
switch (end.kind) {
|
|
313
|
+
case "missing":
|
|
314
|
+
return [finding("noReferences", { venue })];
|
|
315
|
+
case "disagree":
|
|
316
|
+
return [finding("unclear", { venue, why: disagreement(end) })];
|
|
317
|
+
case "found": {
|
|
318
|
+
const what = `body pages (up to the ${end.by} on page ${String(end.page)})`;
|
|
319
|
+
const got = end.bodyPages;
|
|
320
|
+
return got > max
|
|
321
|
+
? [finding("pages", { what, got, max, venue, kind: kind.name })]
|
|
322
|
+
: [];
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/** Where the body ends in what the PDF says, and where hyperref anchored the two ends. */
|
|
328
|
+
const endOfBody = (
|
|
329
|
+
t: Pick<TextFacts, "pages" | "bibAnchorPage" | "appendixAnchorPage">,
|
|
330
|
+
): BodyEnd =>
|
|
331
|
+
bodyEnd(t.pages, { bib: t.bibAnchorPage, appendix: t.appendixAnchorPage });
|
|
332
|
+
|
|
333
|
+
/** What the `unclear` message says about a structural signal the text does not bear out. */
|
|
334
|
+
function disagreement(
|
|
335
|
+
d: Extract<BodyEnd, { readonly kind: "disagree" }>,
|
|
336
|
+
): string {
|
|
337
|
+
const page = String(d.anchor);
|
|
338
|
+
if (d.what === "appendix")
|
|
339
|
+
return `hyperref anchors the appendix on page ${page}, and no line on page ${page} reads «Appendix»`;
|
|
340
|
+
return d.heading === null
|
|
341
|
+
? `the first bibliography entry is on page ${page} (hyperref's destination), and no line on page ${page} reads «References»`
|
|
342
|
+
: `the first bibliography entry is on page ${page} (hyperref's destination), and the «References» heading followed by [1] is on page ${String(d.heading)}`;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/** Everything the PDF says, as places to search: each page, each metadata field, each link. */
|
|
346
|
+
export const searchable = (t: TextFacts): readonly Searchable[] => [
|
|
347
|
+
...t.pages.map((text, i) => ({
|
|
348
|
+
place: { kind: "page" as const, page: i + 1 },
|
|
349
|
+
text,
|
|
350
|
+
})),
|
|
351
|
+
...Object.entries(t.metadata).map(([field, text]) => ({
|
|
352
|
+
place: { kind: "metadata" as const, field },
|
|
353
|
+
text,
|
|
354
|
+
})),
|
|
355
|
+
...t.links.map(({ page, uri }) => ({
|
|
356
|
+
place: { kind: "link" as const, page, uri },
|
|
357
|
+
text: uri,
|
|
358
|
+
})),
|
|
359
|
+
];
|
|
360
|
+
|
|
361
|
+
/** Where a leak is, in words. */
|
|
362
|
+
function whereOf(l: Leak): string {
|
|
363
|
+
switch (l.place.kind) {
|
|
364
|
+
case "page":
|
|
365
|
+
return `PDF, page ${String(l.place.page)}`;
|
|
366
|
+
case "metadata":
|
|
367
|
+
return `PDF metadata: ${l.place.field}`;
|
|
368
|
+
case "link":
|
|
369
|
+
return `link target on page ${String(l.place.page)} (${l.place.uri})`;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/** A blind venue's paper: `identity` declared, and none of it in the PDF. */
|
|
374
|
+
export function judgeAnonymity(
|
|
375
|
+
resolved: Resolved,
|
|
376
|
+
text: TextFacts | null,
|
|
377
|
+
): readonly Finding[] {
|
|
378
|
+
if (!resolved.blind) return [];
|
|
379
|
+
const identity = resolved.identity ?? [];
|
|
380
|
+
if (identity.length === 0)
|
|
381
|
+
return [finding("noIdentity", { venue: resolved.venue })];
|
|
382
|
+
return text === null
|
|
383
|
+
? []
|
|
384
|
+
: findLeaks(identity, searchable(text)).map((l) =>
|
|
385
|
+
finding("leak", {
|
|
386
|
+
found: l.found.replace(/\s+/gu, " "),
|
|
387
|
+
token: l.token,
|
|
388
|
+
where: whereOf(l),
|
|
283
389
|
venue: resolved.venue,
|
|
284
390
|
}),
|
|
285
|
-
|
|
286
|
-
: [];
|
|
391
|
+
);
|
|
287
392
|
}
|
|
288
393
|
|
|
289
|
-
/** The body font size against `body_pt ± body_pt_tol
|
|
394
|
+
/** The body font size against `body_pt ± body_pt_tol`, and the reference font size against its range. */
|
|
290
395
|
export function judgeBodySize(
|
|
291
396
|
g: FlatGeometry,
|
|
292
397
|
format: VenueFormat,
|
|
293
398
|
venue: string,
|
|
399
|
+
): Finding[] {
|
|
400
|
+
return [...judgeBodyPt(g, format, venue), ...judgeRefPt(g, format, venue)];
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
function judgeBodyPt(
|
|
404
|
+
g: FlatGeometry,
|
|
405
|
+
format: VenueFormat,
|
|
406
|
+
venue: string,
|
|
294
407
|
): Finding[] {
|
|
295
408
|
const want = format.bodyPt;
|
|
296
409
|
const tol = format.bodyPtTol;
|
|
@@ -301,6 +414,34 @@ export function judgeBodySize(
|
|
|
301
414
|
: [];
|
|
302
415
|
}
|
|
303
416
|
|
|
417
|
+
/**
|
|
418
|
+
* The reference font size against the profile's range. The range is about the DECLARED size and
|
|
419
|
+
* banal measures the rendered mode, so it is widened by `body_pt_tol` — the drift the preset itself
|
|
420
|
+
* names — or a correct paper fails.
|
|
421
|
+
*/
|
|
422
|
+
function judgeRefPt(
|
|
423
|
+
g: FlatGeometry,
|
|
424
|
+
format: VenueFormat,
|
|
425
|
+
venue: string,
|
|
426
|
+
): Finding[] {
|
|
427
|
+
const slop = format.bodyPtTol ?? 0;
|
|
428
|
+
if (format.refPtMin === null || format.refPtMax === null || g.ref_pt === null)
|
|
429
|
+
return [];
|
|
430
|
+
return g.ref_pt < format.refPtMin - slop || g.ref_pt > format.refPtMax + slop
|
|
431
|
+
? [
|
|
432
|
+
finding("refPt", {
|
|
433
|
+
got: g.ref_pt,
|
|
434
|
+
range:
|
|
435
|
+
format.refPtMin === format.refPtMax
|
|
436
|
+
? String(format.refPtMin)
|
|
437
|
+
: `${String(format.refPtMin)}–${String(format.refPtMax)}`,
|
|
438
|
+
slop,
|
|
439
|
+
venue,
|
|
440
|
+
}),
|
|
441
|
+
]
|
|
442
|
+
: [];
|
|
443
|
+
}
|
|
444
|
+
|
|
304
445
|
// ── the ESLint rules ────────────────────────────────────────────────────────────────
|
|
305
446
|
|
|
306
447
|
/** The slice of ESLint's rule context these rules use. */
|
|
@@ -323,7 +464,7 @@ export interface VenueRuleContext {
|
|
|
323
464
|
export interface VenueRuleModule {
|
|
324
465
|
readonly meta: {
|
|
325
466
|
readonly type: "problem" | "suggestion";
|
|
326
|
-
readonly docs: { readonly description: string };
|
|
467
|
+
readonly docs: { readonly description: string; readonly url?: string };
|
|
327
468
|
readonly schema: readonly object[];
|
|
328
469
|
readonly messages: Readonly<Record<string, string>>;
|
|
329
470
|
};
|
|
@@ -339,6 +480,10 @@ type Judge = (a: Assessment, ctx: JudgeContext) => Finding[];
|
|
|
339
480
|
|
|
340
481
|
const REBUILD = "rebuild the paper (`paperlint build`) to rewrite it";
|
|
341
482
|
|
|
483
|
+
/** Where a rule's page lives: `docs/rules/<plugin>/<name>.md` on the default branch. */
|
|
484
|
+
export const rulePageUrl = (id: string): string =>
|
|
485
|
+
`https://github.com/zernie/paperlint/blob/main/docs/rules/${id}.md`;
|
|
486
|
+
|
|
342
487
|
/** Which rule reports what: each takes the assessment and returns its own findings. */
|
|
343
488
|
const JUDGES: Readonly<Record<VenueRuleName, Judge>> = {
|
|
344
489
|
profile: (a) =>
|
|
@@ -376,10 +521,19 @@ const JUDGES: Readonly<Record<VenueRuleName, Judge>> = {
|
|
|
376
521
|
dimTolOf(options[0]),
|
|
377
522
|
)
|
|
378
523
|
: [],
|
|
379
|
-
|
|
380
|
-
a.kind === "ready"
|
|
381
|
-
?
|
|
524
|
+
"page-limit": (a) =>
|
|
525
|
+
a.kind === "ready"
|
|
526
|
+
? [
|
|
527
|
+
...judgeBodyEnd(a.facts.text, a.venue),
|
|
528
|
+
...(a.facts.geometry ? judgePages(a.facts.geometry, a.venue) : []),
|
|
529
|
+
]
|
|
382
530
|
: [],
|
|
531
|
+
anonymity: (a) =>
|
|
532
|
+
a.kind === "ready"
|
|
533
|
+
? [...judgeAnonymity(a.venue, a.facts.text)]
|
|
534
|
+
: a.kind === "unbuilt"
|
|
535
|
+
? [...judgeAnonymity(a.venue, null)]
|
|
536
|
+
: [],
|
|
383
537
|
"body-size": (a) =>
|
|
384
538
|
a.kind === "ready" && a.facts.geometry
|
|
385
539
|
? judgeBodySize(a.facts.geometry, a.venue.format, a.venue.venue)
|
|
@@ -408,7 +562,7 @@ const META: Readonly<Record<VenueRuleName, Meta>> = {
|
|
|
408
562
|
},
|
|
409
563
|
messages: {
|
|
410
564
|
factsBroken: `_build/paper.facts.json cannot be read: {{why}} — ${REBUILD}`,
|
|
411
|
-
schema: `_build/paper.facts.json has schema {{got}}; the venue rules read schema
|
|
565
|
+
schema: `_build/paper.facts.json has schema {{got}}; the venue rules read schema {{want}} — ${REBUILD}`,
|
|
412
566
|
pdfMissing:
|
|
413
567
|
"_build/paper.facts.json describes {{pdf}}, which is not on disk (a failed build removes it) — rebuild the paper",
|
|
414
568
|
stale:
|
|
@@ -477,25 +631,41 @@ const META: Readonly<Record<VenueRuleName, Meta>> = {
|
|
|
477
631
|
"{{got}} column(s), {{venue}} requires {{want}} — the wrong document class or class option",
|
|
478
632
|
},
|
|
479
633
|
},
|
|
480
|
-
|
|
634
|
+
"page-limit": {
|
|
481
635
|
docs: {
|
|
482
636
|
description:
|
|
483
|
-
"body and reference pages within the
|
|
637
|
+
"body and reference pages within the limits of the paper's kind at its venue",
|
|
638
|
+
url: rulePageUrl("format/page-limit"),
|
|
484
639
|
},
|
|
485
640
|
messages: {
|
|
486
641
|
pages:
|
|
487
642
|
"{{what}}: {{got}}, over the limit {{max}} for {{venue}}/{{kind}} — a desk reject; cut the text",
|
|
488
|
-
|
|
489
|
-
"the
|
|
643
|
+
noReferences:
|
|
644
|
+
"{{venue}} limits the body as the pages up to the references, and no page of the PDF has a line reading «References» or «Bibliography» — so the body was NOT counted. Give the bibliography its heading",
|
|
645
|
+
unclear:
|
|
646
|
+
"could not tell where the body ends, so it was NOT counted against {{venue}}'s limit: {{why}}. Check the headings of the bibliography and the appendix, and that nothing before them reads «References» above a [1]",
|
|
647
|
+
},
|
|
648
|
+
},
|
|
649
|
+
anonymity: {
|
|
650
|
+
docs: {
|
|
651
|
+
description:
|
|
652
|
+
"a double-blind venue's PDF names none of the authors' declared identity — in its text, its metadata or its links",
|
|
653
|
+
url: rulePageUrl("anonymity/identity"),
|
|
654
|
+
},
|
|
655
|
+
messages: {
|
|
656
|
+
noIdentity: `{{venue}} reviews double-blind, and this paper declares no \`identity\`, so nothing in the PDF can be checked against the authors. Declare it in ${CONFIG_FILE} (the root's for every paper, or this paper's): "identity": ["Your Name", "your-handle", "you@example.org", "Your University", "your-project"]`,
|
|
657
|
+
leak: "{{where}}: «{{found}}» matches «{{token}}» in `identity`, and {{venue}} reviews double-blind — remove it, or refer to your own work in the third person",
|
|
490
658
|
},
|
|
491
659
|
},
|
|
492
660
|
"body-size": {
|
|
493
|
-
type: "suggestion",
|
|
494
661
|
docs: {
|
|
495
662
|
description:
|
|
496
|
-
"body font
|
|
663
|
+
"the body and reference font sizes are the venue template's, within the preset's tolerance",
|
|
664
|
+
url: rulePageUrl("pdf/body-size"),
|
|
497
665
|
},
|
|
498
666
|
messages: {
|
|
667
|
+
refPt:
|
|
668
|
+
"the reference font size is {{got}} pt, outside {{range}} pt for {{venue}} (allowing ±{{slop}} pt for the measuring drift) — fix the bibliography's font size",
|
|
499
669
|
body: "the body font size measures {{got}} pt against {{want}} ± {{tol}} pt for {{venue}}. The measurement is the mode of the rendered text, not the declared size — check \\documentclass and its options",
|
|
500
670
|
bodyMissing:
|
|
501
671
|
"the build did not measure a body font size — rebuild the paper",
|
|
@@ -509,25 +679,42 @@ export type VenueRuleName =
|
|
|
509
679
|
| "measured"
|
|
510
680
|
| "fonts"
|
|
511
681
|
| "geometry"
|
|
512
|
-
| "
|
|
513
|
-
| "body-size"
|
|
682
|
+
| "page-limit"
|
|
683
|
+
| "body-size"
|
|
684
|
+
| "anonymity";
|
|
685
|
+
|
|
686
|
+
/** The venue rules of the `pdf` plugin; `page-limit` and `anonymity` live in their own. */
|
|
687
|
+
export type PdfRuleName = Exclude<VenueRuleName, "page-limit" | "anonymity">;
|
|
514
688
|
|
|
515
689
|
/**
|
|
516
690
|
* The level each venue rule is on at in paperlint's own config, for every `paper.tex`. One owner:
|
|
517
691
|
* the config reads it, the docs describe it, a consumer overrides it in `rules`.
|
|
518
692
|
*/
|
|
519
693
|
export const VENUE_RULE_LEVELS: Readonly<
|
|
520
|
-
Record<`pdf/${
|
|
694
|
+
Record<`pdf/${PdfRuleName}`, "error" | "warn">
|
|
521
695
|
> = {
|
|
522
696
|
"pdf/fresh": "error",
|
|
523
697
|
"pdf/profile": "error",
|
|
524
698
|
"pdf/fonts": "error",
|
|
525
699
|
"pdf/geometry": "error",
|
|
526
|
-
"pdf/
|
|
527
|
-
"pdf/body-size": "warn",
|
|
700
|
+
"pdf/body-size": "error",
|
|
528
701
|
"pdf/measured": "warn",
|
|
529
702
|
};
|
|
530
703
|
|
|
704
|
+
/** The `anonymity` plugin's rule and its level, on for every `paper.tex` like the venue rules. */
|
|
705
|
+
export const ANONYMITY_RULE_LEVELS: Readonly<
|
|
706
|
+
Record<"anonymity/identity", "error">
|
|
707
|
+
> = {
|
|
708
|
+
"anonymity/identity": "error",
|
|
709
|
+
};
|
|
710
|
+
|
|
711
|
+
/** The `format` plugin's rule over the built PDF and its level, on for every `paper.tex`. */
|
|
712
|
+
export const PAGE_LIMIT_RULE_LEVELS: Readonly<
|
|
713
|
+
Record<"format/page-limit", "error">
|
|
714
|
+
> = {
|
|
715
|
+
"format/page-limit": "error",
|
|
716
|
+
};
|
|
717
|
+
|
|
531
718
|
/** The line of `\documentclass`, where the class and its options — most of the fixes — live. */
|
|
532
719
|
function reportLine(source: string): number {
|
|
533
720
|
const i = source
|
|
@@ -569,7 +756,7 @@ function rule(name: VenueRuleName, deps: VenueRuleDeps): VenueRuleModule {
|
|
|
569
756
|
/** The venue rules, as the rules of the `pdf` plugin (beside `last-page-balance`). */
|
|
570
757
|
export function venueRules(
|
|
571
758
|
deps: VenueRuleDeps,
|
|
572
|
-
): Record<
|
|
759
|
+
): Record<PdfRuleName, VenueRuleModule> {
|
|
573
760
|
// Every name listed: a record missing one is a compile error.
|
|
574
761
|
return {
|
|
575
762
|
fresh: rule("fresh", deps),
|
|
@@ -577,7 +764,20 @@ export function venueRules(
|
|
|
577
764
|
measured: rule("measured", deps),
|
|
578
765
|
fonts: rule("fonts", deps),
|
|
579
766
|
geometry: rule("geometry", deps),
|
|
580
|
-
limits: rule("limits", deps),
|
|
581
767
|
"body-size": rule("body-size", deps),
|
|
582
768
|
};
|
|
583
769
|
}
|
|
770
|
+
|
|
771
|
+
/** The rule of the `anonymity` plugin: `identity`. */
|
|
772
|
+
export function anonymityRules(
|
|
773
|
+
deps: VenueRuleDeps,
|
|
774
|
+
): Readonly<Record<"identity", VenueRuleModule>> {
|
|
775
|
+
return { identity: rule("anonymity", deps) };
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
/** The `format` plugin's rule over the built PDF: `page-limit` (beside the source's in `tex-venue-rules.ts`). */
|
|
779
|
+
export function pageLimitRules(
|
|
780
|
+
deps: VenueRuleDeps,
|
|
781
|
+
): Readonly<Record<"page-limit", VenueRuleModule>> {
|
|
782
|
+
return { "page-limit": rule("page-limit", deps) };
|
|
783
|
+
}
|